Bug 876277 - Convert the debugger frontend to use the EventEmitter instead of relying on DOM events, r=past
authorVictor Porof <vporof@mozilla.com>
Fri, 13 Sep 2013 16:23:14 +0300
changeset 147072 b4f56e9c5f9687e8e5ef711ffb7ad4cf35cddd83
parent 147071 12928639d2bffd56a6196683fdb5db27b3cc2223
child 147073 4c1d5a059c6fd98078e6fe8974d5147ad5f2769f
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewerspast
bugs876277
milestone26.0a1
Bug 876277 - Convert the debugger frontend to use the EventEmitter instead of relying on DOM events, r=past
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-toolbar.js
browser/devtools/debugger/debugger-view.js
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -9,20 +9,74 @@ const { classes: Cc, interfaces: Ci, uti
 
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
 const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
 const FRAME_STEP_CLEAR_DELAY = 100; // ms
 const CALL_STACK_PAGE_SIZE = 25; // frames
 
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+  // When the debugger's source editor instance finishes loading or unloading.
+  EDITOR_LOADED: "Debugger:EditorLoaded",
+  EDITOR_UNLOADED: "Debugger:EditorUnoaded",
+
+  // When new sources are received from the debugger server.
+  NEW_SOURCE: "Debugger:NewSource",
+  SOURCES_ADDED: "Debugger:SourcesAdded",
+
+  // When a source is shown in the source editor.
+  SOURCE_SHOWN: "Debugger:EditorSourceShown",
+  SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
+
+  // When scopes, variables, properties and watch expressions are fetched and
+  // displayed in the variables view.
+  FETCHED_SCOPES: "Debugger:FetchedScopes",
+  FETCHED_VARIABLES: "Debugger:FetchedVariables",
+  FETCHED_PROPERTIES: "Debugger:FetchedProperties",
+  FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
+
+  // When a breakpoint has been added or removed on the debugger server.
+  BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
+  BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
+
+  // When a breakpoint has been shown or hidden in the source editor.
+  BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
+  BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
+
+  // When a conditional breakpoint's popup is showing or hiding.
+  CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
+  CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding",
+
+  // When a file search was performed.
+  FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
+  FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",
+
+  // When a function search was performed.
+  FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
+  FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
+
+  // When a global text search was performed.
+  GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
+  GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
+
+  // After the stackframes are cleared and debugger won't pause anymore.
+  AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
+
+  // When the options popup is showing or hiding.
+  OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
+  OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
+};
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
@@ -638,17 +692,16 @@ StackFrames.prototype = {
 
     for (let frame of this.activeThread.cachedFrames) {
       let { depth, where: { url, line }, source } = frame;
       let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
       let location = NetworkHelper.convertToUnicode(unescape(url));
       let title = StackFrameUtils.getFrameTitle(frame);
       DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
     }
-
     if (this.currentFrameDepth == -1) {
       DebuggerView.StackFrames.selectedDepth = 0;
     }
     if (this.activeThread.moreFrames) {
       DebuggerView.StackFrames.dirty = true;
     }
   },
 
@@ -686,17 +739,18 @@ StackFrames.prototype = {
     // Ignore useless notifications.
     if (this.activeThread.cachedFrames.length) {
       return;
     }
     DebuggerView.StackFrames.empty();
     DebuggerView.Sources.unhighlightBreakpoint();
     DebuggerView.WatchExpressions.toggleContents(true);
     DebuggerView.Variables.empty(0);
-    window.dispatchEvent(document, "Debugger:AfterFramesCleared");
+
+    window.emit(EVENTS.AFTER_FRAMES_CLEARED);
   },
 
   /**
    * Marks the stack frame at the specified depth as selected and updates the
    * properties view with the stack frame's data.
    *
    * @param number aDepth
    *        The depth of the frame in the stack.
@@ -766,18 +820,20 @@ StackFrames.prototype = {
       // The innermost scope is always automatically expanded, because it
       // contains the variables in the current stack frame which are likely to
       // be inspected.
       if (innermost) {
         scope.expand();
       }
     } while ((environment = environment.parent));
 
-    // Signal that variables have been fetched.
-    window.dispatchEvent(document, "Debugger:FetchedVariables");
+    // Signal that scope environments have been shown and commit the current
+    // variables view hierarchy to briefly flash items that changed between the
+    // previous and current scope/variables/properties.
+    window.emit(EVENTS.FETCHED_SCOPES);
     DebuggerView.Variables.commitHierarchy();
   },
 
   /**
    * Loads more stack frames from the debugger server cache.
    */
   addMoreFrames: function() {
     this.activeThread.fillFrames(
@@ -854,18 +910,20 @@ StackFrames.prototype = {
         // Revert some of the custom watch expressions scope presentation flags,
         // so that they don't propagate to child items.
         expRef.switch = null;
         expRef.delete = null;
         expRef.descriptorTooltip = true;
         expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
       }
 
-      // Signal that watch expressions have been fetched.
-      window.dispatchEvent(document, "Debugger:FetchedWatchExpressions");
+      // Signal that watch expressions have been fetched and commit the
+      // current variables view hierarchy to briefly flash items that changed
+      // between the previous and current scope/variables/properties.
+      window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
       DebuggerView.Variables.commitHierarchy();
     });
   },
 
   /**
    * Updates a list of watch expressions to evaluate on each pause.
    * TODO: handle all of this server-side: Bug 832470, comment 14.
    */
@@ -1007,17 +1065,17 @@ SourceScripts.prototype = {
     }
 
     // If there are any stored breakpoints for this source, display them again,
     // both in the editor and the breakpoints pane.
     DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that a new source has been added.
-    window.dispatchEvent(document, "Debugger:AfterNewSource");
+    window.emit(EVENTS.NEW_SOURCE);
   },
 
   /**
    * Callback for the debugger's active thread getSources() method.
    */
   _onSourcesAdded: function(aResponse) {
     if (aResponse.error) {
       let msg = "Error getting sources: " + aResponse.message;
@@ -1048,17 +1106,17 @@ SourceScripts.prototype = {
     }
 
     // If there are any stored breakpoints for the sources, display them again,
     // both in the editor and the breakpoints pane.
     DebuggerController.Breakpoints.updateEditorBreakpoints();
     DebuggerController.Breakpoints.updatePaneBreakpoints();
 
     // Signal that sources have been added.
-    window.dispatchEvent(document, "Debugger:AfterSourcesAdded");
+    window.emit(EVENTS.SOURCES_ADDED);
   },
 
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
   _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
     const item = DebuggerView.Sources.getItemByValue(url);
     if (item) {
@@ -1269,17 +1327,17 @@ Breakpoints.prototype = {
       // If the breakpoint client has an "requestedLocation" attached, then
       // the original requested placement for the breakpoint wasn't accepted.
       // In this case, we need to update the editor with the new location.
       if (aBreakpointClient.requestedLocation) {
         DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1);
         DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1);
       }
       // Notify that we've shown a breakpoint in the source editor.
-      window.dispatchEvent(document, "Debugger:BreakpointShown", aEditorBreakpoint);
+      window.emit(EVENTS.BREAKPOINT_SHOWN, aEditorBreakpoint);
     });
   },
 
   /**
    * Event handler for breakpoints that are removed from the editor.
    *
    * @param object aEditorBreakpoint
    *        The breakpoint object that was removed from the editor.
@@ -1288,17 +1346,17 @@ Breakpoints.prototype = {
     let url = DebuggerView.Sources.selectedValue;
     let line = aEditorBreakpoint.line + 1;
     let location = { url: url, line: line };
 
     // Destroy the breakpoint, but don't update the editor, since this callback
     // is invoked because a breakpoint was removed from the editor itself.
     this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
       // Notify that we've hidden a breakpoint in the source editor.
-      window.dispatchEvent(document, "Debugger:BreakpointHidden", aEditorBreakpoint);
+      window.emit(EVENTS.BREAKPOINT_HIDDEN, aEditorBreakpoint);
     });
   },
 
   /**
    * Update the breakpoints in the editor view. This function takes the list of
    * breakpoints in the debugger and adds them back into the editor view.
    * This is invoked when the selected script is changed, or when new sources
    * are received via the _onNewSource and _onSourcesAdded event listeners.
@@ -1401,16 +1459,19 @@ Breakpoints.prototype = {
       // in the sources pane without requiring fetching the source (for example,
       // after the target navigated). Note that this will get out of sync
       // if the source text contents change.
       let line = aBreakpointClient.location.line - 1;
       aBreakpointClient.text = DebuggerView.getEditorLineText(line).trim();
 
       // Show the breakpoint in the editor and breakpoints pane, and resolve.
       this._showBreakpoint(aBreakpointClient, aOptions);
+
+      // Notify that we've added a breakpoint.
+      window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient);
       deferred.resolve(aBreakpointClient);
     });
 
     return deferred.promise;
   },
 
   /**
    * Remove a breakpoint.
@@ -1461,16 +1522,19 @@ Breakpoints.prototype = {
         }
 
         // Forget both the initialization and removal promises from the store.
         this._added.delete(identifier);
         this._removing.delete(identifier);
 
         // Hide the breakpoint from the editor and breakpoints pane, and resolve.
         this._hideBreakpoint(aLocation, aOptions);
+
+        // Notify that we've removed a breakpoint.
+        window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation);
         deferred.resolve(aLocation);
       });
     });
 
     return deferred.promise;
   },
 
   /**
@@ -1627,16 +1691,21 @@ let Prefs = new ViewHelpers.Prefs("devto
  * @return boolean
  */
 XPCOMUtils.defineLazyGetter(window, "_isChromeDebugger", function() {
   // We're inside a single top level XUL window in a different process.
   return !(window.frameElement instanceof XULElement);
 });
 
 /**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
  * Preliminary setup for the DebuggerController object.
  */
 DebuggerController.initialize();
 DebuggerController.Parser = new Parser();
 DebuggerController.ThreadState = new ThreadState();
 DebuggerController.StackFrames = new StackFrames();
 DebuggerController.SourceScripts = new SourceScripts();
 DebuggerController.Breakpoints = new Breakpoints();
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -47,18 +47,18 @@ SourcesView.prototype = Heritage.extend(
     this._commandset = document.getElementById("debuggerCommands");
     this._popupset = document.getElementById("debuggerPopupset");
     this._cmPopup = document.getElementById("sourceEditorContextMenu");
     this._cbPanel = document.getElementById("conditional-breakpoint-panel");
     this._cbTextbox = document.getElementById("conditional-breakpoint-panel-textbox");
     this._editorDeck = document.getElementById("editor-deck");
     this._stopBlackBoxButton = document.getElementById("black-boxed-message-button");
 
-    window.addEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
-    window.addEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
+    window.on(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
+    window.on(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
     this.widget.addEventListener("select", this._onSourceSelect, false);
     this.widget.addEventListener("click", this._onSourceClick, false);
     this.widget.addEventListener("check", this._onSourceCheck, false);
     this._stopBlackBoxButton.addEventListener("click", this._onStopBlackBoxing, false);
     this._cbPanel.addEventListener("popupshowing", this._onConditionalPopupShowing, false);
     this._cbPanel.addEventListener("popupshown", this._onConditionalPopupShown, false);
     this._cbPanel.addEventListener("popuphiding", this._onConditionalPopupHiding, false);
     this._cbTextbox.addEventListener("input", this._onConditionalTextboxInput, false);
@@ -71,18 +71,18 @@ SourcesView.prototype = Heritage.extend(
   },
 
   /**
    * Destruction function, called when the debugger is closed.
    */
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
-    window.removeEventListener("Debugger:EditorLoaded", this._onEditorLoad, false);
-    window.removeEventListener("Debugger:EditorUnloaded", this._onEditorUnload, false);
+    window.off(EVENTS.EDITOR_LOADED, this._onEditorLoad, false);
+    window.off(EVENTS.EDITOR_UNLOADED, this._onEditorUnload, false);
     this.widget.removeEventListener("select", this._onSourceSelect, false);
     this.widget.removeEventListener("click", this._onSourceClick, false);
     this.widget.removeEventListener("check", this._onSourceCheck, false);
     this._stopBlackBoxButton.removeEventListener("click", this._onStopBlackBoxing, false);
     this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShowing, false);
     this._cbPanel.removeEventListener("popupshowing", this._onConditionalPopupShown, false);
     this._cbPanel.removeEventListener("popuphiding", this._onConditionalPopupHiding, false);
     this._cbTextbox.removeEventListener("input", this._onConditionalTextboxInput, false);
@@ -595,27 +595,27 @@ SourcesView.prototype = Heritage.extend(
     if (this._selectedBreakpointItem == aItem) {
       this._selectedBreakpointItem = null;
     }
   },
 
   /**
    * The load listener for the source editor.
    */
-  _onEditorLoad: function({ detail: editor }) {
-    editor.addEventListener("Selection", this._onEditorSelection, false);
-    editor.addEventListener("ContextMenu", this._onEditorContextMenu, false);
+  _onEditorLoad: function(aName, aEditor) {
+    aEditor.addEventListener(SourceEditor.EVENTS.SELECTION, this._onEditorSelection, false);
+    aEditor.addEventListener(SourceEditor.EVENTS.CONTEXT_MENU, this._onEditorContextMenu, false);
   },
 
   /**
    * The unload listener for the source editor.
    */
-  _onEditorUnload: function({ detail: editor }) {
-    editor.removeEventListener("Selection", this._onEditorSelection, false);
-    editor.removeEventListener("ContextMenu", this._onEditorContextMenu, false);
+  _onEditorUnload: function(aName, aEditor) {
+    aEditor.removeEventListener(SourceEditor.EVENTS.SELECTION, this._onEditorSelection, false);
+    aEditor.removeEventListener(SourceEditor.EVENTS.CONTEXT_MENU, this._onEditorContextMenu, false);
   },
 
   /**
    * The selection listener for the source editor.
    */
   _onEditorSelection: function(e) {
     let { start, end } = e.newValue;
 
@@ -735,31 +735,33 @@ SourcesView.prototype = Heritage.extend(
     e.stopPropagation();
   },
 
   /**
    * The popup showing listener for the breakpoints conditional expression panel.
    */
   _onConditionalPopupShowing: function() {
     this._conditionalPopupVisible = true; // Used in tests.
+    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
   },
 
   /**
    * The popup shown listener for the breakpoints conditional expression panel.
    */
   _onConditionalPopupShown: function() {
     this._cbTextbox.focus();
     this._cbTextbox.select();
   },
 
   /**
    * The popup hiding listener for the breakpoints conditional expression panel.
    */
   _onConditionalPopupHiding: function() {
     this._conditionalPopupVisible = false; // Used in tests.
+    window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
   },
 
   /**
    * The input listener for the breakpoints conditional expression textbox.
    */
   _onConditionalTextboxInput: function() {
     let breakpointItem = this._selectedBreakpointItem;
     let attachment = breakpointItem.attachment;
@@ -1512,17 +1514,16 @@ GlobalSearchView.prototype = Heritage.ex
     this._splitter.getAttribute("hidden") == "true",
 
   /**
    * Hides and removes all items from this search container.
    */
   clearView: function() {
     this.hidden = true;
     this.empty();
-    window.dispatchEvent(document, "Debugger:GlobalSearch:ViewCleared");
   },
 
   /**
    * Selects the next found item in this container.
    * Does not change the currently focused node.
    */
   selectNext: function() {
     let totalLineResults = LineResults.size();
@@ -1584,17 +1585,16 @@ GlobalSearchView.prototype = Heritage.ex
    *        The string to search for.
    * @param array aSources
    *        An array of [url, text] tuples for each source.
    */
   _doSearch: function(aToken, aSources) {
     // Don't continue filtering if the searched token is an empty string.
     if (!aToken) {
       this.clearView();
-      window.dispatchEvent(document, "Debugger:GlobalSearch:TokenEmpty");
       return;
     }
 
     // Search is not case sensitive, prepare the actual searched token.
     let lowerCaseToken = aToken.toLowerCase();
     let tokenLength = aToken.length;
 
     // Create a Map containing search details for each source.
@@ -1652,19 +1652,19 @@ GlobalSearchView.prototype = Heritage.ex
       }
     }
 
     // Rebuild the results, then signal if there are any matches.
     if (globalResults.matchCount) {
       this.hidden = false;
       this._currentlyFocusedMatch = -1;
       this._createGlobalResultsUI(globalResults);
-      window.dispatchEvent(document, "Debugger:GlobalSearch:MatchFound");
+      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_FOUND);
     } else {
-      window.dispatchEvent(document, "Debugger:GlobalSearch:MatchNotFound");
+      window.emit(EVENTS.GLOBAL_SEARCH_MATCH_NOT_FOUND);
     }
   },
 
   /**
    * Creates global search results entries and adds them to this container.
    *
    * @param GlobalResults aGlobalResults
    *        An object containing all source results, grouped by source location.
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -228,30 +228,31 @@ OptionsView.prototype = {
     // Nothing to do here yet.
   },
 
   /**
    * Listener handling the 'gear menu' popup showing event.
    */
   _onPopupShowing: function() {
     this._button.setAttribute("open", "true");
+    window.emit(EVENTS.OPTIONS_POPUP_SHOWING);
   },
 
   /**
    * Listener handling the 'gear menu' popup hiding event.
    */
   _onPopupHiding: function() {
     this._button.removeAttribute("open");
   },
 
   /**
    * Listener handling the 'gear menu' popup hidden event.
    */
   _onPopupHidden: function() {
-    window.dispatchEvent(document, "Debugger:OptionsPopupHidden");
+    window.emit(EVENTS.OPTIONS_POPUP_HIDDEN);
   },
 
   /**
    * Listener handling the 'pause on exceptions' menuitem command.
    */
   _togglePauseOnExceptions: function() {
     Prefs.pauseOnExceptions =
       this._pauseOnExceptionsItem.getAttribute("checked") == "true";
@@ -301,19 +302,17 @@ OptionsView.prototype = {
   /**
    * Listener handling the 'show original source' menuitem command.
    */
   _toggleShowOriginalSource: function() {
     let pref = Prefs.sourceMapsEnabled =
       this._showOriginalSourceItem.getAttribute("checked") == "true";
 
     // Don't block the UI while reconfiguring the server.
-    window.addEventListener("Debugger:OptionsPopupHidden", function onHidden() {
-      window.removeEventListener("Debugger:OptionsPopupHidden", onHidden, false);
-
+    window.once(EVENTS.OPTIONS_POPUP_HIDDEN, () => {
       // The popup panel needs more time to hide after triggering onpopuphidden.
       window.setTimeout(() => {
         DebuggerController.reconfigureThread(pref);
       }, POPUP_HIDDEN_DELAY);
     }, false);
   },
 
   _button: null,
@@ -1303,16 +1302,17 @@ FilteredSourcesView.prototype = Heritage
    *
    * @param array aSearchResults
    *        The results array, containing search details for each source.
    */
   _syncView: function(aSearchResults) {
     // If there are no matches found, keep the popup hidden and avoid
     // creating the view.
     if (!aSearchResults.length) {
+      window.emit(EVENTS.FILE_SEARCH_MATCH_NOT_FOUND);
       return;
     }
 
     for (let item of aSearchResults) {
       // Append a location item to this container for each match.
       let trimmedLabel = SourceUtils.trimUrlLength(item.label);
       let trimmedValue = SourceUtils.trimUrlLength(item.value, 0, "start");
 
@@ -1323,16 +1323,19 @@ FilteredSourcesView.prototype = Heritage
           url: item.value
         }
       });
     }
 
     // Select the first entry in this container.
     this.selectedIndex = 0;
     this.hidden = false;
+
+    // Signal that file search matches were found and displayed.
+    window.emit(EVENTS.FILE_SEARCH_MATCH_FOUND);
   },
 
   /**
    * The click listener for this container.
    */
   _onClick: function(e) {
     let locationItem = this.getItemForElement(e.target);
     if (locationItem) {
@@ -1474,16 +1477,17 @@ FilteredFunctionsView.prototype = Herita
    *
    * @param array aSearchResults
    *        The results array, containing search details for each source.
    */
   _syncView: function(aSearchResults) {
     // If there are no matches found, keep the popup hidden and avoid
     // creating the view.
     if (!aSearchResults.length) {
+      window.emit(EVENTS.FUNCTION_SEARCH_MATCH_NOT_FOUND);
       return;
     }
 
     for (let item of aSearchResults) {
       // Some function expressions don't necessarily have a name, but the
       // parser provides us with an inferred name from an enclosing
       // VariableDeclarator, AssignmentExpression, ObjectExpression node.
       if (item.functionName && item.inferredName &&
@@ -1518,16 +1522,19 @@ FilteredFunctionsView.prototype = Herita
         relaxed: true, /* this container should allow dupes & degenerates */
         attachment: item
       });
     }
 
     // Select the first entry in this container.
     this.selectedIndex = 0;
     this.hidden = false;
+
+    // Signal that function search matches were found and displayed.
+    window.emit(EVENTS.FUNCTION_SEARCH_MATCH_FOUND);
   },
 
   /**
    * The click listener for this container.
    */
   _onClick: function(e) {
     let functionItem = this.getItemForElement(e.target);
     if (functionItem) {
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -150,20 +150,20 @@ let DebuggerView = {
     VariablesViewController.attach(this.Variables, {
       getObjectClient: aObject => gThreadClient.pauseGrip(aObject)
     });
 
     // Relay events from the VariablesView.
     this.Variables.on("fetched", (aEvent, aType) => {
       switch (aType) {
         case "variables":
-          window.dispatchEvent(document, "Debugger:FetchedVariables");
+          window.emit(EVENTS.FETCHED_VARIABLES);
           break;
         case "properties":
-          window.dispatchEvent(document, "Debugger:FetchedProperties");
+          window.emit(EVENTS.FETCHED_PROPERTIES);
           break;
       }
     });
   },
 
   /**
    * Initializes the SourceEditor instance.
    *
@@ -186,33 +186,33 @@ let DebuggerView = {
    *
    * @param function aCallback
    *        Called after the editor finishes loading.
    */
   _onEditorLoad: function(aCallback) {
     dumpn("Finished loading the DebuggerView editor");
 
     DebuggerController.Breakpoints.initialize().then(() => {
-      window.dispatchEvent(document, "Debugger:EditorLoaded", this.editor);
+      window.emit(EVENTS.EDITOR_LOADED, this.editor);
       aCallback();
     });
   },
 
   /**
    * Destroys the SourceEditor instance and also executes any necessary
    * post-unload operations.
    *
    * @param function aCallback
    *        Called after the editor finishes destroying.
    */
   _destroyEditor: function(aCallback) {
     dumpn("Destroying the DebuggerView editor");
 
     DebuggerController.Breakpoints.destroy().then(() => {
-      window.dispatchEvent(document, "Debugger:EditorUnloaded", this.editor);
+      window.emit(EVENTS.EDITOR_UNLOADED, this.editor);
       aCallback();
     });
   },
 
   /**
    * Sets the currently displayed text contents in the source editor.
    * This resets the mode and undo stack.
    *
@@ -295,27 +295,27 @@ let DebuggerView = {
       this._setEditorText(aText);
       this._setEditorMode(aSource.url, aSource.contentType, aText);
 
       // Synchronize any other components with the currently displayed source.
       DebuggerView.Sources.selectedValue = aSource.url;
       DebuggerController.Breakpoints.updateEditorBreakpoints();
 
       // Resolve and notify that a source file was shown.
-      window.dispatchEvent(document, "Debugger:SourceShown", aSource);
+      window.emit(EVENTS.SOURCE_SHOWN, aSource);
       deferred.resolve([aSource, aText]);
     },
     ([, aError]) => {
       let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError);
       this._setEditorText(msg);
       Cu.reportError(msg);
       dumpn(msg);
 
       // Reject and notify that there was an error showing the source file.
-      window.dispatchEvent(document, "Debugger:SourceErrorShown", aError);
+      window.emit(EVENTS.SOURCE_ERROR_SHOWN, aSource);
       deferred.reject([aSource, aError]);
     });
 
     return deferred.promise;
   },
 
   /**
    * Update the source editor's current caret and debug location based on