Bug 916180 - Make pretty printing toggle-able; r=vporof
authorNick Fitzgerald <fitzgen@gmail.com>
Tue, 22 Oct 2013 00:04:46 -0700
changeset 166323 83562506fa87d73ead8c8064c5a301872764140f
parent 166322 cc1740f2a675b3bdc87605d30b1f2fce0584ba37
child 166324 73bebd77c3ed9d5bd4f4f033a23a8d5c386159b3
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvporof
bugs916180
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 916180 - Make pretty printing toggle-able; r=vporof
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-panes.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/test/browser.ini
browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
browser/devtools/debugger/test/browser_dbg_pretty-print-03.js
browser/devtools/debugger/test/browser_dbg_pretty-print-04.js
browser/devtools/debugger/test/browser_dbg_pretty-print-05.js
browser/devtools/debugger/test/browser_dbg_pretty-print-06.js
browser/devtools/debugger/test/browser_dbg_pretty-print-07.js
browser/devtools/debugger/test/browser_dbg_pretty-print-09.js
browser/devtools/debugger/test/browser_dbg_pretty-print-10.js
browser/devtools/debugger/test/browser_dbg_pretty-print-11.js
browser/devtools/debugger/test/code_ugly-4.js
toolkit/devtools/client/dbg-client.jsm
toolkit/devtools/server/actors/script.js
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -510,16 +510,17 @@ ThreadState.prototype = {
  * stack frame cache.
  */
 function StackFrames() {
   this._onPaused = this._onPaused.bind(this);
   this._onResumed = this._onResumed.bind(this);
   this._onFrames = this._onFrames.bind(this);
   this._onFramesCleared = this._onFramesCleared.bind(this);
   this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
+  this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
   this._afterFramesCleared = this._afterFramesCleared.bind(this);
   this.evaluate = this.evaluate.bind(this);
 }
 
 StackFrames.prototype = {
   get activeThread() DebuggerController.activeThread,
   currentFrameDepth: -1,
   _isWatchExpressionsEvaluation: false,
@@ -536,32 +537,34 @@ StackFrames.prototype = {
    */
   connect: function() {
     dumpn("StackFrames is connecting...");
     this.activeThread.addListener("paused", this._onPaused);
     this.activeThread.addListener("resumed", this._onResumed);
     this.activeThread.addListener("framesadded", this._onFrames);
     this.activeThread.addListener("framescleared", this._onFramesCleared);
     this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
+    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
     this.handleTabNavigation();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("StackFrames is disconnecting...");
     this.activeThread.removeListener("paused", this._onPaused);
     this.activeThread.removeListener("resumed", this._onResumed);
     this.activeThread.removeListener("framesadded", this._onFrames);
     this.activeThread.removeListener("framescleared", this._onFramesCleared);
     this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
+    this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
   },
 
   /**
    * Handles any initialization on a tab navigation event issued by the client.
    */
   handleTabNavigation: function() {
     dumpn("Handling tab navigation in the StackFrames");
     // Nothing to do here yet.
@@ -738,22 +741,30 @@ StackFrames.prototype = {
     window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
   },
 
   /**
    * Handler for the debugger's blackboxchange notification.
    */
   _onBlackBoxChange: function() {
     if (this.activeThread.state == "paused") {
-      this.currentFrame = null;
       this._refillFrames();
     }
   },
 
   /**
+   * Handler for the debugger's prettyprintchange notification.
+   */
+  _onPrettyPrintChange: function() {
+    if (this.activeThread.state == "paused") {
+      this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+    }
+  },
+
+  /**
    * Called soon after the thread client's framescleared notification.
    */
   _afterFramesCleared: function() {
     // Ignore useless notifications.
     if (this.activeThread.cachedFrames.length) {
       return;
     }
     DebuggerView.StackFrames.empty();
@@ -990,45 +1001,48 @@ StackFrames.prototype = {
  * Keeps the source script list up-to-date, using the thread client's
  * source script cache.
  */
 function SourceScripts() {
   this._onNewGlobal = this._onNewGlobal.bind(this);
   this._onNewSource = this._onNewSource.bind(this);
   this._onSourcesAdded = this._onSourcesAdded.bind(this);
   this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
+  this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
 }
 
 SourceScripts.prototype = {
   get activeThread() DebuggerController.activeThread,
   get debuggerClient() DebuggerController.client,
   _cache: new Map(),
 
   /**
    * Connect to the current thread client.
    */
   connect: function() {
     dumpn("SourceScripts is connecting...");
     this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.addListener("newSource", this._onNewSource);
     this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
+    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
     this.handleTabNavigation();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function() {
     if (!this.activeThread) {
       return;
     }
     dumpn("SourceScripts is disconnecting...");
     this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
     this.debuggerClient.removeListener("newSource", this._onNewSource);
     this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
+    this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
   },
 
   /**
    * Clears all the cached source contents.
    */
   clearCache: function() {
     this._cache.clear();
   },
@@ -1175,65 +1189,74 @@ SourceScripts.prototype = {
         deferred.resolve([aSource, sourceClient.isBlackBoxed]);
       }
     });
 
     return deferred.promise;
   },
 
   /**
-   * Pretty print a source's text. All subsequent calls to |getText| will return
-   * the pretty text. Nothing will happen for non-javascript files.
+   * Toggle the pretty printing of a source's text. All subsequent calls to
+   * |getText| will return the pretty-toggled text. Nothing will happen for
+   * non-javascript files.
    *
    * @param Object aSource
    *        The source form from the RDP.
    * @returns Promise
    *          A promise that resolves to [aSource, prettyText] or rejects to
    *          [aSource, error].
    */
-  prettyPrint: function(aSource) {
+  togglePrettyPrint: function(aSource) {
     // Only attempt to pretty print JavaScript sources.
     if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) {
       return promise.reject([aSource, "Can't prettify non-javascript files."]);
     }
 
+    const sourceClient = this.activeThread.source(aSource);
+    const wantPretty = !sourceClient.isPrettyPrinted;
+
     // Only use the existing promise if it is pretty printed.
     let textPromise = this._cache.get(aSource.url);
-    if (textPromise && textPromise.pretty) {
+    if (textPromise && textPromise.pretty === wantPretty) {
       return textPromise;
     }
 
     const deferred = promise.defer();
+    deferred.promise.pretty = wantPretty;
     this._cache.set(aSource.url, deferred.promise);
 
-    this.activeThread.source(aSource)
-      .prettyPrint(Prefs.editorTabSize, ({ error, message, source: text }) => {
-        if (error) {
-          // Revert the rejected promise from the cache, so that the original
-          // source's text may be shown when the source is selected.
-          this._cache.set(aSource.url, textPromise);
-          deferred.reject([aSource, message || error]);
-          return;
-        }
+    const afterToggle = ({ error, message, source: text }) => {
+      if (error) {
+        // Revert the rejected promise from the cache, so that the original
+        // source's text may be shown when the source is selected.
+        this._cache.set(aSource.url, textPromise);
+
+        deferred.reject([aSource, message || error]);
+        return;
+      }
+
+      deferred.resolve([aSource, text]);
+    };
 
-        // Remove the cached source AST from the Parser, to avoid getting
-        // wrong locations when searching for functions.
-        DebuggerController.Parser.clearSource(aSource.url);
+    if (wantPretty) {
+      sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle);
+    } else {
+      sourceClient.disablePrettyPrint(afterToggle);
+    }
 
-        if (this.activeThread.paused) {
-          // Update the stack frame list.
-          this.activeThread._clearFrames();
-          this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
-        }
+    return deferred.promise;
+  },
 
-        deferred.resolve([aSource, text]);
-      });
-
-    deferred.promise.pretty = true;
-    return deferred.promise;
+  /**
+   * Handler for the debugger's prettyprintchange notification.
+   */
+  _onPrettyPrintChange: function(aEvent, { url }) {
+    // Remove the cached source AST from the Parser, to avoid getting
+    // wrong locations when searching for functions.
+    DebuggerController.Parser.clearSource(url);
   },
 
   /**
    * Gets a specified source's text.
    *
    * @param object aSource
    *        The source object coming from the active thread.
    * @param function aOnTimeout [optional]
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -6,33 +6,34 @@
 "use strict";
 
 /**
  * Functions handling the sources UI.
  */
 function SourcesView() {
   dumpn("SourcesView was instantiated");
 
-  this.prettyPrint = this.prettyPrint.bind(this);
+  this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
   this._onEditorLoad = this._onEditorLoad.bind(this);
   this._onEditorUnload = this._onEditorUnload.bind(this);
   this._onEditorSelection = this._onEditorSelection.bind(this);
   this._onEditorContextMenu = this._onEditorContextMenu.bind(this);
   this._onSourceSelect = this._onSourceSelect.bind(this);
   this._onSourceClick = this._onSourceClick.bind(this);
   this._onBreakpointRemoved = this._onBreakpointRemoved.bind(this);
   this._onSourceCheck = this._onSourceCheck.bind(this);
   this._onStopBlackBoxing = this._onStopBlackBoxing.bind(this);
   this._onBreakpointClick = this._onBreakpointClick.bind(this);
   this._onBreakpointCheckboxClick = this._onBreakpointCheckboxClick.bind(this);
   this._onConditionalPopupShowing = this._onConditionalPopupShowing.bind(this);
   this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
   this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
   this._onConditionalTextboxInput = this._onConditionalTextboxInput.bind(this);
   this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
+  this._updatePrettyPrintButtonState = this._updatePrettyPrintButtonState.bind(this);
 }
 
 SourcesView.prototype = Heritage.extend(WidgetMethods, {
   /**
    * Initialization function, called when the debugger is started.
    */
   initialize: function() {
     dumpn("Initializing the SourcesView");
@@ -58,17 +59,16 @@ SourcesView.prototype = Heritage.extend(
     }
 
     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._prettyPrintButton.addEventListener("click", this.prettyPrint, 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);
     this._cbTextbox.addEventListener("keypress", this._onConditionalTextboxKeyPress, false);
 
     this.autoFocusOnSelection = false;
 
@@ -83,17 +83,16 @@ SourcesView.prototype = Heritage.extend(
     dumpn("Destroying the SourcesView");
 
     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._prettyPrintButton.removeEventListener("click", this.prettyPrint, 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);
     this._cbTextbox.removeEventListener("keypress", this._onConditionalTextboxKeyPress, false);
   },
 
   /**
@@ -374,40 +373,44 @@ SourcesView.prototype = Heritage.extend(
    * Unhighlights the current breakpoint in this sources container.
    */
   unhighlightBreakpoint: function() {
     this._unselectBreakpoint();
     this._hideConditionalPopup();
   },
 
   /**
-   * Pretty print the selected source.
+   * Toggle the pretty printing of the selected source.
    */
-  prettyPrint: function() {
+  togglePrettyPrint: function() {
     if (this._prettyPrintButton.hasAttribute("disabled")) {
       return;
     }
 
     const resetEditor = ([{ url }]) => {
       // Only set the text when the source is still selected.
       if (url == this.selectedValue) {
         DebuggerView.setEditorLocation(url, 0, { force: true });
       }
     };
+
     const printError = ([{ url }, error]) => {
-      let err = DevToolsUtils.safeErrorString(error);
-      let msg = "Couldn't prettify source: " + url + "\n" + err;
-      Cu.reportError(msg);
-      dumpn(msg);
-      return;
-    }
+      DevToolsUtils.reportException("togglePrettyPrint", error);
+    };
 
     DebuggerView.showProgressBar();
     const { source } = this.selectedItem.attachment;
-    DebuggerController.SourceScripts.prettyPrint(source)
+
+    if (gThreadClient.source(source).isPrettyPrinted) {
+      this._prettyPrintButton.removeAttribute("checked");
+    } else {
+      this._prettyPrintButton.setAttribute("checked", true);
+    }
+
+    DebuggerController.SourceScripts.togglePrettyPrint(source)
       .then(resetEditor, printError)
       .then(DebuggerView.showEditor);
   },
 
   /**
    * Marks a breakpoint as selected in this sources container.
    *
    * @param object aItem
@@ -696,25 +699,34 @@ SourcesView.prototype = Heritage.extend(
     document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
 
     DebuggerView.maybeShowBlackBoxMessage();
     this._updatePrettyPrintButtonState();
   },
 
   /**
    * Enable or disable the pretty print button depending on whether the selected
-   * source is black boxed or not.
+   * source is black boxed or not and check or uncheck it depending on if the
+   * selected source is already pretty printed or not.
    */
   _updatePrettyPrintButtonState: function() {
     const { source } = this.selectedItem.attachment;
-    if (gThreadClient.source(source).isBlackBoxed) {
+    const sourceClient = gThreadClient.source(source);
+
+    if (sourceClient.isBlackBoxed) {
       this._prettyPrintButton.setAttribute("disabled", true);
     } else {
       this._prettyPrintButton.removeAttribute("disabled");
     }
+
+    if (sourceClient.isPrettyPrinted) {
+      this._prettyPrintButton.setAttribute("checked", true);
+    } else {
+      this._prettyPrintButton.removeAttribute("checked");
+    }
   },
 
   /**
    * The click listener for the sources container.
    */
   _onSourceClick: function() {
     // Use this container as a filtering target.
     DebuggerView.Filtering.target = this;
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -63,16 +63,17 @@ let DebuggerView = {
     this.ChromeGlobals.initialize();
     this.StackFrames.initialize();
     this.Sources.initialize();
     this.WatchExpressions.initialize();
     this.EventListeners.initialize();
     this.GlobalSearch.initialize();
     this._initializeVariablesView();
     this._initializeEditor(deferred.resolve);
+
     document.title = L10N.getStr("DebuggerWindowTitle");
 
     return deferred.promise;
   },
 
   /**
    * Destroys the debugger view.
    *
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -28,17 +28,17 @@
   <script type="text/javascript" src="debugger-toolbar.js"/>
   <script type="text/javascript" src="debugger-panes.js"/>
 
   <commandset id="editMenuCommands"/>
   <commandset id="sourceEditorCommands"/>
 
   <commandset id="debuggerCommands">
     <command id="prettyPrintCommand"
-             oncommand="DebuggerView.Sources.prettyPrint()"/>
+             oncommand="DebuggerView.Sources.togglePrettyPrint()"/>
     <command id="unBlackBoxButton"
              oncommand="DebuggerView.Sources._onStopBlackBoxing()"/>
     <command id="nextSourceCommand"
              oncommand="DebuggerView.Sources.selectNextItem()"/>
     <command id="prevSourceCommand"
              oncommand="DebuggerView.Sources.selectPrevItem()"/>
     <command id="resumeCommand"
              oncommand="DebuggerView.Toolbar._onResumePressed()"/>
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -119,16 +119,17 @@ support-files =
 [browser_dbg_pretty-print-03.js]
 [browser_dbg_pretty-print-04.js]
 [browser_dbg_pretty-print-05.js]
 [browser_dbg_pretty-print-06.js]
 [browser_dbg_pretty-print-07.js]
 [browser_dbg_pretty-print-08.js]
 [browser_dbg_pretty-print-09.js]
 [browser_dbg_pretty-print-10.js]
+[browser_dbg_pretty-print-11.js]
 [browser_dbg_progress-listener-bug.js]
 [browser_dbg_reload-preferred-script-01.js]
 [browser_dbg_reload-preferred-script-02.js]
 [browser_dbg_reload-preferred-script-03.js]
 [browser_dbg_reload-same-script.js]
 [browser_dbg_scripts-switching-01.js]
 [browser_dbg_scripts-switching-02.js]
 [browser_dbg_scripts-switching-03.js]
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-01.js
@@ -38,19 +38,17 @@ function test() {
 }
 
 function testSourceIsUgly() {
   ok(!gEditor.getText().contains("\n    "),
      "The source shouldn't be pretty printed yet.");
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-                            gDebugger.document.getElementById("pretty-print"),
-                            gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 function testProgressBarShown() {
   const deck = gDebugger.document.getElementById("editor-deck");
   is(deck.selectedIndex, 2, "The progress bar should be shown");
 }
 
 function testSourceIsPretty() {
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-03.js
@@ -36,19 +36,17 @@ function runCodeAndPause() {
   const deferred = promise.defer();
   once(gDebugger.gThreadClient, "paused").then(deferred.resolve);
   // Have to executeSoon so that we don't pause before this function returns.
   executeSoon(gDebuggee.foo);
   return deferred.promise;
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-                            gDebugger.document.getElementById("pretty-print"),
-                            gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-04.js
@@ -42,19 +42,17 @@ function testUglySearch() {
     deferred.resolve();
   });
 
   setText(gSearchBox, "@bar");
   return deferred.promise;
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-                            gDebugger.document.getElementById("pretty-print"),
-                            gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 function testPrettyPrintedSearch() {
   const deferred = promise.defer();
 
   once(gDebugger, "popupshown").then(() => {
     ok(isCaretPos(gPanel, 6, 10),
        "The bar function's pretty printed location should be shown.");
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-05.js
@@ -32,17 +32,17 @@ function test() {
         "The correct source is currently selected.");
       ok(gEditor.getText().contains("myFunction"),
         "The source shouldn't be pretty printed yet.");
 
       clickPrettyPrintButton();
 
       let { source } = gSources.selectedItem.attachment;
       try {
-        yield gControllerSources.prettyPrint(source);
+        yield gControllerSources.togglePrettyPrint(source);
         ok(false, "The promise for a prettified source should be rejected!");
       } catch ([source, error]) {
         is(error, "Can't prettify non-javascript files.",
           "The promise was correctly rejected with a meaningful message.");
       }
 
       let [source, text] = yield gControllerSources.getText(source);
       is(gSources.selectedValue, TAB_URL,
@@ -53,19 +53,17 @@ function test() {
         "The cached source text wasn't altered in any way.");
 
       yield closeDebuggerAndFinish(gPanel);
     });
   });
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-    gDebugger.document.getElementById("pretty-print"),
-    gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 function prepareDebugger(aPanel) {
   aPanel._view.Sources.preferredSource = TAB_URL;
 }
 
 registerCleanupFunction(function() {
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-06.js
@@ -48,17 +48,17 @@ function test() {
         "The correct source is currently selected.");
       ok(gEditor.getText().contains("myFunction"),
         "The source shouldn't be pretty printed yet.");
 
       clickPrettyPrintButton();
 
       let { source } = gSources.selectedItem.attachment;
       try {
-        yield gControllerSources.prettyPrint(source);
+        yield gControllerSources.togglePrettyPrint(source);
         ok(false, "The promise for a prettified source should be rejected!");
       } catch ([source, error]) {
         ok(error.contains("prettyPrintError"),
           "The promise was correctly rejected with a meaningful message.");
       }
 
       let [source, text] = yield gControllerSources.getText(source);
       is(gSources.selectedValue, JS_URL,
@@ -72,19 +72,17 @@ function test() {
         "The hijacked pretty print method was executed.");
 
       yield closeDebuggerAndFinish(gPanel);
     });
   });
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-    gDebugger.document.getElementById("pretty-print"),
-    gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 registerCleanupFunction(function() {
   gTab = null;
   gDebuggee = null;
   gPanel = null;
   gDebugger = null;
   gClient = null;
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-07.js
@@ -1,16 +1,16 @@
 /* -*- Mode: javascript; js-indent-level: 2; -*- */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test basic pretty printing functionality. Would be an xpcshell test, except
 // for bug 921252.
 
-let gTab, gDebuggee, gPanel, gClient, gThreadClient;
+let gTab, gDebuggee, gPanel, gClient, gThreadClient, gSource;
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
@@ -21,26 +21,36 @@ function test() {
   });
 }
 
 function findSource() {
   gThreadClient.getSources(({ error, sources }) => {
     ok(!error);
     sources = sources.filter(s => s.url.contains('code_ugly-2.js'));
     is(sources.length, 1);
-    prettyPrintSource(sources[0]);
+    gSource = sources[0];
+    prettyPrintSource();
   });
 }
 
-function prettyPrintSource(source) {
-  gThreadClient.source(source).prettyPrint(4, testPrettyPrinted);
+function prettyPrintSource() {
+  gThreadClient.source(gSource).prettyPrint(4, testPrettyPrinted);
 }
 
-function testPrettyPrinted({ error, source}) {
+function testPrettyPrinted({ error, source }) {
   ok(!error);
   ok(source.contains("\n    "));
+  disablePrettyPrint();
+}
 
+function disablePrettyPrint() {
+  gThreadClient.source(gSource).disablePrettyPrint(testUgly);
+}
+
+function testUgly({ error, source }) {
+  ok(!error);
+  ok(!source.contains("\n    "));
   closeDebuggerAndFinish(gPanel);
 }
 
 registerCleanupFunction(function() {
-  gTab = gDebuggee = gPanel = gClient = gThreadClient = null;
+  gTab = gDebuggee = gPanel = gClient = gThreadClient = gSource = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-09.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-09.js
@@ -4,17 +4,17 @@
 
 // Test pretty printing source mapped sources.
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
 var gSource;
 
-let gTab, gDebuggee, gPanel, gClient, gThreadClient;
+let gTab, gDebuggee, gPanel, gClient, gThreadClient, gSource;
 
 const TAB_URL = EXAMPLE_URL + "doc_pretty-print-2.html";
 
 function test() {
   initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPanel = aPanel;
@@ -33,41 +33,58 @@ const A_URL = dataUrl(A);
 const B = "function b(){debugger}";
 const B_URL = dataUrl(B);
 
 function findSource() {
   gThreadClient.getSources(({ error, sources }) => {
     ok(!error);
     sources = sources.filter(s => s.url === B_URL);
     is(sources.length, 1);
-    prettyPrint(sources[0]);
+    gSource = sources[0];
+    prettyPrint();
   });
 }
 
-function prettyPrint(source) {
-  gThreadClient.source(source).prettyPrint(2, runCode);
+function prettyPrint() {
+  gThreadClient.source(gSource).prettyPrint(2, runCode);
 }
 
 function runCode({ error }) {
   ok(!error);
   gClient.addOneTimeListener("paused", testDbgStatement);
   gDebuggee.a();
 }
 
 function testDbgStatement(event, { frame, why }) {
-  dump("FITZGEN: inside testDbgStatement\n");
+  is(why.type, "debuggerStatement");
+  const { url, line, column } = frame.where;
+  is(url, B_URL);
+  is(line, 2);
+  is(column, 2);
+
+  disablePrettyPrint();
+}
+
+function disablePrettyPrint() {
+  gThreadClient.source(gSource).disablePrettyPrint(testUgly);
+}
 
-  try {
-    is(why.type, "debuggerStatement");
-    const { url, line, column } = frame.where;
-    is(url, B_URL);
-    is(line, 2);
-    is(column, 2);
+function testUgly({ error, source }) {
+  ok(!error);
+  ok(!source.contains("\n  "));
+  getFrame();
+}
 
-    resumeDebuggerThenCloseAndFinish(gPanel);
-  } catch (e) {
-    dump("FITZGEN: got an error! " + DevToolsUtils.safeErrorString(e) + "\n");
-  }
+function getFrame() {
+  gThreadClient.getFrames(0, 1, testFrame);
+}
+
+function testFrame({ frames: [frame] }) {
+  const { url, line } = frame.where;
+  is(url, B_URL);
+  is(line, 1);
+
+  resumeDebuggerThenCloseAndFinish(gPanel);
 }
 
 registerCleanupFunction(function() {
   gTab = gDebuggee = gPanel = gClient = gThreadClient = null;
 });
--- a/browser/devtools/debugger/test/browser_dbg_pretty-print-10.js
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-10.js
@@ -40,19 +40,17 @@ function testSourceIsUgly() {
 
 function blackBoxSource() {
   const checkbox = gDebugger.document.querySelector(
     ".selected .side-menu-widget-item-checkbox");
   checkbox.click();
 }
 
 function clickPrettyPrintButton() {
-  EventUtils.sendMouseEvent({ type: "click" },
-                            gDebugger.document.getElementById("pretty-print"),
-                            gDebugger);
+  gDebugger.document.getElementById("pretty-print").click();
 }
 
 function testSourceIsStillUgly() {
   const { source } = gSources.selectedItem.attachment;
   return gDebugger.DebuggerController.SourceScripts.getText(source).then(([, text]) => {
     ok(!text.contains("\n    "));
   });
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pretty-print-11.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that pretty printing is maintained across refreshes.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_pretty-print.html";
+
+let gTab, gDebuggee, gPanel, gDebugger;
+let gEditor, gSources;
+
+function test() {
+  initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+    gTab = aTab;
+    gDebuggee = aDebuggee;
+    gPanel = aPanel;
+    gDebugger = gPanel.panelWin;
+    gEditor = gDebugger.DebuggerView.editor;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    waitForSourceShown(gPanel, "code_ugly.js")
+      .then(testSourceIsUgly)
+      .then(() => {
+        const finished = waitForSourceShown(gPanel, "code_ugly.js");
+        clickPrettyPrintButton();
+        return finished;
+      })
+      .then(testSourceIsPretty)
+      .then(reloadActiveTab.bind(null, gPanel, gDebugger.EVENTS.SOURCE_SHOWN))
+      .then(testSourceIsPretty)
+      .then(() => resumeDebuggerThenCloseAndFinish(gPanel))
+      .then(null, aError => {
+        ok(false, "Got an error: " + DevToolsUtils.safeErrorString(aError));
+      });
+  });
+}
+
+function testSourceIsUgly() {
+  ok(!gEditor.getText().contains("\n    "),
+     "The source shouldn't be pretty printed yet.");
+}
+
+function clickPrettyPrintButton() {
+  gDebugger.document.getElementById("pretty-print").click();
+}
+
+function testSourceIsPretty() {
+  ok(gEditor.getText().contains("\n    "),
+     "The source should be pretty printed.")
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+  gDebugger = null;
+  gEditor = null;
+  gSources = null;
+});
--- a/browser/devtools/debugger/test/code_ugly-4.js
+++ b/browser/devtools/debugger/test/code_ugly-4.js
@@ -15,10 +15,10 @@ function a(){b()}function b(){debugger}
 //
 //    let result = (new SourceNode(null, null, null, [
 //      new SourceNode(1, 0, A_URL, A),
 //      B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch))
 //    ])).toStringWithSourceMap({
 //      file: "abc.js"
 //    });
 //
-//    result.code + "\n//# sourceMappingURL=data:application/json;base64," + btoa(JSON.stringify(result.map));
+//    result.code + "\n//# " + "sourceMappingURL=data:application/json;base64," + btoa(JSON.stringify(result.map));
 
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -640,17 +640,17 @@ DebuggerClient.prototype = {
    * @param aIgnoreCompatibility boolean
    *        Set true to not pass the packet through the compatibility layer.
    */
   onPacket: function DC_onPacket(aPacket, aIgnoreCompatibility=false) {
     let packet = aIgnoreCompatibility
       ? aPacket
       : this.compat.onPacket(aPacket);
 
-    resolve(packet).then((aPacket) => {
+    resolve(packet).then(aPacket => {
       if (!aPacket.from) {
         let msg = "Server did not specify an actor, dropping packet: " +
                   JSON.stringify(aPacket);
         Cu.reportError(msg);
         dumpn(msg);
         return;
       }
 
@@ -1972,23 +1972,25 @@ LongStringClient.prototype = {
  * @param aClient DebuggerClient
  *        The debugger client parent.
  * @param aForm Object
  *        The form sent across the remote debugging protocol.
  */
 function SourceClient(aClient, aForm) {
   this._form = aForm;
   this._isBlackBoxed = aForm.isBlackBoxed;
+  this._isPrettyPrinted = aForm.isPrettyPrinted;
   this._client = aClient;
 }
 
 SourceClient.prototype = {
   get _transport() this._client._transport,
   get _activeThread() this._client.activeThread,
   get isBlackBoxed() this._isBlackBoxed,
+  get isPrettyPrinted() this._isPrettyPrinted,
   get actor() this._form.actor,
   get request() this._client.request,
   get url() this._form.url,
 
   /**
    * Black box this SourceClient's source.
    *
    * @param aCallback Function
@@ -2048,16 +2050,39 @@ SourceClient.prototype = {
    */
   prettyPrint: function SC_prettyPrint(aIndent, aCallback) {
     const packet = {
       to: this._form.actor,
       type: "prettyPrint",
       indent: aIndent
     };
     this._client.request(packet, aResponse => {
+      if (!aResponse.error) {
+        this._isPrettyPrinted = true;
+        this._activeThread._clearFrames();
+        this._activeThread.notify("prettyprintchange", this);
+      }
+      this._onSourceResponse(aResponse, aCallback);
+    });
+  },
+
+  /**
+   * Stop pretty printing this source's text.
+   */
+  disablePrettyPrint: function SC_disablePrettyPrint(aCallback) {
+    const packet = {
+      to: this._form.actor,
+      type: "disablePrettyPrint"
+    };
+    this._client.request(packet, aResponse => {
+      if (!aResponse.error) {
+        this._isPrettyPrinted = false;
+        this._activeThread._clearFrames();
+        this._activeThread.notify("prettyprintchange", this);
+      }
       this._onSourceResponse(aResponse, aCallback);
     });
   },
 
   _onSourceResponse: function SC__onSourceResponse(aResponse, aCallback) {
     if (aResponse.error) {
       aCallback(aResponse);
       return;
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2360,34 +2360,49 @@ function SourceActor({ url, thread, sour
   this._sourceMap = sourceMap;
   this._generatedSource = generatedSource;
   this._text = text;
   this._contentType = contentType;
 
   this.onSource = this.onSource.bind(this);
   this._invertSourceMap = this._invertSourceMap.bind(this);
   this._saveMap = this._saveMap.bind(this);
+  this._getSourceText = this._getSourceText.bind(this);
+
+  if (this.threadActor.sources.isPrettyPrinted(this.url)) {
+    this._init = this.onPrettyPrint({
+      indent: this.threadActor.sources.prettyPrintIndent(this.url)
+    }).then(null, error => {
+      DevToolsUtils.reportException("SourceActor", error);
+    });
+  } else {
+    this._init = null;
+  }
 }
 
 SourceActor.prototype = {
   constructor: SourceActor,
   actorPrefix: "source",
 
+  _oldSourceMap: null,
+  _init: null,
+
   get threadActor() this._threadActor,
   get url() this._url,
 
   get prettyPrintWorker() {
     return this.threadActor.prettyPrintWorker;
   },
 
   form: function SA_form() {
     return {
       actor: this.actorID,
       url: this._url,
-      isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url)
+      isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
+      isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url)
       // TODO bug 637572: introductionScript
     };
   },
 
   disconnect: function SA_disconnect() {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
     }
@@ -2412,52 +2427,63 @@ SourceActor.prototype = {
     // source because we can't guarantee that the cache has the most up to date
     // content for this source like we can if it isn't source mapped.
     return fetch(this._url, { loadFromCache: !this._sourceMap });
   },
 
   /**
    * Handler for the "source" packet.
    */
-  onSource: function SA_onSource(aRequest) {
-    return this._getSourceText()
+  onSource: function SA_onSource() {
+    return resolve(this._init)
+      .then(this._getSourceText)
       .then(({ content, contentType }) => {
         return {
           from: this.actorID,
           source: this.threadActor.createValueGrip(
             content, this.threadActor.threadLifetimePool),
           contentType: contentType
         };
       })
-      .then(null, (aError) => {
+      .then(null, aError => {
         reportError(aError, "Got an exception during SA_onSource: ");
         return {
           "from": this.actorID,
           "error": "loadSourceError",
           "message": "Could not load the source for " + this._url + ".\n"
             + safeErrorString(aError)
         };
       });
   },
 
   /**
    * Handler for the "prettyPrint" packet.
    */
   onPrettyPrint: function ({ indent }) {
+    this.threadActor.sources.prettyPrint(this._url, indent);
     return this._getSourceText()
       .then(this._parseAST)
       .then(this._sendToPrettyPrintWorker(indent))
       .then(this._invertSourceMap)
       .then(this._saveMap)
+      .then(() => {
+        // We need to reset `_init` now because we have already done the work of
+        // pretty printing, and don't want onSource to wait forever for
+        // initialization to complete.
+        this._init = null;
+      })
       .then(this.onSource)
-      .then(null, error => ({
-        from: this.actorID,
-        error: "prettyPrintError",
-        message: DevToolsUtils.safeErrorString(error)
-      }));
+      .then(null, error => {
+        this.onDisablePrettyPrint();
+        return {
+          from: this.actorID,
+          error: "prettyPrintError",
+          message: DevToolsUtils.safeErrorString(error)
+        };
+      });
   },
 
   /**
    * Parse the source content into an AST.
    */
   _parseAST: function SA__parseAST({ content}) {
     return Reflect.parse(content);
   },
@@ -2567,28 +2593,40 @@ SourceActor.prototype = {
    * Save the source map back to our thread's ThreadSources object so that
    * stepping, breakpoints, debugger statements, etc can use it. If we are
    * pretty printing a source mapped source, we need to compose the existing
    * source map with our new one.
    */
   _saveMap: function SA__saveMap({ map }) {
     if (this._sourceMap) {
       // Compose the source maps
+      this._oldSourceMap = this._sourceMap;
       this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap);
       this._sourceMap.applySourceMap(map, this._url);
       this._sourceMap = SourceMapConsumer.fromSourceMap(this._sourceMap);
       this._threadActor.sources.saveSourceMap(this._sourceMap,
                                               this._generatedSource);
     } else {
       this._sourceMap = map;
       this._threadActor.sources.saveSourceMap(this._sourceMap, this._url);
     }
   },
 
   /**
+   * Handler for the "disablePrettyPrint" packet.
+   */
+  onDisablePrettyPrint: function SA_onDisablePrettyPrint() {
+    this._sourceMap = this._oldSourceMap;
+    this.threadActor.sources.saveSourceMap(this._sourceMap,
+                                           this._generatedSource || this._url);
+    this.threadActor.sources.disablePrettyPrint(this._url);
+    return this.onSource();
+  },
+
+  /**
    * Handler for the "blackbox" packet.
    */
   onBlackBox: function SA_onBlackBox(aRequest) {
     this.threadActor.sources.blackBox(this.url);
     let packet = {
       from: this.actorID
     };
     if (this.threadActor.state == "paused"
@@ -2609,17 +2647,18 @@ SourceActor.prototype = {
     };
   }
 };
 
 SourceActor.prototype.requestTypes = {
   "source": SourceActor.prototype.onSource,
   "blackbox": SourceActor.prototype.onBlackBox,
   "unblackbox": SourceActor.prototype.onUnblackBox,
-  "prettyPrint": SourceActor.prototype.onPrettyPrint
+  "prettyPrint": SourceActor.prototype.onPrettyPrint,
+  "disablePrettyPrint": SourceActor.prototype.onDisablePrettyPrint
 };
 
 
 /**
  * Creates an actor for the specified object.
  *
  * @param aObj Debugger.Object
  *        The debuggee object.
@@ -3713,16 +3752,17 @@ function ThreadSources(aThreadActor, aUs
   this._generatedUrlsByOriginalUrl = Object.create(null);
 }
 
 /**
  * Must be a class property because it needs to persist across reloads, same as
  * the breakpoint store.
  */
 ThreadSources._blackBoxedSources = new Set();
+ThreadSources._prettyPrintedSources = new Map();
 
 ThreadSources.prototype = {
   /**
    * Return the source actor representing |url|, creating one if none
    * exists already. Returns null if |url| is not allowed by the 'allow'
    * predicate.
    *
    * Right now this takes a URL, but in the future it should
@@ -3842,16 +3882,20 @@ ThreadSources.prototype = {
     return map;
   },
 
   /**
    * Save the given source map so that we can use it to query source locations
    * down the line.
    */
   saveSourceMap: function TS_saveSourceMap(aSourceMap, aGeneratedSource) {
+    if (!aSourceMap) {
+      delete this._sourceMapsByGeneratedSource[aGeneratedSource];
+      return null;
+    }
     this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap);
     for (let s of aSourceMap.sources) {
       this._generatedUrlsByOriginalUrl[s] = aGeneratedSource;
       this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
     }
     return aSourceMap;
   },
 
@@ -3964,19 +4008,17 @@ ThreadSources.prototype = {
    *        The URL of the source which we are checking whether it is black
    *        boxed or not.
    */
   isBlackBoxed: function TS_isBlackBoxed(aURL) {
     return ThreadSources._blackBoxedSources.has(aURL);
   },
 
   /**
-   * Add the given source URL to the set of sources that are black boxed. If the
-   * thread is currently paused and we are black boxing the yougest frame's
-   * source, this will force a step.
+   * Add the given source URL to the set of sources that are black boxed.
    *
    * @param aURL String
    *        The URL of the source which we are black boxing.
    */
   blackBox: function TS_blackBox(aURL) {
     ThreadSources._blackBoxedSources.add(aURL);
   },
 
@@ -3986,16 +4028,53 @@ ThreadSources.prototype = {
    * @param aURL String
    *        The URL of the source which we are no longer black boxing.
    */
   unblackBox: function TS_unblackBox(aURL) {
     ThreadSources._blackBoxedSources.delete(aURL);
   },
 
   /**
+   * Returns true if the given URL is pretty printed.
+   *
+   * @param aURL String
+   *        The URL of the source that might be pretty printed.
+   */
+  isPrettyPrinted: function TS_isPrettyPrinted(aURL) {
+    return ThreadSources._prettyPrintedSources.has(aURL);
+  },
+
+  /**
+   * Add the given URL to the set of sources that are pretty printed.
+   *
+   * @param aURL String
+   *        The URL of the source to be pretty printed.
+   */
+  prettyPrint: function TS_prettyPrint(aURL, aIndent) {
+    ThreadSources._prettyPrintedSources.set(aURL, aIndent);
+  },
+
+  /**
+   * Return the indent the given URL was pretty printed by.
+   */
+  prettyPrintIndent: function TS_prettyPrintIndent(aURL) {
+    return ThreadSources._prettyPrintedSources.get(aURL);
+  },
+
+  /**
+   * Remove the given URL from the set of sources that are pretty printed.
+   *
+   * @param aURL String
+   *        The URL of the source that is no longer pretty printed.
+   */
+  disablePrettyPrint: function TS_disablePrettyPrint(aURL) {
+    ThreadSources._prettyPrintedSources.delete(aURL);
+  },
+
+  /**
    * Normalize multiple relative paths towards the base paths on the right.
    */
   _normalize: function TS__normalize(...aURLs) {
     dbg_assert(aURLs.length > 1, "Should have more than 1 URL");
     let base = Services.io.newURI(aURLs.pop(), null, null);
     let url;
     while ((url = aURLs.pop())) {
       base = Services.io.newURI(url, null, base);