browser/devtools/debugger/debugger-controller.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/. */
     6 "use strict";
     8 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
    10 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
    11 const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
    12 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
    13 const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
    14 const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
    15 const FRAME_STEP_CLEAR_DELAY = 100; // ms
    16 const CALL_STACK_PAGE_SIZE = 25; // frames
    18 // The panel's window global is an EventEmitter firing the following events:
    19 const EVENTS = {
    20   // When the debugger's source editor instance finishes loading or unloading.
    21   EDITOR_LOADED: "Debugger:EditorLoaded",
    22   EDITOR_UNLOADED: "Debugger:EditorUnoaded",
    24   // When new sources are received from the debugger server.
    25   NEW_SOURCE: "Debugger:NewSource",
    26   SOURCES_ADDED: "Debugger:SourcesAdded",
    28   // When a source is shown in the source editor.
    29   SOURCE_SHOWN: "Debugger:EditorSourceShown",
    30   SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
    32   // When the editor has shown a source and set the line / column position
    33   EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
    35   // When scopes, variables, properties and watch expressions are fetched and
    36   // displayed in the variables view.
    37   FETCHED_SCOPES: "Debugger:FetchedScopes",
    38   FETCHED_VARIABLES: "Debugger:FetchedVariables",
    39   FETCHED_PROPERTIES: "Debugger:FetchedProperties",
    40   FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
    41   FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
    43   // When a breakpoint has been added or removed on the debugger server.
    44   BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
    45   BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
    47   // When a breakpoint has been shown or hidden in the source editor.
    48   BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
    49   BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
    51   // When a conditional breakpoint's popup is showing or hiding.
    52   CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
    53   CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding",
    55   // When event listeners are fetched or event breakpoints are updated.
    56   EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
    57   EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",
    59   // When a file search was performed.
    60   FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
    61   FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",
    63   // When a function search was performed.
    64   FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
    65   FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
    67   // When a global text search was performed.
    68   GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
    69   GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
    71   // After the stackframes are cleared and debugger won't pause anymore.
    72   AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
    74   // When the options popup is showing or hiding.
    75   OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
    76   OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
    78   // When the widgets layout has been changed.
    79   LAYOUT_CHANGED: "Debugger:LayoutChanged"
    80 };
    82 // Descriptions for what a stack frame represents after the debugger pauses.
    83 const FRAME_TYPE = {
    84   NORMAL: 0,
    85   CONDITIONAL_BREAKPOINT_EVAL: 1,
    86   WATCH_EXPRESSIONS_EVAL: 2,
    87   PUBLIC_CLIENT_EVAL: 3
    88 };
    90 Cu.import("resource://gre/modules/Services.jsm");
    91 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    92 Cu.import("resource://gre/modules/devtools/event-emitter.js");
    93 Cu.import("resource://gre/modules/Task.jsm");
    94 Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
    95 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
    96 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
    97 Cu.import("resource:///modules/devtools/VariablesView.jsm");
    98 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
    99 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
   101 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
   102 const promise = require("devtools/toolkit/deprecated-sync-thenables");
   103 const Editor = require("devtools/sourceeditor/editor");
   104 const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
   105 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
   106 const FastListWidget = require("devtools/shared/widgets/FastListWidget");
   108 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   109   "resource:///modules/devtools/Parser.jsm");
   111 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   112   "resource://gre/modules/devtools/Loader.jsm");
   114 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
   115   "resource://gre/modules/devtools/DevToolsUtils.jsm");
   117 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
   118   "resource://gre/modules/ShortcutUtils.jsm");
   120 Object.defineProperty(this, "NetworkHelper", {
   121   get: function() {
   122     return devtools.require("devtools/toolkit/webconsole/network-helper");
   123   },
   124   configurable: true,
   125   enumerable: true
   126 });
   128 /**
   129  * Object defining the debugger controller components.
   130  */
   131 let DebuggerController = {
   132   /**
   133    * Initializes the debugger controller.
   134    */
   135   initialize: function() {
   136     dumpn("Initializing the DebuggerController");
   138     this.startupDebugger = this.startupDebugger.bind(this);
   139     this.shutdownDebugger = this.shutdownDebugger.bind(this);
   140     this._onTabNavigated = this._onTabNavigated.bind(this);
   141     this._onTabDetached = this._onTabDetached.bind(this);
   142   },
   144   /**
   145    * Initializes the view.
   146    *
   147    * @return object
   148    *         A promise that is resolved when the debugger finishes startup.
   149    */
   150   startupDebugger: function() {
   151     if (this._startup) {
   152       return this._startup;
   153     }
   155     return this._startup = DebuggerView.initialize();
   156   },
   158   /**
   159    * Destroys the view and disconnects the debugger client from the server.
   160    *
   161    * @return object
   162    *         A promise that is resolved when the debugger finishes shutdown.
   163    */
   164   shutdownDebugger: function() {
   165     if (this._shutdown) {
   166       return this._shutdown;
   167     }
   169     return this._shutdown = DebuggerView.destroy().then(() => {
   170       DebuggerView.destroy();
   171       this.SourceScripts.disconnect();
   172       this.StackFrames.disconnect();
   173       this.ThreadState.disconnect();
   174       this.Tracer.disconnect();
   175       this.disconnect();
   176     });
   177   },
   179   /**
   180    * Initiates remote debugging based on the current target, wiring event
   181    * handlers as necessary.
   182    *
   183    * @return object
   184    *         A promise that is resolved when the debugger finishes connecting.
   185    */
   186   connect: function() {
   187     if (this._connection) {
   188       return this._connection;
   189     }
   191     let startedDebugging = promise.defer();
   192     this._connection = startedDebugging.promise;
   194     let target = this._target;
   195     let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
   196     target.on("close", this._onTabDetached);
   197     target.on("navigate", this._onTabNavigated);
   198     target.on("will-navigate", this._onTabNavigated);
   199     this.client = client;
   201     if (addonActor) {
   202       this._startAddonDebugging(addonActor, startedDebugging.resolve);
   203     } else if (target.chrome) {
   204       this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
   205     } else {
   206       this._startDebuggingTab(startedDebugging.resolve);
   207       const startedTracing = promise.defer();
   208       if (Prefs.tracerEnabled && traceActor) {
   209         this._startTracingTab(traceActor, startedTracing.resolve);
   210       } else {
   211         startedTracing.resolve();
   212       }
   214       return promise.all([startedDebugging.promise, startedTracing.promise]);
   215     }
   217     return startedDebugging.promise;
   218   },
   220   /**
   221    * Disconnects the debugger client and removes event handlers as necessary.
   222    */
   223   disconnect: function() {
   224     // Return early if the client didn't even have a chance to instantiate.
   225     if (!this.client) {
   226       return;
   227     }
   229     this._connection = null;
   230     this.client = null;
   231     this.activeThread = null;
   232   },
   234   /**
   235    * Called for each location change in the debugged tab.
   236    *
   237    * @param string aType
   238    *        Packet type.
   239    * @param object aPacket
   240    *        Packet received from the server.
   241    */
   242   _onTabNavigated: function(aType, aPacket) {
   243     switch (aType) {
   244       case "will-navigate": {
   245         // Reset UI.
   246         DebuggerView.handleTabNavigation();
   248         // Discard all the cached sources *before* the target starts navigating.
   249         // Sources may be fetched during navigation, in which case we don't
   250         // want to hang on to the old source contents.
   251         DebuggerController.SourceScripts.clearCache();
   252         DebuggerController.Parser.clearCache();
   253         SourceUtils.clearCache();
   255         // Prevent performing any actions that were scheduled before navigation.
   256         clearNamedTimeout("new-source");
   257         clearNamedTimeout("event-breakpoints-update");
   258         clearNamedTimeout("event-listeners-fetch");
   259         break;
   260       }
   261       case "navigate": {
   262         this.ThreadState.handleTabNavigation();
   263         this.StackFrames.handleTabNavigation();
   264         this.SourceScripts.handleTabNavigation();
   265         break;
   266       }
   267     }
   268   },
   270   /**
   271    * Called when the debugged tab is closed.
   272    */
   273   _onTabDetached: function() {
   274     this.shutdownDebugger();
   275   },
   277   /**
   278    * Warn if resuming execution produced a wrongOrder error.
   279    */
   280   _ensureResumptionOrder: function(aResponse) {
   281     if (aResponse.error == "wrongOrder") {
   282       DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
   283     }
   284   },
   286   /**
   287    * Sets up a debugging session.
   288    *
   289    * @param function aCallback
   290    *        A function to invoke once the client attaches to the active thread.
   291    */
   292   _startDebuggingTab: function(aCallback) {
   293     this._target.activeTab.attachThread({
   294       useSourceMaps: Prefs.sourceMapsEnabled
   295     }, (aResponse, aThreadClient) => {
   296       if (!aThreadClient) {
   297         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
   298         return;
   299       }
   300       this.activeThread = aThreadClient;
   302       this.ThreadState.connect();
   303       this.StackFrames.connect();
   304       this.SourceScripts.connect();
   305       if (aThreadClient.paused) {
   306         aThreadClient.resume(this._ensureResumptionOrder);
   307       }
   309       if (aCallback) {
   310         aCallback();
   311       }
   312     });
   313   },
   315   /**
   316    * Sets up an addon debugging session.
   317    *
   318    * @param object aAddonActor
   319    *        The actor for the addon that is being debugged.
   320    * @param function aCallback
   321    *        A function to invoke once the client attaches to the active thread.
   322    */
   323   _startAddonDebugging: function(aAddonActor, aCallback) {
   324     this.client.attachAddon(aAddonActor, (aResponse) => {
   325       return this._startChromeDebugging(aResponse.threadActor, aCallback);
   326     });
   327   },
   329   /**
   330    * Sets up a chrome debugging session.
   331    *
   332    * @param object aChromeDebugger
   333    *        The remote protocol grip of the chrome debugger.
   334    * @param function aCallback
   335    *        A function to invoke once the client attaches to the active thread.
   336    */
   337   _startChromeDebugging: function(aChromeDebugger, aCallback) {
   338     this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
   339       if (!aThreadClient) {
   340         Cu.reportError("Couldn't attach to thread: " + aResponse.error);
   341         return;
   342       }
   343       this.activeThread = aThreadClient;
   345       this.ThreadState.connect();
   346       this.StackFrames.connect();
   347       this.SourceScripts.connect();
   348       if (aThreadClient.paused) {
   349         aThreadClient.resume(this._ensureResumptionOrder);
   350       }
   352       if (aCallback) {
   353         aCallback();
   354       }
   355     }, { useSourceMaps: Prefs.sourceMapsEnabled });
   356   },
   358   /**
   359    * Sets up an execution tracing session.
   360    *
   361    * @param object aTraceActor
   362    *        The remote protocol grip of the trace actor.
   363    * @param function aCallback
   364    *        A function to invoke once the client attaches to the tracer.
   365    */
   366   _startTracingTab: function(aTraceActor, aCallback) {
   367     this.client.attachTracer(aTraceActor, (response, traceClient) => {
   368       if (!traceClient) {
   369         DevToolsUtils.reportException("DebuggerController._startTracingTab",
   370                                       new Error("Failed to attach to tracing actor."));
   371         return;
   372       }
   374       this.traceClient = traceClient;
   375       this.Tracer.connect();
   377       if (aCallback) {
   378         aCallback();
   379       }
   380     });
   381   },
   383   /**
   384    * Detach and reattach to the thread actor with useSourceMaps true, blow
   385    * away old sources and get them again.
   386    */
   387   reconfigureThread: function(aUseSourceMaps) {
   388     this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => {
   389       if (aResponse.error) {
   390         let msg = "Couldn't reconfigure thread: " + aResponse.message;
   391         Cu.reportError(msg);
   392         dumpn(msg);
   393         return;
   394       }
   396       // Reset the view and fetch all the sources again.
   397       DebuggerView.handleTabNavigation();
   398       this.SourceScripts.handleTabNavigation();
   400       // Update the stack frame list.
   401       if (this.activeThread.paused) {
   402         this.activeThread._clearFrames();
   403         this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
   404       }
   405     });
   406   },
   408   _startup: null,
   409   _shutdown: null,
   410   _connection: null,
   411   client: null,
   412   activeThread: null
   413 };
   415 /**
   416  * ThreadState keeps the UI up to date with the state of the
   417  * thread (paused/attached/etc.).
   418  */
   419 function ThreadState() {
   420   this._update = this._update.bind(this);
   421 }
   423 ThreadState.prototype = {
   424   get activeThread() DebuggerController.activeThread,
   426   /**
   427    * Connect to the current thread client.
   428    */
   429   connect: function() {
   430     dumpn("ThreadState is connecting...");
   431     this.activeThread.addListener("paused", this._update);
   432     this.activeThread.addListener("resumed", this._update);
   433     this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions,
   434                                         Prefs.ignoreCaughtExceptions);
   435     this.handleTabNavigation();
   436   },
   438   /**
   439    * Disconnect from the client.
   440    */
   441   disconnect: function() {
   442     if (!this.activeThread) {
   443       return;
   444     }
   445     dumpn("ThreadState is disconnecting...");
   446     this.activeThread.removeListener("paused", this._update);
   447     this.activeThread.removeListener("resumed", this._update);
   448   },
   450   /**
   451    * Handles any initialization on a tab navigation event issued by the client.
   452    */
   453   handleTabNavigation: function() {
   454     if (!this.activeThread) {
   455       return;
   456     }
   457     dumpn("Handling tab navigation in the ThreadState");
   458     this._update();
   459   },
   461   /**
   462    * Update the UI after a thread state change.
   463    */
   464   _update: function(aEvent) {
   465     DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
   467     if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
   468       gTarget.emit("thread-" + aEvent);
   469     }
   470   }
   471 };
   473 /**
   474  * Keeps the stack frame list up-to-date, using the thread client's
   475  * stack frame cache.
   476  */
   477 function StackFrames() {
   478   this._onPaused = this._onPaused.bind(this);
   479   this._onResumed = this._onResumed.bind(this);
   480   this._onFrames = this._onFrames.bind(this);
   481   this._onFramesCleared = this._onFramesCleared.bind(this);
   482   this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
   483   this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
   484   this._afterFramesCleared = this._afterFramesCleared.bind(this);
   485   this.evaluate = this.evaluate.bind(this);
   486 }
   488 StackFrames.prototype = {
   489   get activeThread() DebuggerController.activeThread,
   490   currentFrameDepth: -1,
   491   _currentFrameDescription: FRAME_TYPE.NORMAL,
   492   _syncedWatchExpressions: null,
   493   _currentWatchExpressions: null,
   494   _currentBreakpointLocation: null,
   495   _currentEvaluation: null,
   496   _currentException: null,
   497   _currentReturnedValue: null,
   499   /**
   500    * Connect to the current thread client.
   501    */
   502   connect: function() {
   503     dumpn("StackFrames is connecting...");
   504     this.activeThread.addListener("paused", this._onPaused);
   505     this.activeThread.addListener("resumed", this._onResumed);
   506     this.activeThread.addListener("framesadded", this._onFrames);
   507     this.activeThread.addListener("framescleared", this._onFramesCleared);
   508     this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
   509     this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
   510     this.handleTabNavigation();
   511   },
   513   /**
   514    * Disconnect from the client.
   515    */
   516   disconnect: function() {
   517     if (!this.activeThread) {
   518       return;
   519     }
   520     dumpn("StackFrames is disconnecting...");
   521     this.activeThread.removeListener("paused", this._onPaused);
   522     this.activeThread.removeListener("resumed", this._onResumed);
   523     this.activeThread.removeListener("framesadded", this._onFrames);
   524     this.activeThread.removeListener("framescleared", this._onFramesCleared);
   525     this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
   526     this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
   527     clearNamedTimeout("frames-cleared");
   528   },
   530   /**
   531    * Handles any initialization on a tab navigation event issued by the client.
   532    */
   533   handleTabNavigation: function() {
   534     dumpn("Handling tab navigation in the StackFrames");
   535     // Nothing to do here yet.
   536   },
   538   /**
   539    * Handler for the thread client's paused notification.
   540    *
   541    * @param string aEvent
   542    *        The name of the notification ("paused" in this case).
   543    * @param object aPacket
   544    *        The response packet.
   545    */
   546   _onPaused: function(aEvent, aPacket) {
   547     switch (aPacket.why.type) {
   548       // If paused by a breakpoint, store the breakpoint location.
   549       case "breakpoint":
   550         this._currentBreakpointLocation = aPacket.frame.where;
   551         break;
   552       // If paused by a client evaluation, store the evaluated value.
   553       case "clientEvaluated":
   554         this._currentEvaluation = aPacket.why.frameFinished;
   555         break;
   556       // If paused by an exception, store the exception value.
   557       case "exception":
   558         this._currentException = aPacket.why.exception;
   559         break;
   560       // If paused while stepping out of a frame, store the returned value or
   561       // thrown exception.
   562       case "resumeLimit":
   563         if (!aPacket.why.frameFinished) {
   564           break;
   565         } else if (aPacket.why.frameFinished.throw) {
   566           this._currentException = aPacket.why.frameFinished.throw;
   567         } else if (aPacket.why.frameFinished.return) {
   568           this._currentReturnedValue = aPacket.why.frameFinished.return;
   569         }
   570         break;
   571     }
   573     this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
   574     DebuggerView.editor.focus();
   575   },
   577   /**
   578    * Handler for the thread client's resumed notification.
   579    */
   580   _onResumed: function() {
   581     // Prepare the watch expression evaluation string for the next pause.
   582     if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
   583       this._currentWatchExpressions = this._syncedWatchExpressions;
   584     }
   585   },
   587   /**
   588    * Handler for the thread client's framesadded notification.
   589    */
   590   _onFrames: function() {
   591     // Ignore useless notifications.
   592     if (!this.activeThread || !this.activeThread.cachedFrames.length) {
   593       return;
   594     }
   596     let waitForNextPause = false;
   597     let breakLocation = this._currentBreakpointLocation;
   598     let watchExpressions = this._currentWatchExpressions;
   599     let client = DebuggerController.activeThread.client;
   601     // We moved conditional breakpoint handling to the server, but
   602     // need to support it in the client for a while until most of the
   603     // server code in production is updated with it. bug 990137 is
   604     // filed to mark this code to be removed.
   605     if (!client.mainRoot.traits.conditionalBreakpoints) {
   606       // Conditional breakpoints are { breakpoint, expression } tuples. The
   607       // boolean evaluation of the expression decides if the active thread
   608       // automatically resumes execution or not.
   609       if (breakLocation) {
   610         // Make sure a breakpoint actually exists at the specified url and line.
   611         let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
   612         if (breakpointPromise) {
   613           breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
   614             // Evaluating the current breakpoint's conditional expression will
   615             // cause the stack frames to be cleared and active thread to pause,
   616             // sending a 'clientEvaluated' packed and adding the frames again.
   617             this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
   618             waitForNextPause = true;
   619           }});
   620         }
   621       }
   622       // We'll get our evaluation of the current breakpoint's conditional
   623       // expression the next time the thread client pauses...
   624       if (waitForNextPause) {
   625         return;
   626       }
   627       if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
   628         this._currentFrameDescription = FRAME_TYPE.NORMAL;
   629         // If the breakpoint's conditional expression evaluation is falsy,
   630         // automatically resume execution.
   631         if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
   632           this.activeThread.resume(DebuggerController._ensureResumptionOrder);
   633           return;
   634         }
   635       }
   636     }
   638     // Watch expressions are evaluated in the context of the topmost frame,
   639     // and the results are displayed in the variables view.
   640     // TODO: handle all of this server-side: Bug 832470, comment 14.
   641     if (watchExpressions) {
   642       // Evaluation causes the stack frames to be cleared and active thread to
   643       // pause, sending a 'clientEvaluated' packet and adding the frames again.
   644       this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
   645       waitForNextPause = true;
   646     }
   647     // We'll get our evaluation of the current watch expressions the next time
   648     // the thread client pauses...
   649     if (waitForNextPause) {
   650       return;
   651     }
   652     if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
   653       this._currentFrameDescription = FRAME_TYPE.NORMAL;
   654       // If an error was thrown during the evaluation of the watch expressions,
   655       // then at least one expression evaluation could not be performed. So
   656       // remove the most recent watch expression and try again.
   657       if (this._currentEvaluation.throw) {
   658         DebuggerView.WatchExpressions.removeAt(0);
   659         DebuggerController.StackFrames.syncWatchExpressions();
   660         return;
   661       }
   662     }
   664     // Make sure the debugger view panes are visible, then refill the frames.
   665     DebuggerView.showInstrumentsPane();
   666     this._refillFrames();
   668     // No additional processing is necessary for this stack frame.
   669     if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
   670       this._currentFrameDescription = FRAME_TYPE.NORMAL;
   671     }
   672   },
   674   /**
   675    * Fill the StackFrames view with the frames we have in the cache, compressing
   676    * frames which have black boxed sources into single frames.
   677    */
   678   _refillFrames: function() {
   679     // Make sure all the previous stackframes are removed before re-adding them.
   680     DebuggerView.StackFrames.empty();
   682     for (let frame of this.activeThread.cachedFrames) {
   683       let { depth, where: { url, line }, source } = frame;
   684       let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
   685       let location = NetworkHelper.convertToUnicode(unescape(url));
   686       let title = StackFrameUtils.getFrameTitle(frame);
   687       DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
   688     }
   690     DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
   691     DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
   692   },
   694   /**
   695    * Handler for the thread client's framescleared notification.
   696    */
   697   _onFramesCleared: function() {
   698     switch (this._currentFrameDescription) {
   699       case FRAME_TYPE.NORMAL:
   700         this._currentEvaluation = null;
   701         this._currentException = null;
   702         this._currentReturnedValue = null;
   703         break;
   704       case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL:
   705         this._currentBreakpointLocation = null;
   706         break;
   707       case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL:
   708         this._currentWatchExpressions = null;
   709         break;
   710     }
   712     // After each frame step (in, over, out), framescleared is fired, which
   713     // forces the UI to be emptied and rebuilt on framesadded. Most of the times
   714     // this is not necessary, and will result in a brief redraw flicker.
   715     // To avoid it, invalidate the UI only after a short time if necessary.
   716     setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared);
   717   },
   719   /**
   720    * Handler for the debugger's blackboxchange notification.
   721    */
   722   _onBlackBoxChange: function() {
   723     if (this.activeThread.state == "paused") {
   724       // Hack to avoid selecting the topmost frame after blackboxing a source.
   725       this.currentFrameDepth = NaN;
   726       this._refillFrames();
   727     }
   728   },
   730   /**
   731    * Handler for the debugger's prettyprintchange notification.
   732    */
   733   _onPrettyPrintChange: function() {
   734     // Makes sure the selected source remains selected
   735     // after the fillFrames is called.
   736     const source = DebuggerView.Sources.selectedValue;
   737     if (this.activeThread.state == "paused") {
   738       this.activeThread.fillFrames(
   739          CALL_STACK_PAGE_SIZE,
   740          () => DebuggerView.Sources.selectedValue = source);
   741     }
   742   },
   744   /**
   745    * Called soon after the thread client's framescleared notification.
   746    */
   747   _afterFramesCleared: function() {
   748     // Ignore useless notifications.
   749     if (this.activeThread.cachedFrames.length) {
   750       return;
   751     }
   752     DebuggerView.editor.clearDebugLocation();
   753     DebuggerView.StackFrames.empty();
   754     DebuggerView.Sources.unhighlightBreakpoint();
   755     DebuggerView.WatchExpressions.toggleContents(true);
   756     DebuggerView.Variables.empty(0);
   758     window.emit(EVENTS.AFTER_FRAMES_CLEARED);
   759   },
   761   /**
   762    * Marks the stack frame at the specified depth as selected and updates the
   763    * properties view with the stack frame's data.
   764    *
   765    * @param number aDepth
   766    *        The depth of the frame in the stack.
   767    */
   768   selectFrame: function(aDepth) {
   769     // Make sure the frame at the specified depth exists first.
   770     let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth];
   771     if (!frame) {
   772       return;
   773     }
   775     // Check if the frame does not represent the evaluation of debuggee code.
   776     let { environment, where } = frame;
   777     if (!environment) {
   778       return;
   779     }
   781     // Don't change the editor's location if the execution was paused by a
   782     // public client evaluation. This is useful for adding overlays on
   783     // top of the editor, like a variable inspection popup.
   784     let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
   785     let isPopupShown = DebuggerView.VariableBubble.contentsShown();
   786     if (!isClientEval && !isPopupShown) {
   787       // Move the editor's caret to the proper url and line.
   788       DebuggerView.setEditorLocation(where.url, where.line);
   789       // Highlight the breakpoint at the specified url and line if it exists.
   790       DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true });
   791     }
   793     // Don't display the watch expressions textbox inputs in the pane.
   794     DebuggerView.WatchExpressions.toggleContents(false);
   796     // Start recording any added variables or properties in any scope and
   797     // clear existing scopes to create each one dynamically.
   798     DebuggerView.Variables.empty();
   800     // If watch expressions evaluation results are available, create a scope
   801     // to contain all the values.
   802     if (this._syncedWatchExpressions && aDepth == 0) {
   803       let label = L10N.getStr("watchExpressionsScopeLabel");
   804       let scope = DebuggerView.Variables.addScope(label);
   806       // Customize the scope for holding watch expressions evaluations.
   807       scope.descriptorTooltip = false;
   808       scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
   809       scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel");
   810       scope.switch = DebuggerView.WatchExpressions.switchExpression;
   811       scope.delete = DebuggerView.WatchExpressions.deleteExpression;
   813       // The evaluation hasn't thrown, so fetch and add the returned results.
   814       this._fetchWatchExpressions(scope, this._currentEvaluation.return);
   816       // The watch expressions scope is always automatically expanded.
   817       scope.expand();
   818     }
   820     do {
   821       // Create a scope to contain all the inspected variables in the
   822       // current environment.
   823       let label = StackFrameUtils.getScopeLabel(environment);
   824       let scope = DebuggerView.Variables.addScope(label);
   825       let innermost = environment == frame.environment;
   827       // Handle special additions to the innermost scope.
   828       if (innermost) {
   829         this._insertScopeFrameReferences(scope, frame);
   830       }
   832       // Handle the expansion of the scope, lazily populating it with the
   833       // variables in the current environment.
   834       DebuggerView.Variables.controller.addExpander(scope, environment);
   836       // The innermost scope is always automatically expanded, because it
   837       // contains the variables in the current stack frame which are likely to
   838       // be inspected.
   839       if (innermost) {
   840         scope.expand();
   841       }
   842     } while ((environment = environment.parent));
   844     // Signal that scope environments have been shown.
   845     window.emit(EVENTS.FETCHED_SCOPES);
   846   },
   848   /**
   849    * Loads more stack frames from the debugger server cache.
   850    */
   851   addMoreFrames: function() {
   852     this.activeThread.fillFrames(
   853       this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
   854   },
   856   /**
   857    * Evaluate an expression in the context of the selected frame.
   858    *
   859    * @param string aExpression
   860    *        The expression to evaluate.
   861    * @param object aOptions [optional]
   862    *        Additional options for this client evaluation:
   863    *          - depth: the frame depth used for evaluation, 0 being the topmost.
   864    *          - meta: some meta-description for what this evaluation represents.
   865    * @return object
   866    *         A promise that is resolved when the evaluation finishes,
   867    *         or rejected if there was no stack frame available or some
   868    *         other error occurred.
   869    */
   870   evaluate: function(aExpression, aOptions = {}) {
   871     let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth;
   872     let frame = this.activeThread.cachedFrames[depth];
   873     if (frame == null) {
   874       return promise.reject(new Error("No stack frame available."));
   875     }
   877     let deferred = promise.defer();
   879     this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
   880       let { type, frameFinished } = aPacket.why;
   881       if (type == "clientEvaluated") {
   882         if (!("terminated" in frameFinished)) {
   883           deferred.resolve(frameFinished);
   884         } else {
   885           deferred.reject(new Error("The execution was abruptly terminated."));
   886         }
   887       } else {
   888         deferred.reject(new Error("Active thread paused unexpectedly."));
   889       }
   890     });
   892     let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
   893     this._currentFrameDescription = meta;
   894     this.activeThread.eval(frame.actor, aExpression);
   896     return deferred.promise;
   897   },
   899   /**
   900    * Add nodes for special frame references in the innermost scope.
   901    *
   902    * @param Scope aScope
   903    *        The scope where the references will be placed into.
   904    * @param object aFrame
   905    *        The frame to get some references from.
   906    */
   907   _insertScopeFrameReferences: function(aScope, aFrame) {
   908     // Add any thrown exception.
   909     if (this._currentException) {
   910       let excRef = aScope.addItem("<exception>", { value: this._currentException });
   911       DebuggerView.Variables.controller.addExpander(excRef, this._currentException);
   912     }
   913     // Add any returned value.
   914     if (this._currentReturnedValue) {
   915       let retRef = aScope.addItem("<return>", { value: this._currentReturnedValue });
   916       DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue);
   917     }
   918     // Add "this".
   919     if (aFrame.this) {
   920       let thisRef = aScope.addItem("this", { value: aFrame.this });
   921       DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
   922     }
   923   },
   925   /**
   926    * Adds the watch expressions evaluation results to a scope in the view.
   927    *
   928    * @param Scope aScope
   929    *        The scope where the watch expressions will be placed into.
   930    * @param object aExp
   931    *        The grip of the evaluation results.
   932    */
   933   _fetchWatchExpressions: function(aScope, aExp) {
   934     // Fetch the expressions only once.
   935     if (aScope._fetched) {
   936       return;
   937     }
   938     aScope._fetched = true;
   940     // Add nodes for every watch expression in scope.
   941     this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => {
   942       let ownProperties = aResponse.ownProperties;
   943       let totalExpressions = DebuggerView.WatchExpressions.itemCount;
   945       for (let i = 0; i < totalExpressions; i++) {
   946         let name = DebuggerView.WatchExpressions.getString(i);
   947         let expVal = ownProperties[i].value;
   948         let expRef = aScope.addItem(name, ownProperties[i]);
   949         DebuggerView.Variables.controller.addExpander(expRef, expVal);
   951         // Revert some of the custom watch expressions scope presentation flags,
   952         // so that they don't propagate to child items.
   953         expRef.switch = null;
   954         expRef.delete = null;
   955         expRef.descriptorTooltip = true;
   956         expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
   957       }
   959       // Signal that watch expressions have been fetched.
   960       window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
   961     });
   962   },
   964   /**
   965    * Updates a list of watch expressions to evaluate on each pause.
   966    * TODO: handle all of this server-side: Bug 832470, comment 14.
   967    */
   968   syncWatchExpressions: function() {
   969     let list = DebuggerView.WatchExpressions.getAllStrings();
   971     // Sanity check all watch expressions before syncing them. To avoid
   972     // having the whole watch expressions array throw because of a single
   973     // faulty expression, simply convert it to a string describing the error.
   974     // There's no other information necessary to be offered in such cases.
   975     let sanitizedExpressions = list.map(aString => {
   976       // Reflect.parse throws when it encounters a syntax error.
   977       try {
   978         Parser.reflectionAPI.parse(aString);
   979         return aString; // Watch expression can be executed safely.
   980       } catch (e) {
   981         return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
   982       }
   983     });
   985     if (sanitizedExpressions.length) {
   986       this._syncedWatchExpressions =
   987         this._currentWatchExpressions =
   988           "[" +
   989             sanitizedExpressions.map(aString =>
   990               "eval(\"" +
   991                 "try {" +
   992                   // Make sure all quotes are escaped in the expression's syntax,
   993                   // and add a newline after the statement to avoid comments
   994                   // breaking the code integrity inside the eval block.
   995                   aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
   996                 "} catch (e) {" +
   997                   "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
   998                 "}" +
   999               "\")"
  1000             ).join(",") +
  1001           "]";
  1002     } else {
  1003       this._syncedWatchExpressions =
  1004         this._currentWatchExpressions = null;
  1007     this.currentFrameDepth = -1;
  1008     this._onFrames();
  1010 };
  1012 /**
  1013  * Keeps the source script list up-to-date, using the thread client's
  1014  * source script cache.
  1015  */
  1016 function SourceScripts() {
  1017   this._onNewGlobal = this._onNewGlobal.bind(this);
  1018   this._onNewSource = this._onNewSource.bind(this);
  1019   this._onSourcesAdded = this._onSourcesAdded.bind(this);
  1020   this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
  1021   this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
  1024 SourceScripts.prototype = {
  1025   get activeThread() DebuggerController.activeThread,
  1026   get debuggerClient() DebuggerController.client,
  1027   _cache: new Map(),
  1029   /**
  1030    * Connect to the current thread client.
  1031    */
  1032   connect: function() {
  1033     dumpn("SourceScripts is connecting...");
  1034     this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
  1035     this.debuggerClient.addListener("newSource", this._onNewSource);
  1036     this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
  1037     this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
  1038     this.handleTabNavigation();
  1039   },
  1041   /**
  1042    * Disconnect from the client.
  1043    */
  1044   disconnect: function() {
  1045     if (!this.activeThread) {
  1046       return;
  1048     dumpn("SourceScripts is disconnecting...");
  1049     this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
  1050     this.debuggerClient.removeListener("newSource", this._onNewSource);
  1051     this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
  1052     this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
  1053   },
  1055   /**
  1056    * Clears all the cached source contents.
  1057    */
  1058   clearCache: function() {
  1059     this._cache.clear();
  1060   },
  1062   /**
  1063    * Handles any initialization on a tab navigation event issued by the client.
  1064    */
  1065   handleTabNavigation: function() {
  1066     if (!this.activeThread) {
  1067       return;
  1069     dumpn("Handling tab navigation in the SourceScripts");
  1071     // Retrieve the list of script sources known to the server from before
  1072     // the client was ready to handle "newSource" notifications.
  1073     this.activeThread.getSources(this._onSourcesAdded);
  1074   },
  1076   /**
  1077    * Handler for the debugger client's unsolicited newGlobal notification.
  1078    */
  1079   _onNewGlobal: function(aNotification, aPacket) {
  1080     // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
  1081     // from bug 801084.
  1082   },
  1084   /**
  1085    * Handler for the debugger client's unsolicited newSource notification.
  1086    */
  1087   _onNewSource: function(aNotification, aPacket) {
  1088     // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
  1089     if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) {
  1090       return;
  1093     // Add the source in the debugger view sources container.
  1094     DebuggerView.Sources.addSource(aPacket.source, { staged: false });
  1096     // Select this source if it's the preferred one.
  1097     let preferredValue = DebuggerView.Sources.preferredValue;
  1098     if (aPacket.source.url == preferredValue) {
  1099       DebuggerView.Sources.selectedValue = preferredValue;
  1101     // ..or the first entry if there's none selected yet after a while
  1102     else {
  1103       setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
  1104         // If after a certain delay the preferred source still wasn't received,
  1105         // just give up on waiting and display the first entry.
  1106         if (!DebuggerView.Sources.selectedValue) {
  1107           DebuggerView.Sources.selectedIndex = 0;
  1109       });
  1112     // If there are any stored breakpoints for this source, display them again,
  1113     // both in the editor and the breakpoints pane.
  1114     DebuggerController.Breakpoints.updateEditorBreakpoints();
  1115     DebuggerController.Breakpoints.updatePaneBreakpoints();
  1117     // Make sure the events listeners are up to date.
  1118     if (DebuggerView.instrumentsPaneTab == "events-tab") {
  1119       DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
  1122     // Signal that a new source has been added.
  1123     window.emit(EVENTS.NEW_SOURCE);
  1124   },
  1126   /**
  1127    * Callback for the debugger's active thread getSources() method.
  1128    */
  1129   _onSourcesAdded: function(aResponse) {
  1130     if (aResponse.error) {
  1131       let msg = "Error getting sources: " + aResponse.message;
  1132       Cu.reportError(msg);
  1133       dumpn(msg);
  1134       return;
  1137     if (aResponse.sources.length === 0) {
  1138       DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText");
  1139       window.emit(EVENTS.SOURCES_ADDED);
  1140       return;
  1143     // Add all the sources in the debugger view sources container.
  1144     for (let source of aResponse.sources) {
  1145       // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
  1146       if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) {
  1147         DebuggerView.Sources.addSource(source, { staged: true });
  1151     // Flushes all the prepared sources into the sources container.
  1152     DebuggerView.Sources.commit({ sorted: true });
  1154     // Select the preferred source if it exists and was part of the response.
  1155     let preferredValue = DebuggerView.Sources.preferredValue;
  1156     if (DebuggerView.Sources.containsValue(preferredValue)) {
  1157       DebuggerView.Sources.selectedValue = preferredValue;
  1159     // ..or the first entry if there's no one selected yet.
  1160     else if (!DebuggerView.Sources.selectedValue) {
  1161       DebuggerView.Sources.selectedIndex = 0;
  1164     // If there are any stored breakpoints for the sources, display them again,
  1165     // both in the editor and the breakpoints pane.
  1166     DebuggerController.Breakpoints.updateEditorBreakpoints();
  1167     DebuggerController.Breakpoints.updatePaneBreakpoints();
  1169     // Signal that sources have been added.
  1170     window.emit(EVENTS.SOURCES_ADDED);
  1171   },
  1173   /**
  1174    * Handler for the debugger client's 'blackboxchange' notification.
  1175    */
  1176   _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
  1177     const item = DebuggerView.Sources.getItemByValue(url);
  1178     if (item) {
  1179       if (isBlackBoxed) {
  1180         item.target.classList.add("black-boxed");
  1181       } else {
  1182         item.target.classList.remove("black-boxed");
  1185     DebuggerView.Sources.updateToolbarButtonsState();
  1186     DebuggerView.maybeShowBlackBoxMessage();
  1187   },
  1189   /**
  1190    * Set the black boxed status of the given source.
  1192    * @param Object aSource
  1193    *        The source form.
  1194    * @param bool aBlackBoxFlag
  1195    *        True to black box the source, false to un-black box it.
  1196    * @returns Promise
  1197    *          A promize that resolves to [aSource, isBlackBoxed] or rejects to
  1198    *          [aSource, error].
  1199    */
  1200   setBlackBoxing: function(aSource, aBlackBoxFlag) {
  1201     const sourceClient = this.activeThread.source(aSource);
  1202     const deferred = promise.defer();
  1204     sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => {
  1205       const { error, message } = aPacket;
  1206       if (error) {
  1207         let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message;
  1208         dumpn(msg);
  1209         Cu.reportError(msg);
  1210         deferred.reject([aSource, msg]);
  1211       } else {
  1212         deferred.resolve([aSource, sourceClient.isBlackBoxed]);
  1214     });
  1216     return deferred.promise;
  1217   },
  1219   /**
  1220    * Toggle the pretty printing of a source's text. All subsequent calls to
  1221    * |getText| will return the pretty-toggled text. Nothing will happen for
  1222    * non-javascript files.
  1224    * @param Object aSource
  1225    *        The source form from the RDP.
  1226    * @returns Promise
  1227    *          A promise that resolves to [aSource, prettyText] or rejects to
  1228    *          [aSource, error].
  1229    */
  1230   togglePrettyPrint: function(aSource) {
  1231     // Only attempt to pretty print JavaScript sources.
  1232     if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) {
  1233       return promise.reject([aSource, "Can't prettify non-javascript files."]);
  1236     const sourceClient = this.activeThread.source(aSource);
  1237     const wantPretty = !sourceClient.isPrettyPrinted;
  1239     // Only use the existing promise if it is pretty printed.
  1240     let textPromise = this._cache.get(aSource.url);
  1241     if (textPromise && textPromise.pretty === wantPretty) {
  1242       return textPromise;
  1245     const deferred = promise.defer();
  1246     deferred.promise.pretty = wantPretty;
  1247     this._cache.set(aSource.url, deferred.promise);
  1249     const afterToggle = ({ error, message, source: text, contentType }) => {
  1250       if (error) {
  1251         // Revert the rejected promise from the cache, so that the original
  1252         // source's text may be shown when the source is selected.
  1253         this._cache.set(aSource.url, textPromise);
  1254         deferred.reject([aSource, message || error]);
  1255         return;
  1257       deferred.resolve([aSource, text, contentType]);
  1258     };
  1260     if (wantPretty) {
  1261       sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle);
  1262     } else {
  1263       sourceClient.disablePrettyPrint(afterToggle);
  1266     return deferred.promise;
  1267   },
  1269   /**
  1270    * Handler for the debugger's prettyprintchange notification.
  1271    */
  1272   _onPrettyPrintChange: function(aEvent, { url }) {
  1273     // Remove the cached source AST from the Parser, to avoid getting
  1274     // wrong locations when searching for functions.
  1275     DebuggerController.Parser.clearSource(url);
  1276   },
  1278   /**
  1279    * Gets a specified source's text.
  1281    * @param object aSource
  1282    *        The source object coming from the active thread.
  1283    * @param function aOnTimeout [optional]
  1284    *        Function called when the source text takes a long time to fetch,
  1285    *        but not necessarily failing. Long fetch times don't cause the
  1286    *        rejection of the returned promise.
  1287    * @param number aDelay [optional]
  1288    *        The amount of time it takes to consider a source slow to fetch.
  1289    *        If unspecified, it defaults to a predefined value.
  1290    * @return object
  1291    *         A promise that is resolved after the source text has been fetched.
  1292    */
  1293   getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) {
  1294     // Fetch the source text only once.
  1295     let textPromise = this._cache.get(aSource.url);
  1296     if (textPromise) {
  1297       return textPromise;
  1300     let deferred = promise.defer();
  1301     this._cache.set(aSource.url, deferred.promise);
  1303     // If the source text takes a long time to fetch, invoke a callback.
  1304     if (aOnTimeout) {
  1305       var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
  1308     // Get the source text from the active thread.
  1309     this.activeThread.source(aSource)
  1310         .source(({ error, message, source: text, contentType }) => {
  1311       if (aOnTimeout) {
  1312         window.clearTimeout(fetchTimeout);
  1314       if (error) {
  1315         deferred.reject([aSource, message || error]);
  1316       } else {
  1317         deferred.resolve([aSource, text, contentType]);
  1319     });
  1321     return deferred.promise;
  1322   },
  1324   /**
  1325    * Starts fetching all the sources, silently.
  1327    * @param array aUrls
  1328    *        The urls for the sources to fetch. If fetching a source's text
  1329    *        takes too long, it will be discarded.
  1330    * @return object
  1331    *         A promise that is resolved after source texts have been fetched.
  1332    */
  1333   getTextForSources: function(aUrls) {
  1334     let deferred = promise.defer();
  1335     let pending = new Set(aUrls);
  1336     let fetched = [];
  1338     // Can't use promise.all, because if one fetch operation is rejected, then
  1339     // everything is considered rejected, thus no other subsequent source will
  1340     // be getting fetched. We don't want that. Something like Q's allSettled
  1341     // would work like a charm here.
  1343     // Try to fetch as many sources as possible.
  1344     for (let url of aUrls) {
  1345       let sourceItem = DebuggerView.Sources.getItemByValue(url);
  1346       let sourceForm = sourceItem.attachment.source;
  1347       this.getText(sourceForm, onTimeout).then(onFetch, onError);
  1350     /* Called if fetching a source takes too long. */
  1351     function onTimeout(aSource) {
  1352       onError([aSource]);
  1355     /* Called if fetching a source finishes successfully. */
  1356     function onFetch([aSource, aText, aContentType]) {
  1357       // If fetching the source has previously timed out, discard it this time.
  1358       if (!pending.has(aSource.url)) {
  1359         return;
  1361       pending.delete(aSource.url);
  1362       fetched.push([aSource.url, aText, aContentType]);
  1363       maybeFinish();
  1366     /* Called if fetching a source failed because of an error. */
  1367     function onError([aSource, aError]) {
  1368       pending.delete(aSource.url);
  1369       maybeFinish();
  1372     /* Called every time something interesting happens while fetching sources. */
  1373     function maybeFinish() {
  1374       if (pending.size == 0) {
  1375         // Sort the fetched sources alphabetically by their url.
  1376         deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
  1380     return deferred.promise;
  1382 };
  1384 /**
  1385  * Tracer update the UI according to the messages exchanged with the tracer
  1386  * actor.
  1387  */
  1388 function Tracer() {
  1389   this._trace = null;
  1390   this._idCounter = 0;
  1391   this.onTraces = this.onTraces.bind(this);
  1394 Tracer.prototype = {
  1395   get client() {
  1396     return DebuggerController.client;
  1397   },
  1399   get traceClient() {
  1400     return DebuggerController.traceClient;
  1401   },
  1403   get tracing() {
  1404     return !!this._trace;
  1405   },
  1407   /**
  1408    * Hooks up the debugger controller with the tracer client.
  1409    */
  1410   connect: function() {
  1411     this._stack = [];
  1412     this.client.addListener("traces", this.onTraces);
  1413   },
  1415   /**
  1416    * Disconnects the debugger controller from the tracer client. Any further
  1417    * communcation with the tracer actor will not have any effect on the UI.
  1418    */
  1419   disconnect: function() {
  1420     this._stack = null;
  1421     this.client.removeListener("traces", this.onTraces);
  1422   },
  1424   /**
  1425    * Instructs the tracer actor to start tracing.
  1426    */
  1427   startTracing: function(aCallback = () => {}) {
  1428     DebuggerView.Tracer.selectTab();
  1429     if (this.tracing) {
  1430       return;
  1432     this._trace = "dbg.trace" + Math.random();
  1433     this.traceClient.startTrace([
  1434       "name",
  1435       "location",
  1436       "parameterNames",
  1437       "depth",
  1438       "arguments",
  1439       "return",
  1440       "throw",
  1441       "yield"
  1442     ], this._trace, (aResponse) => {
  1443       const { error } = aResponse;
  1444       if (error) {
  1445         DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
  1446         this._trace = null;
  1449       aCallback(aResponse);
  1450     });
  1451   },
  1453   /**
  1454    * Instructs the tracer actor to stop tracing.
  1455    */
  1456   stopTracing: function(aCallback = () => {}) {
  1457     if (!this.tracing) {
  1458       return;
  1460     this.traceClient.stopTrace(this._trace, aResponse => {
  1461       const { error } = aResponse;
  1462       if (error) {
  1463         DevToolsUtils.reportException("Tracer.prototype.stopTracing", error);
  1466       this._trace = null;
  1467       aCallback(aResponse);
  1468     });
  1469   },
  1471   onTraces: function (aEvent, { traces }) {
  1472     const tracesLength = traces.length;
  1473     let tracesToShow;
  1474     if (tracesLength > TracerView.MAX_TRACES) {
  1475       tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
  1476                                   tracesLength);
  1477       DebuggerView.Tracer.empty();
  1478       this._stack.splice(0, this._stack.length);
  1479     } else {
  1480       tracesToShow = traces;
  1483     for (let t of tracesToShow) {
  1484       if (t.type == "enteredFrame") {
  1485         this._onCall(t);
  1486       } else {
  1487         this._onReturn(t);
  1491     DebuggerView.Tracer.commit();
  1492   },
  1494   /**
  1495    * Callback for handling a new call frame.
  1496    */
  1497   _onCall: function({ name, location, parameterNames, depth, arguments: args }) {
  1498     const item = {
  1499       name: name,
  1500       location: location,
  1501       id: this._idCounter++
  1502     };
  1504     this._stack.push(item);
  1505     DebuggerView.Tracer.addTrace({
  1506       type: "call",
  1507       name: name,
  1508       location: location,
  1509       depth: depth,
  1510       parameterNames: parameterNames,
  1511       arguments: args,
  1512       frameId: item.id
  1513     });
  1514   },
  1516   /**
  1517    * Callback for handling an exited frame.
  1518    */
  1519   _onReturn: function(aPacket) {
  1520     if (!this._stack.length) {
  1521       return;
  1524     const { name, id, location } = this._stack.pop();
  1525     DebuggerView.Tracer.addTrace({
  1526       type: aPacket.why,
  1527       name: name,
  1528       location: location,
  1529       depth: aPacket.depth,
  1530       frameId: id,
  1531       returnVal: aPacket.return || aPacket.throw || aPacket.yield
  1532     });
  1533   },
  1535   /**
  1536    * Create an object which has the same interface as a normal object client,
  1537    * but since we already have all the information for an object that we will
  1538    * ever get (the server doesn't create actors when tracing, just firehoses
  1539    * data and forgets about it) just return the data immdiately.
  1541    * @param Object aObject
  1542    *        The tracer object "grip" (more like a limited snapshot).
  1543    * @returns Object
  1544    *          The synchronous client object.
  1545    */
  1546   syncGripClient: function(aObject) {
  1547     return {
  1548       get isFrozen() { return aObject.frozen; },
  1549       get isSealed() { return aObject.sealed; },
  1550       get isExtensible() { return aObject.extensible; },
  1552       get ownProperties() { return aObject.ownProperties; },
  1553       get prototype() { return null; },
  1555       getParameterNames: callback => callback(aObject),
  1556       getPrototypeAndProperties: callback => callback(aObject),
  1557       getPrototype: callback => callback(aObject),
  1559       getOwnPropertyNames: (callback) => {
  1560         callback({
  1561           ownPropertyNames: aObject.ownProperties
  1562             ? Object.keys(aObject.ownProperties)
  1563             : []
  1564         });
  1565       },
  1567       getProperty: (property, callback) => {
  1568         callback({
  1569           descriptor: aObject.ownProperties
  1570             ? aObject.ownProperties[property]
  1571             : null
  1572         });
  1573       },
  1575       getDisplayString: callback => callback("[object " + aObject.class + "]"),
  1577       getScope: callback => callback({
  1578         error: "scopeNotAvailable",
  1579         message: "Cannot get scopes for traced objects"
  1580       })
  1581     };
  1582   },
  1584   /**
  1585    * Wraps object snapshots received from the tracer server so that we can
  1586    * differentiate them from long living object grips from the debugger server
  1587    * in the variables view.
  1589    * @param Object aObject
  1590    *        The object snapshot from the tracer actor.
  1591    */
  1592   WrappedObject: function(aObject) {
  1593     this.object = aObject;
  1595 };
  1597 /**
  1598  * Handles breaking on event listeners in the currently debugged target.
  1599  */
  1600 function EventListeners() {
  1601   this._onEventListeners = this._onEventListeners.bind(this);
  1604 EventListeners.prototype = {
  1605   /**
  1606    * A list of event names on which the debuggee will automatically pause
  1607    * when invoked.
  1608    */
  1609   activeEventNames: [],
  1611   /**
  1612    * Updates the list of events types with listeners that, when invoked,
  1613    * will automatically pause the debuggee. The respective events are
  1614    * retrieved from the UI.
  1615    */
  1616   scheduleEventBreakpointsUpdate: function() {
  1617     // Make sure we're not sending a batch of closely repeated requests.
  1618     // This can easily happen when toggling all events of a certain type.
  1619     setNamedTimeout("event-breakpoints-update", 0, () => {
  1620       this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents();
  1621       gThreadClient.pauseOnDOMEvents(this.activeEventNames);
  1623       // Notify that event breakpoints were added/removed on the server.
  1624       window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
  1625     });
  1626   },
  1628   /**
  1629    * Fetches the currently attached event listeners from the debugee.
  1630    */
  1631   scheduleEventListenersFetch: function() {
  1632     let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
  1633       if (aResponse.error) {
  1634         let msg = "Error getting event listeners: " + aResponse.message;
  1635         DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
  1636         return;
  1639       let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
  1640         const deferred = promise.defer();
  1642         gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
  1643           if (aResponse.error) {
  1644             const msg = "Error getting function definition site: " + aResponse.message;
  1645             DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
  1646           } else {
  1647             aListener.function.url = aResponse.url;
  1650           deferred.resolve(aListener);
  1651         });
  1653         return deferred.promise;
  1654       });
  1656       promise.all(outstandingListenersDefinitionSite).then(aListeners => {
  1657         this._onEventListeners(aListeners);
  1659         // Notify that event listeners were fetched and shown in the view,
  1660         // and callback to resume the active thread if necessary.
  1661         window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
  1662         aCallback && aCallback();
  1663       });
  1664     });
  1666     // Make sure we're not sending a batch of closely repeated requests.
  1667     // This can easily happen whenever new sources are fetched.
  1668     setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
  1669       if (gThreadClient.state != "paused") {
  1670         gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
  1671       } else {
  1672         getListeners();
  1674     });
  1675   },
  1677   /**
  1678    * Callback for a debugger's successful active thread eventListeners() call.
  1679    */
  1680   _onEventListeners: function(aListeners) {
  1681     // Add all the listeners in the debugger view event linsteners container.
  1682     for (let listener of aListeners) {
  1683       DebuggerView.EventListeners.addListener(listener, { staged: true });
  1686     // Flushes all the prepared events into the event listeners container.
  1687     DebuggerView.EventListeners.commit();
  1689 };
  1691 /**
  1692  * Handles all the breakpoints in the current debugger.
  1693  */
  1694 function Breakpoints() {
  1695   this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
  1696   this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
  1697   this.addBreakpoint = this.addBreakpoint.bind(this);
  1698   this.removeBreakpoint = this.removeBreakpoint.bind(this);
  1701 Breakpoints.prototype = {
  1702   /**
  1703    * A map of breakpoint promises as tracked by the debugger frontend.
  1704    * The keys consist of a string representation of the breakpoint location.
  1705    */
  1706   _added: new Map(),
  1707   _removing: new Map(),
  1708   _disabled: new Map(),
  1710   /**
  1711    * Adds the source editor breakpoint handlers.
  1713    * @return object
  1714    *         A promise that is resolved when the breakpoints finishes initializing.
  1715    */
  1716   initialize: function() {
  1717     DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd);
  1718     DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove);
  1720     // Initialization is synchronous, for now.
  1721     return promise.resolve(null);
  1722   },
  1724   /**
  1725    * Removes the source editor breakpoint handlers & all the added breakpoints.
  1727    * @return object
  1728    *         A promise that is resolved when the breakpoints finishes destroying.
  1729    */
  1730   destroy: function() {
  1731     DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd);
  1732     DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove);
  1734     return this.removeAllBreakpoints();
  1735   },
  1737   /**
  1738    * Event handler for new breakpoints that come from the editor.
  1740    * @param number aLine
  1741    *        Line number where breakpoint was set.
  1742    */
  1743   _onEditorBreakpointAdd: function(_, aLine) {
  1744     let url = DebuggerView.Sources.selectedValue;
  1745     let location = { url: url, line: aLine + 1 };
  1747     // Initialize the breakpoint, but don't update the editor, since this
  1748     // callback is invoked because a breakpoint was added in the editor itself.
  1749     this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => {
  1750       // If the breakpoint client has an "requestedLocation" attached, then
  1751       // the original requested placement for the breakpoint wasn't accepted.
  1752       // In this case, we need to update the editor with the new location.
  1753       if (aBreakpointClient.requestedLocation) {
  1754         DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1);
  1755         DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1);
  1757       // Notify that we've shown a breakpoint in the source editor.
  1758       window.emit(EVENTS.BREAKPOINT_SHOWN);
  1759     });
  1760   },
  1762   /**
  1763    * Event handler for breakpoints that are removed from the editor.
  1765    * @param number aLine
  1766    *        Line number where breakpoint was removed.
  1767    */
  1768   _onEditorBreakpointRemove: function(_, aLine) {
  1769     let url = DebuggerView.Sources.selectedValue;
  1770     let location = { url: url, line: aLine + 1 };
  1772     // Destroy the breakpoint, but don't update the editor, since this callback
  1773     // is invoked because a breakpoint was removed from the editor itself.
  1774     this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
  1775       // Notify that we've hidden a breakpoint in the source editor.
  1776       window.emit(EVENTS.BREAKPOINT_HIDDEN);
  1777     });
  1778   },
  1780   /**
  1781    * Update the breakpoints in the editor view. This function takes the list of
  1782    * breakpoints in the debugger and adds them back into the editor view.
  1783    * This is invoked when the selected script is changed, or when new sources
  1784    * are received via the _onNewSource and _onSourcesAdded event listeners.
  1785    */
  1786   updateEditorBreakpoints: function() {
  1787     for (let breakpointPromise of this._addedOrDisabled) {
  1788       breakpointPromise.then(aBreakpointClient => {
  1789         let currentSourceUrl = DebuggerView.Sources.selectedValue;
  1790         let breakpointUrl = aBreakpointClient.location.url;
  1792         // Update the view only if the breakpoint is in the currently shown source.
  1793         if (currentSourceUrl == breakpointUrl) {
  1794           this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
  1796       });
  1798   },
  1800   /**
  1801    * Update the breakpoints in the pane view. This function takes the list of
  1802    * breakpoints in the debugger and adds them back into the breakpoints pane.
  1803    * This is invoked when new sources are received via the _onNewSource and
  1804    * _onSourcesAdded event listeners.
  1805    */
  1806   updatePaneBreakpoints: function() {
  1807     for (let breakpointPromise of this._addedOrDisabled) {
  1808       breakpointPromise.then(aBreakpointClient => {
  1809         let container = DebuggerView.Sources;
  1810         let breakpointUrl = aBreakpointClient.location.url;
  1812         // Update the view only if the breakpoint exists in a known source.
  1813         if (container.containsValue(breakpointUrl)) {
  1814           this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
  1816       });
  1818   },
  1820   /**
  1821    * Add a breakpoint.
  1823    * @param object aLocation
  1824    *        The location where you want the breakpoint.
  1825    *        This object must have two properties:
  1826    *          - url: the breakpoint's source location.
  1827    *          - line: the breakpoint's line number.
  1828    *        It can also have the following optional properties:
  1829    *          - condition: only pause if this condition evaluates truthy
  1830    * @param object aOptions [optional]
  1831    *        Additional options or flags supported by this operation:
  1832    *          - openPopup: tells if the expression popup should be shown.
  1833    *          - noEditorUpdate: tells if you want to skip editor updates.
  1834    *          - noPaneUpdate: tells if you want to skip breakpoint pane updates.
  1835    * @return object
  1836    *         A promise that is resolved after the breakpoint is added, or
  1837    *         rejected if there was an error.
  1838    */
  1839   addBreakpoint: Task.async(function*(aLocation, aOptions = {}) {
  1840     // Make sure a proper location is available.
  1841     if (!aLocation) {
  1842       throw new Error("Invalid breakpoint location.");
  1844     let addedPromise, removingPromise;
  1846     // If the breakpoint was already added, or is currently being added at the
  1847     // specified location, then return that promise immediately.
  1848     if ((addedPromise = this._getAdded(aLocation))) {
  1849       return addedPromise;
  1852     // If the breakpoint is currently being removed from the specified location,
  1853     // then wait for that to finish.
  1854     if ((removingPromise = this._getRemoving(aLocation))) {
  1855       yield removingPromise;
  1858     let deferred = promise.defer();
  1860     // Remember the breakpoint initialization promise in the store.
  1861     let identifier = this.getIdentifier(aLocation);
  1862     this._added.set(identifier, deferred.promise);
  1864     // Try adding the breakpoint.
  1865     gThreadClient.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) {
  1866       // If the breakpoint response has an "actualLocation" attached, then
  1867       // the original requested placement for the breakpoint wasn't accepted.
  1868       if (aResponse.actualLocation) {
  1869         // Remember the initialization promise for the new location instead.
  1870         let oldIdentifier = identifier;
  1871         let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation);
  1872         this._added.delete(oldIdentifier);
  1873         this._added.set(newIdentifier, deferred.promise);
  1876       // By default, new breakpoints are always enabled. Disabled breakpoints
  1877       // are, in fact, removed from the server but preserved in the frontend,
  1878       // so that they may not be forgotten across target navigations.
  1879       let disabledPromise = this._disabled.get(identifier);
  1880       if (disabledPromise) {
  1881         let aPrevBreakpointClient = yield disabledPromise;
  1882         let condition = aPrevBreakpointClient.getCondition();
  1883         this._disabled.delete(identifier);
  1885         if (condition) {
  1886           aBreakpointClient = yield aBreakpointClient.setCondition(
  1887             gThreadClient,
  1888             condition
  1889           );
  1893       if (aResponse.actualLocation) {
  1894         // Store the originally requested location in case it's ever needed
  1895         // and update the breakpoint client with the actual location.
  1896         aBreakpointClient.requestedLocation = aLocation;
  1897         aBreakpointClient.location = aResponse.actualLocation;
  1900       // Preserve information about the breakpoint's line text, to display it
  1901       // in the sources pane without requiring fetching the source (for example,
  1902       // after the target navigated). Note that this will get out of sync
  1903       // if the source text contents change.
  1904       let line = aBreakpointClient.location.line - 1;
  1905       aBreakpointClient.text = DebuggerView.editor.getText(line).trim();
  1907       // Show the breakpoint in the editor and breakpoints pane, and resolve.
  1908       this._showBreakpoint(aBreakpointClient, aOptions);
  1910       // Notify that we've added a breakpoint.
  1911       window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient);
  1912       deferred.resolve(aBreakpointClient);
  1913     }.bind(this)));
  1915     return deferred.promise;
  1916   }),
  1918   /**
  1919    * Remove a breakpoint.
  1921    * @param object aLocation
  1922    *        @see DebuggerController.Breakpoints.addBreakpoint
  1923    * @param object aOptions [optional]
  1924    *        @see DebuggerController.Breakpoints.addBreakpoint
  1925    * @return object
  1926    *         A promise that is resolved after the breakpoint is removed, or
  1927    *         rejected if there was an error.
  1928    */
  1929   removeBreakpoint: function(aLocation, aOptions = {}) {
  1930     // Make sure a proper location is available.
  1931     if (!aLocation) {
  1932       return promise.reject(new Error("Invalid breakpoint location."));
  1935     // If the breakpoint was already removed, or has never even been added,
  1936     // then return a resolved promise immediately.
  1937     let addedPromise = this._getAdded(aLocation);
  1938     if (!addedPromise) {
  1939       return promise.resolve(aLocation);
  1942     // If the breakpoint is currently being removed from the specified location,
  1943     // then return that promise immediately.
  1944     let removingPromise = this._getRemoving(aLocation);
  1945     if (removingPromise) {
  1946       return removingPromise;
  1949     let deferred = promise.defer();
  1951     // Remember the breakpoint removal promise in the store.
  1952     let identifier = this.getIdentifier(aLocation);
  1953     this._removing.set(identifier, deferred.promise);
  1955     // Retrieve the corresponding breakpoint client first.
  1956     addedPromise.then(aBreakpointClient => {
  1957       // Try removing the breakpoint.
  1958       aBreakpointClient.remove(aResponse => {
  1959         // If there was an error removing the breakpoint, reject the promise
  1960         // and forget about it that the breakpoint may be re-removed later.
  1961         if (aResponse.error) {
  1962           deferred.reject(aResponse);
  1963           return void this._removing.delete(identifier);
  1966         // When a breakpoint is removed, the frontend may wish to preserve some
  1967         // details about it, so that it can be easily re-added later. In such
  1968         // cases, breakpoints are marked and stored as disabled, so that they
  1969         // may not be forgotten across target navigations.
  1970         if (aOptions.rememberDisabled) {
  1971           aBreakpointClient.disabled = true;
  1972           this._disabled.set(identifier, promise.resolve(aBreakpointClient));
  1975         // Forget both the initialization and removal promises from the store.
  1976         this._added.delete(identifier);
  1977         this._removing.delete(identifier);
  1979         // Hide the breakpoint from the editor and breakpoints pane, and resolve.
  1980         this._hideBreakpoint(aLocation, aOptions);
  1982         // Notify that we've removed a breakpoint.
  1983         window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation);
  1984         deferred.resolve(aLocation);
  1985       });
  1986     });
  1988     return deferred.promise;
  1989   },
  1991   /**
  1992    * Removes all the currently enabled breakpoints.
  1994    * @return object
  1995    *         A promise that is resolved after all breakpoints are removed, or
  1996    *         rejected if there was an error.
  1997    */
  1998   removeAllBreakpoints: function() {
  1999     /* Gets an array of all the existing breakpoints promises. */
  2000     let getActiveBreakpoints = (aPromises, aStore = []) => {
  2001       for (let [, breakpointPromise] of aPromises) {
  2002         aStore.push(breakpointPromise);
  2004       return aStore;
  2007     /* Gets an array of all the removed breakpoints promises. */
  2008     let getRemovedBreakpoints = (aClients, aStore = []) => {
  2009       for (let breakpointClient of aClients) {
  2010         aStore.push(this.removeBreakpoint(breakpointClient.location));
  2012       return aStore;
  2015     // First, populate an array of all the currently added breakpoints promises.
  2016     // Then, once all the breakpoints clients are retrieved, populate an array
  2017     // of all the removed breakpoints promises and wait for their fulfillment.
  2018     return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => {
  2019       return promise.all(getRemovedBreakpoints(aBreakpointClients));
  2020     });
  2021   },
  2023   /**
  2024    * Update the condition of a breakpoint.
  2026    * @param object aLocation
  2027    *        @see DebuggerController.Breakpoints.addBreakpoint
  2028    * @param string aClients
  2029    *        The condition to set on the breakpoint
  2030    * @return object
  2031    *         A promise that will be resolved with the breakpoint client
  2032    */
  2033   updateCondition: function(aLocation, aCondition) {
  2034     let addedPromise = this._getAdded(aLocation);
  2035     if (!addedPromise) {
  2036       return promise.reject(new Error('breakpoint does not exist ' +
  2037                                       'in specified location'));
  2040     var promise = addedPromise.then(aBreakpointClient => {
  2041       return aBreakpointClient.setCondition(gThreadClient, aCondition);
  2042     });
  2044     // `setCondition` returns a new breakpoint that has the condition,
  2045     // so we need to update the store
  2046     this._added.set(this.getIdentifier(aLocation), promise);
  2047     return promise;
  2048   },
  2050   /**
  2051    * Update the editor and breakpoints pane to show a specified breakpoint.
  2053    * @param object aBreakpointData
  2054    *        Information about the breakpoint to be shown.
  2055    *        This object must have the following properties:
  2056    *          - location: the breakpoint's source location and line number
  2057    *          - disabled: the breakpoint's disabled state, boolean
  2058    *          - text: the breakpoint's line text to be displayed
  2059    * @param object aOptions [optional]
  2060    *        @see DebuggerController.Breakpoints.addBreakpoint
  2061    */
  2062   _showBreakpoint: function(aBreakpointData, aOptions = {}) {
  2063     let currentSourceUrl = DebuggerView.Sources.selectedValue;
  2064     let location = aBreakpointData.location;
  2066     // Update the editor if required.
  2067     if (!aOptions.noEditorUpdate && !aBreakpointData.disabled) {
  2068       if (location.url == currentSourceUrl) {
  2069         DebuggerView.editor.addBreakpoint(location.line - 1);
  2073     // Update the breakpoints pane if required.
  2074     if (!aOptions.noPaneUpdate) {
  2075       DebuggerView.Sources.addBreakpoint(aBreakpointData, aOptions);
  2077   },
  2079   /**
  2080    * Update the editor and breakpoints pane to hide a specified breakpoint.
  2082    * @param object aLocation
  2083    *        @see DebuggerController.Breakpoints.addBreakpoint
  2084    * @param object aOptions [optional]
  2085    *        @see DebuggerController.Breakpoints.addBreakpoint
  2086    */
  2087   _hideBreakpoint: function(aLocation, aOptions = {}) {
  2088     let currentSourceUrl = DebuggerView.Sources.selectedValue;
  2090     // Update the editor if required.
  2091     if (!aOptions.noEditorUpdate) {
  2092       if (aLocation.url == currentSourceUrl) {
  2093         DebuggerView.editor.removeBreakpoint(aLocation.line - 1);
  2097     // Update the breakpoints pane if required.
  2098     if (!aOptions.noPaneUpdate) {
  2099       DebuggerView.Sources.removeBreakpoint(aLocation);
  2101   },
  2103   /**
  2104    * Get a Promise for the BreakpointActor client object which is already added
  2105    * or currently being added at the given location.
  2107    * @param object aLocation
  2108    *        @see DebuggerController.Breakpoints.addBreakpoint
  2109    * @return object | null
  2110    *         A promise that is resolved after the breakpoint is added, or
  2111    *         null if no breakpoint was found.
  2112    */
  2113   _getAdded: function(aLocation) {
  2114     return this._added.get(this.getIdentifier(aLocation));
  2115   },
  2117   /**
  2118    * Get a Promise for the BreakpointActor client object which is currently
  2119    * being removed from the given location.
  2121    * @param object aLocation
  2122    *        @see DebuggerController.Breakpoints.addBreakpoint
  2123    * @return object | null
  2124    *         A promise that is resolved after the breakpoint is removed, or
  2125    *         null if no breakpoint was found.
  2126    */
  2127   _getRemoving: function(aLocation) {
  2128     return this._removing.get(this.getIdentifier(aLocation));
  2129   },
  2131   /**
  2132    * Get an identifier string for a given location. Breakpoint promises are
  2133    * identified in the store by a string representation of their location.
  2135    * @param object aLocation
  2136    *        The location to serialize to a string.
  2137    * @return string
  2138    *         The identifier string.
  2139    */
  2140   getIdentifier: function(aLocation) {
  2141     return aLocation.url + ":" + aLocation.line;
  2143 };
  2145 /**
  2146  * Gets all Promises for the BreakpointActor client objects that are
  2147  * either enabled (added to the server) or disabled (removed from the server,
  2148  * but for which some details are preserved).
  2149  */
  2150 Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", {
  2151   get: function* () {
  2152     yield* this._added.values();
  2153     yield* this._disabled.values();
  2155 });
  2157 /**
  2158  * Localization convenience methods.
  2159  */
  2160 let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
  2162 /**
  2163  * Shortcuts for accessing various debugger preferences.
  2164  */
  2165 let Prefs = new ViewHelpers.Prefs("devtools", {
  2166   sourcesWidth: ["Int", "debugger.ui.panes-sources-width"],
  2167   instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
  2168   panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
  2169   variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
  2170   variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
  2171   variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
  2172   pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
  2173   ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
  2174   sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
  2175   prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
  2176   autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
  2177   tracerEnabled: ["Bool", "debugger.tracer"],
  2178   editorTabSize: ["Int", "editor.tabsize"]
  2179 });
  2181 /**
  2182  * Convenient way of emitting events from the panel window.
  2183  */
  2184 EventEmitter.decorate(this);
  2186 /**
  2187  * Preliminary setup for the DebuggerController object.
  2188  */
  2189 DebuggerController.initialize();
  2190 DebuggerController.Parser = new Parser();
  2191 DebuggerController.ThreadState = new ThreadState();
  2192 DebuggerController.StackFrames = new StackFrames();
  2193 DebuggerController.SourceScripts = new SourceScripts();
  2194 DebuggerController.Breakpoints = new Breakpoints();
  2195 DebuggerController.Breakpoints.DOM = new EventListeners();
  2196 DebuggerController.Tracer = new Tracer();
  2198 /**
  2199  * Export some properties to the global scope for easier access.
  2200  */
  2201 Object.defineProperties(window, {
  2202   "gTarget": {
  2203     get: function() DebuggerController._target
  2204   },
  2205   "gHostType": {
  2206     get: function() DebuggerView._hostType
  2207   },
  2208   "gClient": {
  2209     get: function() DebuggerController.client
  2210   },
  2211   "gThreadClient": {
  2212     get: function() DebuggerController.activeThread
  2213   },
  2214   "gCallStackPageSize": {
  2215     get: function() CALL_STACK_PAGE_SIZE
  2217 });
  2219 /**
  2220  * Helper method for debugging.
  2221  * @param string
  2222  */
  2223 function dumpn(str) {
  2224   if (wantLogging) {
  2225     dump("DBG-FRONTEND: " + str + "\n");
  2229 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");

mercurial