Merge Fx-Team to Mozilla-Central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 25 Oct 2013 11:21:15 +0200
changeset 165917 dff9376142682cf42e11dcbebaa28c123f2ceabc
parent 165876 186e834d87dc7b95ccb5f7b51bee6620932584f1 (current diff)
parent 165916 f9208ba78a223b3c874d7c1da3b3ec30be5ea883 (diff)
child 165942 9f8233fcce1d3f0676bc720303dc7bbd7e246c13
child 170489 db37e3b9371d47c0dca85cb5b4884ddcdd84c589
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
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
Merge Fx-Team to Mozilla-Central
browser/app/profile/firefox.js
browser/base/content/browser.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1179,32 +1179,34 @@ pref("devtools.gcli.hideIntro", false);
 pref("devtools.gcli.eagerHelper", 2);
 
 // Remember the Web Console filters
 pref("devtools.webconsole.filter.network", true);
 pref("devtools.webconsole.filter.networkinfo", true);
 pref("devtools.webconsole.filter.netwarn", true);
 pref("devtools.webconsole.filter.csserror", true);
 pref("devtools.webconsole.filter.cssparser", true);
+pref("devtools.webconsole.filter.csslog", false);
 pref("devtools.webconsole.filter.exception", true);
 pref("devtools.webconsole.filter.jswarn", true);
 pref("devtools.webconsole.filter.jslog", true);
 pref("devtools.webconsole.filter.error", true);
 pref("devtools.webconsole.filter.warn", true);
 pref("devtools.webconsole.filter.info", true);
 pref("devtools.webconsole.filter.log", true);
 pref("devtools.webconsole.filter.secerror", true);
 pref("devtools.webconsole.filter.secwarn", true);
 
 // Remember the Browser Console filters
 pref("devtools.browserconsole.filter.network", true);
 pref("devtools.browserconsole.filter.networkinfo", true);
 pref("devtools.browserconsole.filter.netwarn", true);
 pref("devtools.browserconsole.filter.csserror", true);
 pref("devtools.browserconsole.filter.cssparser", true);
+pref("devtools.browserconsole.filter.csslog", false);
 pref("devtools.browserconsole.filter.exception", true);
 pref("devtools.browserconsole.filter.jswarn", true);
 pref("devtools.browserconsole.filter.jslog", true);
 pref("devtools.browserconsole.filter.error", true);
 pref("devtools.browserconsole.filter.warn", true);
 pref("devtools.browserconsole.filter.info", true);
 pref("devtools.browserconsole.filter.log", true);
 pref("devtools.browserconsole.filter.secerror", true);
@@ -1290,18 +1292,20 @@ pref("pdfjs.disabled", false);
 // Used by pdf.js to know the first time firefox is run with it installed so it
 // can become the default pdf viewer.
 pref("pdfjs.firstRun", true);
 // The values of preferredAction and alwaysAskBeforeHandling before pdf.js
 // became the default.
 pref("pdfjs.previousHandler.preferredAction", 0);
 pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
 
+#ifdef NIGHTLY_BUILD
 // Shumway component (SWF player) is disabled by default. Also see bug 904346.
 pref("shumway.disabled", true);
+#endif
 
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 // Default social providers
 pref("social.manifest.facebook", "{\"origin\":\"https://www.facebook.com\",\"name\":\"Facebook Messenger\",\"workerURL\":\"https://www.facebook.com/desktop/fbdesktop2/socialfox/fbworker.js.php\",\"shareURL\":\"https://www.facebook.com/sharer/sharer.php?u=%{url}\",\"iconURL\":\"data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAAX0lEQVQ4jWP4%2F%2F8%2FAyUYTFhHzjgDxP9JxGeQDSBVMxgTbUBCxer%2Fr999%2BQ8DJBuArJksA9A10s8AXIBoA0B%2BR%2FY%2FjD%2BEwoBoA1yT5v3PbdmCE8MAshhID%2FUMoDgzUYIBj0Cgi7ar4coAAAAASUVORK5CYII%3D\",\"sidebarURL\":\"https://www.facebook.com/desktop/fbdesktop2/?socialfox=true\",\"icon32URL\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAADbklEQVRYCc1Xv08UQRj99tctexAuCEFjRE0kGBEtLDSGqIWNxkYKbTAxNlY2JhaGWltNtNFeKgsKKxITK43/gCYW+IsoRhA4D47bH7fn9+bcvdm5JR7sefolC3Ozu9978+bNN7PayUv3HN3umdY0Y6IWBtSJ0HSTarXqTOiuTep6Lj+tdxAcA8RAgSmwdd2aCDs0clldYALb/FvgYVhjmfliVA2XpjEgWo0Attn42Z6WH1RFor5ehwo9XQIUZMoVn4qlCoVMSo62EvD8Kh0b3U2Xz43R2PBO6mUCGDlAf65V6MadZzT/rUimoccc2kYA4BfPHqJb105RzjJigKhRq9kEJUBIjgYVuXeL7SAI6eD+Abp5dTwVHOmEHxT50d8WBYJqSOdPj5BjW8gZR8UNqFR2xagx/65XFYaMH+BGWwiYpi4UkBPPLxTp9v1Z+lHc4DWvCQXWmIy6EjITgKowVd5Jjv7N3Hd6y5esigoOwpkJIAmMpZpLJGdiaaC4F0UmAj6bD84GCEwmB/qxMmRilmnwb/mpjAocHh4UEoNAt5NLZB7oy9OJo0PxqkAtePdhiSqunyC1LQUwWMPQaOr6GRre258Ajn4cP7KHcEXhsxpXbj+lT19X2TMNGTLVAcjcalS8gDwsQ2UOMhH4k8FkcrEn5E5ub2sKohxLK2VR77Hl9RUcsrgeRIEiVOT6z+tDbIeLy+vk+kGTCbXxycet6xhl//3f6bJEkdHYhA+mLtDIvoH4ieev5+juoxdk5+pjhALYEdXIpEB5w+NlSKSzqVQ/+H7IO6BLtl3fngGMiqhGJgIwlM6qpyUGFjySdk8m0Zg0ubeD7X9OIDEFajltRQgUJaUKx69tdgaQa0FMADuahZPMFtcEwNPm2hA7ZI5sK4aoE2NvYI+o8hkCIe7CwTv68zS0q9Dk5vpbm/8FXxitSzmMFHpsGj0wyLUheTwD2Y9fVgh1Ae0EPUgD9241ZEnld+v5kgnVZ/8fE0brVh5BK+1oCqKKF72Dk7HwBsssB/pklU1dfChy3S659H5+uelgIb+8WRv1/uGTV9Sdb5wJFlfW6fPCalMhwhSU1j2xKwKbP838GcOwJja4TqO0bjdmXxYTy1EYjFdCWoCEYZhseH/GDL3yJPHnuW6YmT7P1SlIA4768Hke4vOcsX8BE346lLHhDUQAAAAASUVORK5CYII=\", \"icon64URL\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAACNNJREFUeNrtm3tw1NUVxz/399hHHkgCaCBGEFEEREVFYFQcSoOKdkZay4z+4dDpYIsjHWx1WoTMhFi1gzBSpVgVGbU4U1sHfPESKODwEEnRYDFAICEIeZIQshs2u/v73ds/drMsyW7YLEkl2Z6Z32yy+9v7u+fc7znne8+5KzgvAjDunzlv0M13PjDZ6c4cARj0WhEoaZ1tOn3yq9XLf/tNU0O1D5Ad7wq/OpxpaXOL1j5uZAwuaGlVgwNBhULRm0XXBG6HZrlNa9uRrzfM+3DlgjIgGMsA7rl/XDdHOnNf9vosTfVuvTsaQhdkZ4iykh2rHtqydvkxwI58BhjTfv7MmP55E9/1nLNdfU15ACkVvoAaMCRvRPa+re9+DgTaPjMAx+DrJv3M67Mz+6LybWLb4NfTHhxzx31DDhZvOtqGAgNwWbjGICV9XQJB0e/KobcOP1i8qTzaAEYgaDtNU/V5A9hSaUFLuQEt2gVQSgml+j4CUAIppYgK/m0GkCjZ9xGAUNAu0LUhgJRAAAIVzwBSqVRQH4hlAClRKZAFhOgEASoFECBR8QwgUyQGdJT/B8HzCEiBNKhUJzEgBYIgQsTJAkohe9oFZHgHKvQoHtZ9K3tewfiixXABLdoFeuSSEmkF+PH4QTz7+M3o+ENptzvGtS36uSwmjMpAYF10XCllHCYoe84FlLS555Zs5jx6J6ahY+iCl98pJiDNS1hwSZop+cm91zJmxEBefGsPlu1AxKC67V3gf5oGlZSMuz6Dp2fdhWnoAEwaN5T5hsYLb+4hKB1dcgelFDpB8ifk8thDt3DO5+fZxRvxBV0IjQR0EB3KfD1GhJS0GZnnYuGcKTgdF9ZWx4/No/BJjUUrdtJqm4iL+K5SCmSAiWMHMevhcQzNzaa6ron5SzfQ7HeiaSKx+au4m6HupcJKSYZdZVI4dypuV2yo3zoql0VP3cOiFV/Q4jdiGkGhQFqMGpbJL346kbE3DEYIQWNTC39Ysp4Gr4HQtZDyiRhA0NlmSHZbRM7pr1H0m6lckeHqdGXG3jCYoqfupeC17bT49fNRXIFSNrkDTGbNGM9dtw1D10M1DI/Xx3NLP6OqETRdDy1eglPT4rqA7K56gCIrXfHCvHwGZqUnBMtR113FS/N+xHPLtuJpDa1mVobg0emjmX7vqEjsUErhaw1Q8Mo6yk4F0A1HeOW7kIlFx/u7jworRabLpmhuPjmD+iG7YNDrrhnIS09P5cW/buOeO67lkftvJt3tDE06PE7Qsnh++QYOHPOim86wcVUS0+whJug0ghTMmcK1V2eH8m2UHP++nrwhAyIwjiXDcrN5vXAGhqGhFBeMYUvJ0re2sPfgGTTDGUZrEogVopMgmGQWUCgMEWTBE5MZPSLngnGqas/w9j92s31fJfmThvPM7HyMMKRj+qgmOiBHSsnr73/B5r1V6A53KD4k3bFS8dNgckFQoWPxu1kTGDc6N7JqzR4ff/+smE+2H8FSLkx3FluLawhaG3n2iXwcppGoV/Hemt18tK0c3UwLIfUS2nVafBeQJNUXkAF+/dht3H37cKSUBC2bTTv+w98++YazPg1dT0NoIUhruoMd+2sJrtjI7381rQM3iCVrN33N++tL0c30xFNdp0GQeFRYQhcRIO0gv5xxE/fdfSO2bfNVSTmrPtxLZW0A3XSh6VporaLG1XQHu0pOU/TaOhY8+QAuZ3w6vHnnQd74536EkZ50wOsSE0zcugolbWZOG8GM/LGUVdSw6sM97D/UgGa60QxXzMJDZAq6yb7SJgr//CkLn5pOmsvR4Z5dxUdZ9t6XoKfFjNyXkqZjuoBUCpGgCyhp8eDdQ5k++UaWrdrMlr2VSFxoZlpE8YtNWGgGXx9ppnDZpxTMfTCS8gAOlJ5g8ds7kCItTIi6j6FqMVxAC2sV2RB1ekmLCTcNpH+myeyFH7BxT1Voopoe4RKJXQqhmxw45mXh0o/xeH0opThcXs2iv2wmoFyhAnbC4yX+3PgISMDShrA5XHGa3d9UITRniIeTfHASmsGhEz7mL/mI2TPv4sU3t+KzHAnu7JKpCosOkcSIICAB5hZE0OiRCM0Iwb0b6LPQdI5W+Zn/yucoYYayRk+16eK1xqRMDAHtA0r3lep0lNAjO8kfpCpMqpTF4xZEUqA7rIlOCiKpgADVWXc4FQwgEfHPB5AiByTixIDUCYJx+wJoqdIcJV5VOAWygEZcF7BToT2upFDKtuz2BrAtf8v3mju972cBJX2exso6ok6N64BhOtM11xXXPBz6v6340PcuO+DZfaJkzWqgqY3L64Bqaaz0ZV45Mkc308dG2kd97FLSaq4v317gazr5HeCLRoACFTxbfeBw+oDhWYYj4/rw+30H+rb/VMPxXQsbKnbuABqJOi4vogyRiRB5/XNvvz3zytFTDEf61eF9b0dCKTS36c4afymTsgLeQ9Ly13X/aYnzE1Uy6PV7679trNy1xe+tKwPqAH/0Vla0qw65gH7AFeG/Y3Uy9P45o0bm3PTIaplM6lTK9jWf/OBUyQcrpdXaTIyfsXQb9QcLaAn7vJd2vxY5XxBpo8pwDmgFGsLKx1oeh8OVmUUSLXUlrWZPzbdLag9v+BjUqfDzepKAyDDcZbznGHG+1NmqSKHpVlfbadJqLW+o2LHobNX+PUB1WPkfnHwYyTmX6lI7Lehr3F576NM/+T3V3wH17f2w1xkg2ggXuSvga6p8p+bgmpVKWpXAmVh+2AsNEKogdYYAJa0GT03J4obyf60HTgKe6PTTqw0QOpcQ3wXs4LlDZyq2FXrrS4uBmjDxuCw3G5eIgA46yeC5ho11pWsWW35PWTibBC4Xf+9eBLRPg0q2+s5UvHG6bMNqJYPHw7nXutxZYvIIiMoCSgbrPVX/fv7syS+3AKfC5MOmF4iRpP6RjrId8O5vrNhS1NpUWQLUholUr6muXEoatP3emrWNR9e/avk9R8P+HuxNypPkrk93pGdnK0VtXemaN6UdOHo55vdE5b/0NKx+K4AxtAAAAABJRU5ErkJggg==\", \"description\":\"Keep up with friends wherever you go on the web.\",\"author\":\"Facebook\",\"homepageURL\":\"https://www.facebook.com/about/messenger-for-firefox\",\"builtin\":\"true\"}");
--- a/browser/base/content/aboutSocialError.xhtml
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -107,15 +107,27 @@
       config.tryAgainCallback();
     }
 
     function loadQueryURL() {
       window.location.href = config.queryURL;
     }
 
     function reloadProvider() {
+      // Just incase the current provider *isn't* in a frameworker-error
+      // state, reload the current one.
       Social.provider.reload();
+      // If the problem is a frameworker-error, it may be that the child
+      // process crashed - and if that happened, then *all* providers in that
+      // process will have crashed.  However, only the current provider is
+      // likely to have the error surfaced in the UI - so we reload *all*
+      // providers that are in a frameworker-error state.
+      for (let provider of Social.providers) {
+        if (provider.enabled && provider.errorState == "frameworker-error") {
+          provider.reload();
+        }
+      }
     }
 
     parseQueryString();
     setUpStrings();
   ]]></script>
 </html>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1210,19 +1210,17 @@ var gBrowserInit = {
       // Bug 862519 - Backspace doesn't work in electrolysis builds.
       // We bypass the problem by disabling the backspace-to-go-back command.
       document.getElementById("cmd_handleBackspace").setAttribute("disabled", true);
       document.getElementById("key_delete").setAttribute("disabled", true);
     }
 
     SessionStore.promiseInitialized.then(() => {
       // Enable the Restore Last Session command if needed
-      if (SessionStore.canRestoreLastSession &&
-          !PrivateBrowsingUtils.isWindowPrivate(window))
-        goSetCommandEnabled("Browser:RestoreLastSession", true);
+      RestoreLastSessionObserver.init();
 
       TabView.init();
 
       setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
     });
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
@@ -6716,29 +6714,26 @@ var gIdentityHandler = {
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
   },
 
   /**
    * Click handler for the identity-box element in primary chrome.
    */
   handleIdentityButtonEvent : function(event) {
-    TelemetryStopwatch.start("FX_IDENTITY_POPUP_OPEN_MS");
     event.stopPropagation();
 
     if ((event.type == "click" && event.button != 0) ||
         (event.type == "keypress" && event.charCode != KeyEvent.DOM_VK_SPACE &&
          event.keyCode != KeyEvent.DOM_VK_RETURN)) {
-      TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS");
       return; // Left click, space or enter only
     }
 
     // Don't allow left click, space or enter if the location has been modified.
     if (gURLBar.getAttribute("pageproxystate") != "valid") {
-      TelemetryStopwatch.cancel("FX_IDENTITY_POPUP_OPEN_MS");
       return;
     }
 
     // Make sure that the display:none style we set in xul is removed now that
     // the popup is actually needed
     this._identityPopup.hidden = false;
 
     // Update the popup strings
@@ -6754,18 +6749,16 @@ var gIdentityHandler = {
       self._identityBox.removeAttribute("open");
     }, false);
 
     // Now open the popup, anchored off the primary chrome element
     this._identityPopup.openPopup(this._identityIcon, "bottomcenter topleft");
   },
 
   onPopupShown : function(event) {
-    TelemetryStopwatch.finish("FX_IDENTITY_POPUP_OPEN_MS");
-
     document.getElementById('identity-popup-more-info-button').focus();
 
     this._identityPopup.addEventListener("blur", this, true);
     this._identityPopup.addEventListener("popuphidden", this);
   },
 
   onDragStart: function (event) {
     if (gURLBar.getAttribute("pageproxystate") != "valid")
@@ -6996,16 +6989,36 @@ function switchToTabHavingURI(aURI, aOpe
       gBrowser.selectedBrowser.loadURI(aURI.spec);
     else
       openUILinkIn(aURI.spec, "tab");
   }
 
   return false;
 }
 
+let RestoreLastSessionObserver = {
+  init: function () {
+    if (SessionStore.canRestoreLastSession &&
+        !PrivateBrowsingUtils.isWindowPrivate(window)) {
+      Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
+      goSetCommandEnabled("Browser:RestoreLastSession", true);
+    }
+  },
+
+  observe: function () {
+    // The last session can only be restored once so there's
+    // no way we need to re-enable our menu item.
+    Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
+    goSetCommandEnabled("Browser:RestoreLastSession", false);
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
 function restoreLastSession() {
   SessionStore.restoreLastSession();
 }
 
 var TabContextMenu = {
   contextTab: null,
   updateContextMenu: function updateContextMenu(aPopupMenu) {
     this.contextTab = aPopupMenu.triggerNode.localName == "tab" ?
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -7,16 +7,17 @@ support-files =
   opengraph/opengraph.html
   opengraph/shortlink_linkrel.html
   opengraph/shorturl_link.html
   opengraph/shorturl_linkrel.html
   share.html
   social_activate.html
   social_activate_iframe.html
   social_chat.html
+  social_crash_content_helper.js
   social_flyout.html
   social_mark.html
   social_panel.html
   social_sidebar.html
   social_sidebar_empty.html
   social_window.html
   social_worker.js
   unchecked.jpg
@@ -37,8 +38,9 @@ support-files =
 [browser_social_mozSocial_API.js]
 [browser_social_multiprovider.js]
 [browser_social_multiworker.js]
 [browser_social_perwindowPB.js]
 [browser_social_sidebar.js]
 [browser_social_status.js]
 [browser_social_toolbar.js]
 [browser_social_window.js]
+[browser_social_workercrash.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_workercrash.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This tests our recovery if a child content process hosting providers
+// crashes.
+
+// A content script we inject into one of our browsers
+const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/content/test/social/social_crash_content_helper.js";
+
+let {getFrameWorkerHandle} = Cu.import("resource://gre/modules/FrameWorker.jsm", {});
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
+  // We need to ensure all our workers are in the same content process.
+  Services.prefs.setIntPref("dom.ipc.processCount", 1);
+
+  runSocialTestWithProvider(gProviders, function (finishcb) {
+    Social.enabled = true;
+    runSocialTests(tests, undefined, undefined, function() {
+      Services.prefs.clearUserPref("dom.ipc.processCount");
+      Services.prefs.clearUserPref("social.sidebar.open");
+      Services.prefs.clearUserPref("social.allowMultipleWorkers");
+      finishcb();
+    });
+  });
+}
+
+let gProviders = [
+  {
+    name: "provider 1",
+    origin: "https://example.com",
+    sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1",
+    workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
+    iconURL: "chrome://branding/content/icon48.png"
+  },
+  {
+    name: "provider 2",
+    origin: "https://test1.example.com",
+    sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2",
+    workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
+    iconURL: "chrome://branding/content/icon48.png"
+  }
+];
+
+var tests = {
+  testCrash: function(next) {
+    // open the sidebar, then crash the child.
+    let sbrowser = document.getElementById("social-sidebar-browser");
+    onSidebarLoad(function() {
+      // get the browser element for our provider.
+      let fw = getFrameWorkerHandle(gProviders[0].workerURL);
+      fw.port.close();
+      fw._worker.browserPromise.then(browser => {
+        let mm = browser.messageManager;
+        mm.loadFrameScript(TEST_CONTENT_HELPER, false);
+        // add an observer for the crash - after it sees the crash we attempt
+        // a reload.
+        let observer = new crashObserver(function() {
+          info("Saw the process crash.")
+          Services.obs.removeObserver(observer, 'ipc:content-shutdown');
+          // Add another sidebar load listener - it should be the error page.
+          onSidebarLoad(function() {
+            ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
+            // after reloading, the sidebar should reload
+            onSidebarLoad(function() {
+              // now ping both workers - they should both be alive.
+              ensureWorkerLoaded(gProviders[0], function() {
+                ensureWorkerLoaded(gProviders[1], function() {
+                  // and we are done!
+                  next();
+                });
+              });
+            });
+            // click the try-again button.
+            sbrowser.contentDocument.getElementById("btnTryAgain").click();
+          });
+        });
+        Services.obs.addObserver(observer, 'ipc:content-shutdown', false);
+        // and cause the crash.
+        mm.sendAsyncMessage("social-test:crash");
+      });
+    })
+    Services.prefs.setBoolPref("social.sidebar.open", true);
+  },
+}
+
+function onSidebarLoad(callback) {
+  let sbrowser = document.getElementById("social-sidebar-browser");
+  sbrowser.addEventListener("load", function load() {
+    sbrowser.removeEventListener("load", load, true);
+    callback();
+  }, true);
+}
+
+function ensureWorkerLoaded(manifest, callback) {
+  let fw = getFrameWorkerHandle(manifest.workerURL);
+  // once the worker responds to a ping we know it must be up.
+  let port = fw.port;
+  port.onmessage = function(msg) {
+    if (msg.data.topic == "pong") {
+      port.close();
+      callback();
+    }
+  }
+  port.postMessage({topic: "ping"})
+}
+
+// More duplicated code from browser_thumbnails_brackground_crash.
+// Bug 915518 exists to unify these.
+
+// This observer is needed so we can clean up all evidence of the crash so
+// the testrunner thinks things are peachy.
+let crashObserver = function(callback) {
+  this.callback = callback;
+}
+crashObserver.prototype = {
+  observe: function(subject, topic, data) {
+    is(topic, 'ipc:content-shutdown', 'Received correct observer topic.');
+    ok(subject instanceof Components.interfaces.nsIPropertyBag2,
+       'Subject implements nsIPropertyBag2.');
+    // we might see this called as the process terminates due to previous tests.
+    // We are only looking for "abnormal" exits...
+    if (!subject.hasKey("abnormal")) {
+      info("This is a normal termination and isn't the one we are looking for...");
+      return;
+    }
+
+    var dumpID;
+    if ('nsICrashReporter' in Components.interfaces) {
+      dumpID = subject.getPropertyAsAString('dumpID');
+      ok(dumpID, "dumpID is present and not an empty string");
+    }
+
+    if (dumpID) {
+      var minidumpDirectory = getMinidumpDirectory();
+      removeFile(minidumpDirectory, dumpID + '.dmp');
+      removeFile(minidumpDirectory, dumpID + '.extra');
+    }
+    this.callback();
+  }
+}
+
+function getMinidumpDirectory() {
+  var dir = Services.dirsvc.get('ProfD', Components.interfaces.nsIFile);
+  dir.append("minidumps");
+  return dir;
+}
+function removeFile(directory, filename) {
+  var file = directory.clone();
+  file.append(filename);
+  if (file.exists()) {
+    file.remove(false);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/social_crash_content_helper.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ideally we would use CrashTestUtils.jsm, but that's only available for
+// xpcshell tests - so we just copy a ctypes crasher from it.
+Cu.import("resource://gre/modules/ctypes.jsm");
+let crash = function() { // this will crash when called.
+  let zero = new ctypes.intptr_t(8);
+  let badptr = ctypes.cast(zero, ctypes.PointerType(ctypes.int32_t));
+  badptr.contents
+};
+
+
+TestHelper = {
+  init: function() {
+    addMessageListener("social-test:crash", this);
+  },
+
+  receiveMessage: function(msg) {
+    switch (msg.name) {
+      case "social-test:crash":
+        privateNoteIntentionalCrash();
+        crash();
+      break;
+    }
+  },
+}
+
+TestHelper.init();
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -48,18 +48,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/NewTabUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
                                   "resource:///modules/BrowserNewTabPreloader.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
                                   "resource://pdf.js/PdfJs.jsm");
 
+#ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
                                   "resource://shumway/ShumwayUtils.jsm");
+#endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "webrtcUI",
                                   "resource:///modules/webrtcUI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
@@ -464,17 +466,19 @@ BrowserGlue.prototype = {
     this._syncSearchEngines();
 
     webappsUI.init();
     PageThumbs.init();
     NewTabUtils.init();
     BrowserNewTabPreloader.init();
     SignInToWebsiteUX.init();
     PdfJs.init();
+#ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
+#endif
     webrtcUI.init();
     AboutHome.init();
     SessionStore.init();
 
     if (Services.prefs.getBoolPref("browser.tabs.remote"))
       ContentClick.init();
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -18,16 +18,17 @@ const STATE_QUITTING = -1;
 const STATE_STOPPED_STR = "stopped";
 const STATE_RUNNING_STR = "running";
 
 const TAB_STATE_NEEDS_RESTORE = 1;
 const TAB_STATE_RESTORING = 2;
 
 const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
 const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
+const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
 
 // Maximum number of tabs to restore simultaneously. Previously controlled by
 // the browser.sessionstore.max_concurrent_tabs pref.
 const MAX_CONCURRENT_TAB_RESTORES = 3;
 
 // global notifications observed
 const OBSERVING = [
   "domwindowopened", "domwindowclosed",
@@ -324,22 +325,16 @@ let SessionStoreInternal = {
   _recentCrashes: 0,
 
   // whether the last window was closed and should be restored
   _restoreLastWindow: false,
 
   // number of tabs currently restoring
   _tabsRestoringCount: 0,
 
-  // The state from the previous session (after restoring pinned tabs). This
-  // state is persisted and passed through to the next session during an app
-  // restart to make the third party add-on warning not trash the deferred
-  // session
-  _lastSessionState: null,
-
   // When starting Firefox with a single private window, this is the place
   // where we keep the session we actually wanted to restore in case the user
   // decides to later open a non-private window as well.
   _deferredInitialState: null,
 
   // A promise resolved once initialization is complete
   _deferredInitialized: Promise.defer(),
 
@@ -356,26 +351,25 @@ let SessionStoreInternal = {
 
   /**
    * A promise fulfilled once initialization is complete.
    */
   get promiseInitialized() {
     return this._deferredInitialized.promise;
   },
 
-  /* ........ Public Getters .............. */
   get canRestoreLastSession() {
-    return !!this._lastSessionState;
+    return LastSession.canRestore;
   },
 
   set canRestoreLastSession(val) {
     // Cheat a bit; only allow false.
-    if (val)
-      return;
-    this._lastSessionState = null;
+    if (!val) {
+      LastSession.clear();
+    }
   },
 
   /**
    * Initialize the sessionstore service.
    */
   init: function () {
     if (this._initialized) {
       throw new Error("SessionStore.init() must only be called once!");
@@ -414,23 +408,25 @@ let SessionStoreInternal = {
         if (ss.sessionType == Ci.nsISessionStartup.DEFER_SESSION) {
           let [iniState, remainingState] = this._prepDataForDeferredRestore(state);
           // If we have a iniState with windows, that means that we have windows
           // with app tabs to restore.
           if (iniState.windows.length)
             state = iniState;
           else
             state = null;
-          if (remainingState.windows.length)
-            this._lastSessionState = remainingState;
+
+          if (remainingState.windows.length) {
+            LastSession.setState(remainingState);
+          }
         }
         else {
           // Get the last deferred session in case the user still wants to
           // restore it
-          this._lastSessionState = state.lastSessionState;
+          LastSession.setState(state.lastSessionState);
 
           let lastSessionCrashed =
             state.session && state.session.state &&
             state.session.state == STATE_RUNNING_STR;
           if (lastSessionCrashed) {
             this._recentCrashes = (state.session &&
                                    state.session.recentCrashes || 0) + 1;
 
@@ -516,25 +512,21 @@ let SessionStoreInternal = {
     this._prefBranch = Services.prefs.getBranch("browser.");
 
     gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
 
     Services.prefs.addObserver("browser.sessionstore.debug", () => {
       gDebuggingEnabled = this._prefBranch.getBoolPref("sessionstore.debug");
     }, false);
 
-    XPCOMUtils.defineLazyGetter(this, "_max_tabs_undo", function () {
-      this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
-      return this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
-    });
-
-    XPCOMUtils.defineLazyGetter(this, "_max_windows_undo", function () {
-      this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
-      return this._prefBranch.getIntPref("sessionstore.max_windows_undo");
-    });
+    this._max_tabs_undo = this._prefBranch.getIntPref("sessionstore.max_tabs_undo");
+    this._prefBranch.addObserver("sessionstore.max_tabs_undo", this, true);
+
+    this._max_windows_undo = this._prefBranch.getIntPref("sessionstore.max_windows_undo");
+    this._prefBranch.addObserver("sessionstore.max_windows_undo", this, true);
   },
 
   /**
    * Called on application shutdown, after notifications:
    * quit-application-granted, quit-application
    */
   _uninit: function ssi_uninit() {
     if (!this._initialized) {
@@ -1061,34 +1053,34 @@ let SessionStoreInternal = {
       // session data on disk on a restart.  It is also unnecessary to
       // perform any other sanitization processing on a restart as the
       // browser is about to exit anyway.
       Services.obs.removeObserver(this, "browser:purge-session-history");
     }
 
     if (aData != "restart") {
       // Throw away the previous session on shutdown
-      this._lastSessionState = null;
+      LastSession.clear();
     }
 
     this._loadState = STATE_QUITTING; // just to be sure
     this._uninit();
   },
 
   /**
    * On purge of session history
    */
   onPurgeSessionHistory: function ssi_onPurgeSessionHistory() {
     _SessionFile.wipe();
     // If the browser is shutting down, simply return after clearing the
     // session data on disk as this notification fires after the
     // quit-application notification so the browser is about to exit.
     if (this._loadState == STATE_QUITTING)
       return;
-    this._lastSessionState = null;
+    LastSession.clear();
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
         TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
         delete aTab.linkedBrowser.__SS_tabStillLoading;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
@@ -1764,17 +1756,17 @@ let SessionStoreInternal = {
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
       TabStateCache.clear();
       this.saveStateDelayed();
     }
   },
 
   /**
-   * Restores the session state stored in _lastSessionState. This will attempt
+   * Restores the session state stored in LastSession. This will attempt
    * to merge data into the current session. If a window was opened at startup
    * with pinned tab(s), then the remaining data from the previous session for
    * that window will be opened into that winddow. Otherwise new windows will
    * be opened.
    */
   restoreLastSession: function ssi_restoreLastSession() {
     // Use the public getter since it also checks PB mode
     if (!this.canRestoreLastSession)
@@ -1782,17 +1774,17 @@ let SessionStoreInternal = {
 
     // First collect each window with its id...
     let windows = {};
     this._forEachBrowserWindow(function(aWindow) {
       if (aWindow.__SS_lastSessionWindowID)
         windows[aWindow.__SS_lastSessionWindowID] = aWindow;
     });
 
-    let lastSessionState = this._lastSessionState;
+    let lastSessionState = LastSession.getState();
 
     // This shouldn't ever be the case...
     if (!lastSessionState.windows.length)
       throw (Components.returnCode = Cr.NS_ERROR_UNEXPECTED);
 
     // We're technically doing a restore, so set things up so we send the
     // notification when we're done. We want to send "sessionstore-browser-state-restored".
     this._restoreCount = lastSessionState.windows.length;
@@ -1826,17 +1818,17 @@ let SessionStoreInternal = {
 
       // If there's a window already open that we can restore into, use that
       if (canUseWindow) {
         // Since we're not overwriting existing tabs, we want to merge _closedTabs,
         // putting existing ones first. Then make sure we're respecting the max pref.
         if (winState._closedTabs && winState._closedTabs.length) {
           let curWinState = this._windows[windowToUse.__SSi];
           curWinState._closedTabs = curWinState._closedTabs.concat(winState._closedTabs);
-          curWinState._closedTabs.splice(this._prefBranch.getIntPref("sessionstore.max_tabs_undo"), curWinState._closedTabs.length);
+          curWinState._closedTabs.splice(this._max_tabs_undo, curWinState._closedTabs.length);
         }
 
         // Restore into that window - pretend it's a followup since we'll already
         // have a focused window.
         //XXXzpao This is going to merge extData together (taking what was in
         //        winState over what is in the window already. The hack we have
         //        in _preWindowToRestoreInto will prevent most (all?) Panorama
         //        weirdness but we will still merge other extData.
@@ -1861,17 +1853,17 @@ let SessionStoreInternal = {
 
     // Set data that persists between sessions
     this._recentCrashes = lastSessionState.session &&
                           lastSessionState.session.recentCrashes || 0;
 
     // Update the session start time using the restored session state.
     this._updateSessionStartTime(lastSessionState);
 
-    this._lastSessionState = null;
+    LastSession.clear();
   },
 
   /**
    * See if aWindow is usable for use when restoring a previous session via
    * restoreLastSession. If usable, prepare it for use.
    *
    * @param aWindow
    *        the window to inspect & prepare
@@ -2122,18 +2114,18 @@ let SessionStoreInternal = {
       windows: total,
       selectedWindow: ix + 1,
       _closedWindows: lastClosedWindowsCopy,
       session: session,
       scratchpads: scratchpads
     };
 
     // Persist the last session if we deferred restoring it
-    if (this._lastSessionState) {
-      state.lastSessionState = this._lastSessionState;
+    if (LastSession.canRestore) {
+      state.lastSessionState = LastSession.getState();
     }
 
     // If we were called by the SessionSaver and started with only a private
     // window we want to pass the deferred initial state to not lose the
     // previous session.
     if (this._deferredInitialState) {
       state.deferredInitialState = this._deferredInitialState;
     }
@@ -3521,18 +3513,18 @@ let SessionStoreInternal = {
    * This is going to take a state as provided at startup (via
    * nsISessionStartup.state) and split it into 2 parts. The first part
    * (defaultState) will be a state that should still be restored at startup,
    * while the second part (state) is a state that should be saved for later.
    * defaultState will be comprised of windows with only pinned tabs, extracted
    * from state. It will contain the cookies that go along with the history
    * entries in those tabs. It will also contain window position information.
    *
-   * defaultState will be restored at startup. state will be placed into
-   * this._lastSessionState and will be kept in case the user explicitly wants
+   * defaultState will be restored at startup. state will be passed into
+   * LastSession and will be kept in case the user explicitly wants
    * to restore the previous session (publicly exposed as restoreLastSession).
    *
    * @param state
    *        The state, presumably from nsISessionStartup.state
    * @returns [defaultState, state]
    */
   _prepDataForDeferredRestore: function ssi_prepDataForDeferredRestore(state) {
     // Make sure that we don't modify the global state as provided by
@@ -4532,8 +4524,35 @@ let TabState = {
     TextAndScrollData.updateFrame(tabData.entries[tabIndex],
                                   browser.contentWindow,
                                   !!tabData.pinned,
                                   {includePrivateData: includePrivateData});
 
     return true;
   },
 };
+
+// The state from the previous session (after restoring pinned tabs). This
+// state is persisted and passed through to the next session during an app
+// restart to make the third party add-on warning not trash the deferred
+// session
+let LastSession = {
+  _state: null,
+
+  get canRestore() {
+    return !!this._state;
+  },
+
+  getState: function () {
+    return this._state;
+  },
+
+  setState: function (state) {
+    this._state = state;
+  },
+
+  clear: function () {
+    if (this._state) {
+      this._state = null;
+      Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_CLEARED, null);
+    }
+  }
+};
--- a/browser/devtools/app-manager/content/manifest-editor.js
+++ b/browser/devtools/app-manager/content/manifest-editor.js
@@ -16,16 +16,18 @@ function ManifestEditor(project) {
   this._onEval = this._onEval.bind(this);
   this._onSwitch = this._onSwitch.bind(this);
   this._onDelete = this._onDelete.bind(this);
 }
 
 ManifestEditor.prototype = {
   get manifest() { return this.project.manifest; },
 
+  get editable() { return this.project.type == "packaged"; },
+
   show: function(containerElement) {
     let deferred = promise.defer();
     let iframe = document.createElement("iframe");
 
     iframe.addEventListener("load", function onIframeLoad() {
       iframe.removeEventListener("load", onIframeLoad, true);
       deferred.resolve(iframe.contentWindow);
     }, true);
@@ -38,19 +40,22 @@ ManifestEditor.prototype = {
   },
 
   _onContainerReady: function(varWindow) {
     let variablesContainer = varWindow.document.querySelector("#variables");
 
     let editor = this.editor = new VariablesView(variablesContainer);
 
     editor.onlyEnumVisible = true;
-    editor.eval = this._onEval;
-    editor.switch = this._onSwitch;
-    editor.delete = this._onDelete;
+
+    if (this.editable) {
+      editor.eval = this._onEval;
+      editor.switch = this._onSwitch;
+      editor.delete = this._onDelete;
+    }
 
     return this.update();
   },
 
   _onEval: function(evalString) {
     let manifest = this.manifest;
     eval("manifest" + evalString);
     this.update();
@@ -85,17 +90,17 @@ ManifestEditor.prototype = {
 
     // Wait until the animation from commitHierarchy has completed
     let deferred = promise.defer();
     setTimeout(deferred.resolve, this.editor.lazyEmptyDelay + 1);
     return deferred.promise;
   },
 
   save: function() {
-    if (this.project.type == "packaged") {
+    if (this.editable) {
       let validator = new AppValidator(this.project);
       let manifestFile = validator._getPackagedManifestFile();
       let path = manifestFile.path;
 
       let encoder = new TextEncoder();
       let data = encoder.encode(JSON.stringify(this.manifest, null, 2));
 
       return OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
--- a/browser/devtools/app-manager/content/projects.js
+++ b/browser/devtools/app-manager/content/projects.js
@@ -76,29 +76,31 @@ let UI = {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder);
     let res = fp.show();
     if (res != Ci.nsIFilePicker.returnCancel)
       return fp.file;
     return null;
   },
 
-  addPackaged: function() {
-    let folder = this._selectFolder();
+  addPackaged: function(folder) {
+    if (!folder) {
+      folder = this._selectFolder();
+    }
     if (!folder)
       return;
-    AppProjects.addPackaged(folder)
-               .then(function (project) {
-                 UI.validate(project);
-                 UI.selectProject(project.location);
-               });
+    return AppProjects.addPackaged(folder)
+                      .then(function (project) {
+                        UI.validate(project);
+                        UI.selectProject(project.location);
+                      });
   },
 
   addHosted: function() {
-    let form = document.querySelector("#new-hosted-project-wrapper")
+    let form = document.querySelector("#new-hosted-project-wrapper");
     if (!form.checkValidity())
       return;
 
     let urlInput = document.querySelector("#url-input");
     let manifestURL = urlInput.value;
     return AppProjects.addHosted(manifestURL)
                       .then(function (project) {
                         UI.validate(project);
--- a/browser/devtools/app-manager/content/projects.xhtml
+++ b/browser/devtools/app-manager/content/projects.xhtml
@@ -75,14 +75,15 @@
       </div>
       <div class="project-buttons">
         <button class="project-button-update" onclick="UI.update(this, this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}' title="&projects.updateAppTooltip;">&projects.updateApp;</button>
         <button class="device-action project-button-debug" onclick="UI.debug(this, this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}' title="&projects.debugAppTooltip;">&projects.debugApp;</button>
       </div>
       <div class="project-errors" template='{"type":"textContent","path":"errors"}'></div>
       <div class="project-warnings" template='{"type":"textContent","path":"warnings"}'></div>
     </div>
-    <div class="manifest-editor">
-      <h2>&projects.manifestEditor;</h2>
+    <div class="manifest-editor" template='{"type":"attribute","path":"type","name":"type"}'>
+      <h2 class="editable" title="&projects.manifestEditorTooltip;">&projects.manifestEditor;</h2>
+      <h2 class="viewable" title="&projects.manifestViewerTooltip;">&projects.manifestViewer;</h2>
     </div>
   </div>
   </template>
 </html>
--- a/browser/devtools/app-manager/test/browser.ini
+++ b/browser/devtools/app-manager/test/browser.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 support-files =
   head.js
   hosted_app.manifest
+  manifest.webapp
 
 [browser_manifest_editor.js]
--- a/browser/devtools/app-manager/test/browser_manifest_editor.js
+++ b/browser/devtools/app-manager/test/browser_manifest_editor.js
@@ -8,20 +8,20 @@ const MANIFEST_EDITOR_ENABLED = "devtool
 
 function test() {
   waitForExplicitFinish();
 
   Task.spawn(function() {
     Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, true);
     let tab = yield openAppManager();
     yield selectProjectsPanel();
-    yield addSampleHostedApp();
+    yield addSamplePackagedApp();
     yield showSampleProjectDetails();
     yield changeManifestValue("name", "the best app");
-    yield removeSampleHostedApp();
+    yield removeSamplePackagedApp();
     yield removeTab(tab);
     Services.prefs.setBoolPref(MANIFEST_EDITOR_ENABLED, false);
     finish();
   });
 }
 
 function changeManifestValue(key, value) {
   return Task.spawn(function() {
--- a/browser/devtools/app-manager/test/head.js
+++ b/browser/devtools/app-manager/test/head.js
@@ -1,27 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-const {utils: Cu} = Components;
+const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
 const {Promise: promise} =
   Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
 const {devtools} =
   Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
 
 const {AppProjects} = require("devtools/app-manager/app-projects");
 
 const APP_MANAGER_URL = "about:app-manager";
 const TEST_BASE =
   "chrome://mochitests/content/browser/browser/devtools/app-manager/test/";
 const HOSTED_APP_MANIFEST = TEST_BASE + "hosted_app.manifest";
 
+const PACKAGED_APP_DIR_PATH = getTestFilePath(".");
+
 function addTab(url, targetWindow = window) {
   info("Adding tab: " + url);
 
   let deferred = promise.defer();
   let targetBrowser = targetWindow.gBrowser;
 
   targetWindow.focus();
   let tab = targetBrowser.selectedTab = targetBrowser.addTab(url);
@@ -67,16 +69,28 @@ function addSampleHostedApp() {
   return projectsWindow.UI.addHosted();
 }
 
 function removeSampleHostedApp() {
   info("Removing sample hosted app");
   return AppProjects.remove(HOSTED_APP_MANIFEST);
 }
 
+function addSamplePackagedApp() {
+  info("Adding sample packaged app");
+  let appDir = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);
+  appDir.initWithPath(PACKAGED_APP_DIR_PATH);
+  return getProjectsWindow().UI.addPackaged(appDir);
+}
+
+function removeSamplePackagedApp() {
+  info("Removing sample packaged app");
+  return AppProjects.remove(PACKAGED_APP_DIR_PATH);
+}
+
 function getProjectsWindow() {
   return content.document.querySelector(".projects-panel").contentWindow;
 }
 
 function getManifestWindow() {
   return getProjectsWindow().document.querySelector(".variables-view")
          .contentWindow;
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/app-manager/test/manifest.webapp
@@ -0,0 +1,3 @@
+{
+  "name": "My packaged app"
+}
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -1831,19 +1831,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                       [dateString, timeString]) + ".png";
         }
         // Check there is a .png extension to filename
         else if (!filename.match(/.png$/i)) {
           filename += ".png";
         }
         // If the filename is relative, tack it onto the download directory
         if (!filename.match(/[\\\/]/)) {
-          let tempfile = yield Downloads.getPreferredDownloadsDirectory();
-          tempfile.append(filename);
-          filename = tempfile.path;
+          let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+          filename = OS.Path.join(preferredDir, filename);
         }
 
         try {
           file.initWithPath(filename);
         } catch (ex) {
           div.textContent = gcli.lookup("screenshotErrorSavingToFile") + " " + filename;
           throw new Task.Result(div);
         }
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -10,17 +10,16 @@
  */
 function SourcesView() {
   dumpn("SourcesView was instantiated");
 
   this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
   this._onEditorLoad = this._onEditorLoad.bind(this);
   this._onEditorUnload = this._onEditorUnload.bind(this);
   this._onEditorCursorActivity = this._onEditorCursorActivity.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.toggleBlackBoxing = this.toggleBlackBoxing.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);
@@ -644,25 +643,23 @@ SourcesView.prototype = Heritage.extend(
     }
   },
 
   /**
    * The load listener for the source editor.
    */
   _onEditorLoad: function(aName, aEditor) {
     aEditor.on("cursorActivity", this._onEditorCursorActivity);
-    aEditor.on("contextMenu", this._onEditorContextMenu);
   },
 
   /**
    * The unload listener for the source editor.
    */
   _onEditorUnload: function(aName, aEditor) {
     aEditor.off("cursorActivity", this._onEditorCursorActivity);
-    aEditor.off("contextMenu", this._onEditorContextMenu);
   },
 
   /**
    * The selection listener for the source editor.
    */
   _onEditorCursorActivity: function(e) {
     let editor = DebuggerView.editor;
     let start  = editor.getCursor("start").line + 1;
@@ -674,23 +671,16 @@ SourcesView.prototype = Heritage.extend(
     if (this.getBreakpoint(location) && start == end) {
       this.highlightBreakpoint(location, { noEditorUpdate: true });
     } else {
       this.unhighlightBreakpoint();
     }
   },
 
   /**
-   * The context menu listener for the source editor.
-   */
-  _onEditorContextMenu: function({ x, y }) {
-    this._editorContextMenuLineNumber = DebuggerView.editor.getPositionFromCoords(x, y).line;
-  },
-
-  /**
    * The select listener for the sources container.
    */
   _onSourceSelect: function({ detail: sourceItem }) {
     if (!sourceItem) {
       return;
     }
     // The container is not empty and an actual item was selected.
     DebuggerView.setEditorLocation(sourceItem.value);
@@ -869,25 +859,16 @@ SourcesView.prototype = Heritage.extend(
       this._hideConditionalPopup();
     }
   },
 
   /**
    * Called when the add breakpoint key sequence was pressed.
    */
   _onCmdAddBreakpoint: function() {
-    // If this command was executed via the context menu, add the breakpoint
-    // on the currently hovered line in the source editor.
-    if (this._editorContextMenuLineNumber >= 0) {
-      DebuggerView.editor.setCursor({ line: this._editorContextMenuLineNumber, ch: 0 });
-    }
-
-    // Avoid placing breakpoints incorrectly when using key shortcuts.
-    this._editorContextMenuLineNumber = -1;
-
     let url = DebuggerView.Sources.selectedValue;
     let line = DebuggerView.editor.getCursor().line + 1;
     let location = { url: url, line: line };
     let breakpointItem = this.getBreakpoint(location);
 
     // If a breakpoint already existed, remove it now.
     if (breakpointItem) {
       DebuggerController.Breakpoints.removeBreakpoint(location);
@@ -897,25 +878,16 @@ SourcesView.prototype = Heritage.extend(
       DebuggerController.Breakpoints.addBreakpoint(location);
     }
   },
 
   /**
    * Called when the add conditional breakpoint key sequence was pressed.
    */
   _onCmdAddConditionalBreakpoint: function() {
-    // If this command was executed via the context menu, add the breakpoint
-    // on the currently hovered line in the source editor.
-    if (this._editorContextMenuLineNumber >= 0) {
-      DebuggerView.editor.setCursor({ line: this._editorContextMenuLineNumber, ch: 0 });
-    }
-
-    // Avoid placing breakpoints incorrectly when using key shortcuts.
-    this._editorContextMenuLineNumber = -1;
-
     let url =  DebuggerView.Sources.selectedValue;
     let line = DebuggerView.editor.getCursor().line + 1;
     let location = { url: url, line: line };
     let breakpointItem = this.getBreakpoint(location);
 
     // If a breakpoint already existed or wasn't a conditional, morph it now.
     if (breakpointItem) {
       this.highlightBreakpoint(location, { openPopup: true });
@@ -1038,17 +1010,16 @@ SourcesView.prototype = Heritage.extend(
   },
 
   _commandset: null,
   _popupset: null,
   _cmPopup: null,
   _cbPanel: null,
   _cbTextbox: null,
   _selectedBreakpointItem: null,
-  _editorContextMenuLineNumber: -1,
   _conditionalPopupVisible: false
 });
 
 /**
  * Utility functions for handling sources.
  */
 let SourceUtils = {
   _labelsCache: new Map(), // Can't use WeakMaps because keys are strings.
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -857,17 +857,17 @@ FilterView.prototype = {
    * (Jump to lines in the currently visible source).
    *
    * @param number aLine
    *        The source line number to jump to.
    */
   _performLineSearch: function(aLine) {
     // Make sure we're actually searching for a valid line.
     if (aLine) {
-      DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 });
+      DebuggerView.editor.setCursor({ line: aLine - 1, ch: 0 }, "center");
     }
   },
 
   /**
    * Performs a token search if necessary.
    * (Search for tokens in the currently visible source).
    *
    * @param string aToken
@@ -1492,16 +1492,17 @@ FilteredFunctionsView.prototype = Herita
     if (functionItem) {
       let sourceUrl = functionItem.attachment.sourceUrl;
       let scriptOffset = functionItem.attachment.scriptOffset;
       let actualLocation = functionItem.attachment.actualLocation;
 
       DebuggerView.setEditorLocation(sourceUrl, actualLocation.start.line, {
         charOffset: scriptOffset,
         columnOffset: actualLocation.start.column,
+        align: "center",
         noDebug: true
       });
     }
   },
 
   _searchTimeout: null,
   _searchFunction: null,
   _searchedToken: ""
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -452,17 +452,18 @@ let DebuggerView = {
         aLine += this.editor.getPosition(aFlags.charOffset).line;
       }
 
       if (aFlags.lineOffset) {
         aLine += aFlags.lineOffset;
       }
 
       if (!aFlags.noCaret) {
-        this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 });
+        this.editor.setCursor({ line: aLine -1, ch: aFlags.columnOffset || 0 },
+                              aFlags.align);
       }
 
       if (!aFlags.noDebug) {
         this.editor.setDebugLocation(aLine - 1);
       }
     }).then(null, console.error);
   },
 
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -11,16 +11,17 @@ support-files =
   code_blackboxing_two.js
   code_function-search-01.js
   code_function-search-02.js
   code_function-search-03.js
   code_location-changes.js
   code_math.js
   code_math.map
   code_math.min.js
+  code_math_bogus_map.min.js
   code_script-switching-01.js
   code_script-switching-02.js
   code_test-editor-mode
   code_ugly.js
   code_ugly-2.js
   code_ugly-3.js
   code_ugly-4.js
   doc_binary_search.html
@@ -38,16 +39,17 @@ support-files =
   doc_function-display-name.html
   doc_function-search.html
   doc_iframes.html
   doc_included-script.html
   doc_inline-debugger-statement.html
   doc_inline-script.html
   doc_large-array-buffer.html
   doc_minified.html
+  doc_minified_bogus_map.html
   doc_pause-exceptions.html
   doc_pretty-print.html
   doc_pretty-print-2.html
   doc_random-javascript.html
   doc_recursion-stack.html
   doc_script-switching-01.html
   doc_script-switching-02.html
   doc_step-out.html
@@ -147,16 +149,17 @@ support-files =
 [browser_dbg_search-sources-03.js]
 [browser_dbg_search-symbols.js]
 [browser_dbg_searchbox-help-popup-01.js]
 [browser_dbg_searchbox-help-popup-02.js]
 [browser_dbg_searchbox-parse.js]
 [browser_dbg_source-maps-01.js]
 [browser_dbg_source-maps-02.js]
 [browser_dbg_source-maps-03.js]
+[browser_dbg_source-maps-04.js]
 [browser_dbg_sources-cache.js]
 [browser_dbg_sources-labels.js]
 [browser_dbg_sources-sorting.js]
 [browser_dbg_stack-01.js]
 [browser_dbg_stack-02.js]
 [browser_dbg_stack-03.js]
 [browser_dbg_stack-04.js]
 [browser_dbg_stack-05.js]
--- a/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_conditional-breakpoints-02.js
@@ -87,52 +87,52 @@ function test() {
   function addBreakpoint1() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
     gPanel.addBreakpoint({ url: gSources.selectedValue, line: 18 });
     return finished;
   }
 
   function addBreakpoint2() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
-    setContextPosition(19);
+    setCaretPosition(19);
     gSources._onCmdAddBreakpoint();
     return finished;
   }
 
   function modBreakpoint2() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_SHOWING);
-    setContextPosition(19);
+    setCaretPosition(19);
     gSources._onCmdAddConditionalBreakpoint();
     return finished;
   }
 
   function addBreakpoint3() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
-    setContextPosition(20);
+    setCaretPosition(20);
     gSources._onCmdAddConditionalBreakpoint();
     return finished;
   }
 
   function modBreakpoint3() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
     typeText(gSources._cbTextbox, "bamboocha");
     EventUtils.sendKey("RETURN", gDebugger);
     return finished;
   }
 
   function addBreakpoint4() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_ADDED);
-    setContextPosition(21);
+    setCaretPosition(21);
     gSources._onCmdAddBreakpoint();
     return finished;
   }
 
   function delBreakpoint4() {
     let finished = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_REMOVED);
-    setContextPosition(21);
+    setCaretPosition(21);
     gSources._onCmdAddBreakpoint();
     return finished;
   }
 
   function testBreakpoint(aLine, aOpenPopupFlag, aPopupVisible, aConditionalExpression) {
     let selectedUrl = gSources.selectedValue;
     let selectedBreakpoint = gSources._selectedBreakpointItem;
 
@@ -179,18 +179,14 @@ function test() {
     ok(isCaretPos(gPanel, aLine),
       "The editor caret position is not properly set.");
   }
 
   function setCaretPosition(aLine) {
     gEditor.setCursor({ line: aLine - 1, ch: 0 });
   }
 
-  function setContextPosition(aLine) {
-    gSources._editorContextMenuLineNumber = aLine - 1;
-  }
-
   function clickOnBreakpoint(aIndex) {
     EventUtils.sendMouseEvent({ type: "click" },
       gDebugger.document.querySelectorAll(".dbg-breakpoint")[aIndex],
       gDebugger);
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_source-maps-04.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that bogus source maps don't break debugging.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_minified_bogus_map.html";
+const JS_URL = EXAMPLE_URL + "code_math_bogus_map.min.js";
+
+// This test causes an error to be logged in the console, which appears in TBPL
+// logs, so we are disabling that here.
+let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
+DevToolsUtils.reportingDisabled = true;
+
+let gPanel, gDebugger, gFrames, gSources, gPrefs, gOptions;
+
+function test() {
+  initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+    gPanel = aPanel;
+    gDebugger = gPanel.panelWin;
+    gFrames = gDebugger.DebuggerView.StackFrames;
+    gSources = gDebugger.DebuggerView.Sources;
+    gPrefs = gDebugger.Prefs;
+    gOptions = gDebugger.DebuggerView.Options;
+
+    is(gPrefs.pauseOnExceptions, false,
+      "The pause-on-exceptions pref should be disabled by default.");
+    isnot(gOptions._pauseOnExceptionsItem.getAttribute("checked"), "true",
+      "The pause-on-exceptions menu item should not be checked.");
+
+    waitForSourceShown(gPanel, JS_URL)
+      .then(checkInitialSource)
+      .then(enablePauseOnExceptions)
+      .then(disableIgnoreCaughtExceptions)
+      .then(testSetBreakpoint)
+      .then(reloadPage)
+      .then(testHitBreakpoint)
+      .then(enableIgnoreCaughtExceptions)
+      .then(disablePauseOnExceptions)
+      .then(() => closeDebuggerAndFinish(gPanel))
+      .then(null, aError => {
+        ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+      });
+  });
+}
+
+function checkInitialSource() {
+  isnot(gSources.selectedValue.indexOf(".min.js"), -1,
+    "The debugger should show the minified js file.");
+}
+
+function enablePauseOnExceptions() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+    is(gPrefs.pauseOnExceptions, true,
+      "The pause-on-exceptions pref should now be enabled.");
+
+    ok(true, "Pausing on exceptions was enabled.");
+    deferred.resolve();
+  });
+
+  gOptions._pauseOnExceptionsItem.setAttribute("checked", "true");
+  gOptions._togglePauseOnExceptions();
+
+  return deferred.promise;
+}
+
+function disableIgnoreCaughtExceptions() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+    is(gPrefs.ignoreCaughtExceptions, false,
+      "The ignore-caught-exceptions pref should now be disabled.");
+
+    ok(true, "Ignore caught exceptions was disabled.");
+    deferred.resolve();
+  });
+
+  gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "false");
+  gOptions._toggleIgnoreCaughtExceptions();
+
+  return deferred.promise;
+}
+
+function testSetBreakpoint() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.setBreakpoint({ url: JS_URL, line: 3, column: 61 }, aResponse => {
+    ok(!aResponse.error,
+      "Should be able to set a breakpoint in a js file.");
+    ok(!aResponse.actualLocation,
+      "Should be able to set a breakpoint on line 3 and column 61.");
+
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
+
+function reloadPage() {
+  let loaded = waitForSourceAndCaret(gPanel, ".js", 3);
+  gDebugger.gClient.activeTab.reload();
+  return loaded.then(() => ok(true, "Page was reloaded and execution resumed."));
+}
+
+function testHitBreakpoint() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.resume(aResponse => {
+    ok(!aResponse.error, "Shouldn't get an error resuming.");
+    is(aResponse.type, "resumed", "Type should be 'resumed'.");
+
+    waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES).then(() => {
+      is(gFrames.itemCount, 1, "Should have one frame.");
+
+      gDebugger.gThreadClient.resume(deferred.resolve);
+    });
+  });
+
+  return deferred.promise;
+}
+
+function enableIgnoreCaughtExceptions() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+    is(gPrefs.ignoreCaughtExceptions, true,
+      "The ignore-caught-exceptions pref should now be enabled.");
+
+    ok(true, "Ignore caught exceptions was enabled.");
+    deferred.resolve();
+  });
+
+  gOptions._ignoreCaughtExceptionsItem.setAttribute("checked", "true");
+  gOptions._toggleIgnoreCaughtExceptions();
+
+  return deferred.promise;
+}
+
+function disablePauseOnExceptions() {
+  let deferred = promise.defer();
+
+  gDebugger.gThreadClient.addOneTimeListener("resumed", () => {
+    is(gPrefs.pauseOnExceptions, false,
+      "The pause-on-exceptions pref should now be disabled.");
+
+    ok(true, "Pausing on exceptions was disabled.");
+    deferred.resolve();
+  });
+
+  gOptions._pauseOnExceptionsItem.setAttribute("checked", "false");
+  gOptions._togglePauseOnExceptions();
+
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gPanel = null;
+  gDebugger = null;
+  gFrames = null;
+  gSources = null;
+  gPrefs = null;
+  gOptions = null;
+  DevToolsUtils.reportingDisabled = false;
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/code_math_bogus_map.min.js
@@ -0,0 +1,4 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+function stopMe(){throw Error("boom");}try{stopMe();var a=1;a=a*2;}catch(e){};
+//# sourceMappingURL=bogus.map
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_minified_bogus_map.html
@@ -0,0 +1,14 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8"/>
+    <title>Debugger test page</title>
+  </head>
+
+  <body>
+    <script src="code_math_bogus_map.min.js"></script>
+  </body>
+</html>
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -81,16 +81,18 @@ InspectorPanel.prototype = {
     }).then(defaultSelection => {
       return this._deferredOpen(defaultSelection);
     }).then(null, console.error);
   },
 
   _deferredOpen: function(defaultSelection) {
     let deferred = promise.defer();
 
+    this.outerHTMLEditable = this._target.client.traits.editOuterHTML;
+
     this.onNewRoot = this.onNewRoot.bind(this);
     this.walker.on("new-root", this.onNewRoot);
 
     this.nodemenu = this.panelDoc.getElementById("inspector-node-popup");
     this.lastNodemenuItem = this.nodemenu.lastChild;
     this._setupNodeMenu = this._setupNodeMenu.bind(this);
     this._resetNodeMenu = this._resetNodeMenu.bind(this);
     this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true);
@@ -588,25 +590,33 @@ InspectorPanel.prototype = {
       deleteNode.removeAttribute("disabled");
     }
 
     // Disable / enable "Copy Unique Selector", "Copy inner HTML" &
     // "Copy outer HTML" as appropriate
     let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
     let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
     let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
-    if (this.selection.isElementNode()) {
+    let selectionIsElement = this.selection.isElementNode();
+    if (selectionIsElement) {
       unique.removeAttribute("disabled");
       copyInnerHTML.removeAttribute("disabled");
       copyOuterHTML.removeAttribute("disabled");
     } else {
       unique.setAttribute("disabled", "true");
       copyInnerHTML.setAttribute("disabled", "true");
       copyOuterHTML.setAttribute("disabled", "true");
     }
+
+    let editHTML = this.panelDoc.getElementById("node-menu-edithtml");
+    if (this.outerHTMLEditable && selectionIsElement) {
+      editHTML.removeAttribute("disabled");
+    } else {
+      editHTML.setAttribute("disabled", "true");
+    }
   },
 
   _resetNodeMenu: function InspectorPanel_resetNodeMenu() {
     // Remove any extra items
     while (this.lastNodemenuItem.nextSibling) {
       let toDelete = this.lastNodemenuItem.nextSibling;
       toDelete.parentNode.removeChild(toDelete);
     }
@@ -701,16 +711,29 @@ InspectorPanel.prototype = {
       this.highlighter.hide();
     }
     else if (event.type == "mouseout") {
       this.highlighter.show();
     }
   },
 
   /**
+   * Edit the outerHTML of the selected Node.
+   */
+  editHTML: function InspectorPanel_editHTML()
+  {
+    if (!this.selection.isNode()) {
+      return;
+    }
+    if (this.markup) {
+      this.markup.beginEditingOuterHTML(this.selection.nodeFront);
+    }
+  },
+
+  /**
    * Copy the innerHTML of the selected Node to the clipboard.
    */
   copyInnerHTML: function InspectorPanel_copyInnerHTML()
   {
     if (!this.selection.isNode()) {
       return;
     }
     this._copyLongStr(this.walker.innerHTML(this.selection.nodeFront));
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -28,16 +28,20 @@
       key="&inspectorSearchHTML.key;"
       modifiers="accel"
       command="nodeSearchCommand"/>
   </keyset>
 
   <popupset id="inspectorPopupSet">
     <!-- Used by the Markup Panel, the Highlighter and the Breadcrumbs -->
     <menupopup id="inspector-node-popup">
+      <menuitem id="node-menu-edithtml"
+        label="&inspectorHTMLEdit.label;"
+        accesskey="&inspectorHTMLEdit.accesskey;"
+        oncommand="inspector.editHTML()"/>
       <menuitem id="node-menu-copyinner"
         label="&inspectorHTMLCopyInner.label;"
         accesskey="&inspectorHTMLCopyInner.accesskey;"
         oncommand="inspector.copyInnerHTML()"/>
       <menuitem id="node-menu-copyouter"
         label="&inspectorHTMLCopyOuter.label;"
         accesskey="&inspectorHTMLCopyOuter.accesskey;"
         oncommand="inspector.copyOuterHTML()"/>
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -32,16 +32,17 @@ browser.jar:
     content/browser/devtools/codemirror/codemirror.js                  (sourceeditor/codemirror/codemirror.js)
     content/browser/devtools/codemirror/codemirror.css                 (sourceeditor/codemirror/codemirror.css)
     content/browser/devtools/codemirror/javascript.js                  (sourceeditor/codemirror/javascript.js)
     content/browser/devtools/codemirror/xml.js                         (sourceeditor/codemirror/xml.js)
     content/browser/devtools/codemirror/css.js                         (sourceeditor/codemirror/css.js)
     content/browser/devtools/codemirror/htmlmixed.js                   (sourceeditor/codemirror/htmlmixed.js)
     content/browser/devtools/codemirror/activeline.js                  (sourceeditor/codemirror/activeline.js)
     content/browser/devtools/codemirror/matchbrackets.js               (sourceeditor/codemirror/matchbrackets.js)
+    content/browser/devtools/codemirror/closebrackets.js               (sourceeditor/codemirror/closebrackets.js)
     content/browser/devtools/codemirror/comment.js                     (sourceeditor/codemirror/comment.js)
     content/browser/devtools/codemirror/searchcursor.js                (sourceeditor/codemirror/search/searchcursor.js)
     content/browser/devtools/codemirror/search.js                      (sourceeditor/codemirror/search/search.js)
     content/browser/devtools/codemirror/dialog.js                      (sourceeditor/codemirror/dialog/dialog.js)
     content/browser/devtools/codemirror/dialog.css                     (sourceeditor/codemirror/dialog/dialog.css)
     content/browser/devtools/codemirror/mozilla.css                    (sourceeditor/codemirror/mozilla.css)
 *   content/browser/devtools/source-editor-overlay.xul                 (sourceeditor/source-editor-overlay.xul)
     content/browser/devtools/debugger.xul                              (debugger/debugger.xul)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/html-editor.js
@@ -0,0 +1,182 @@
+/* vim:set ts=2 sw=2 sts=2 et tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+ "use strict";
+
+const {Cu} = require("chrome");
+const Editor = require("devtools/sourceeditor/editor");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+
+exports.HTMLEditor = HTMLEditor;
+
+function ctrl(k) {
+  return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
+}
+function stopPropagation(e) {
+  e.stopPropagation();
+}
+/**
+ * A wrapper around the Editor component, that allows editing of HTML.
+ *
+ * The main functionality this provides around the Editor is the ability
+ * to show/hide/position an editor inplace. It only appends once to the
+ * body, and uses CSS to position the editor.  The reason it is done this
+ * way is that the editor is loaded in an iframe, and calling appendChild
+ * causes it to reload.
+ *
+ * Meant to be embedded inside of an HTML page, as in markup-view.xhtml.
+ *
+ * @param HTMLDocument htmlDocument
+ *        The document to attach the editor to.  Will also use this
+ *        document as a basis for listening resize events.
+ */
+function HTMLEditor(htmlDocument)
+{
+  this.doc = htmlDocument;
+  this.container = this.doc.createElement("div");
+  this.container.className = "html-editor theme-body";
+  this.container.style.display = "none";
+  this.editorInner = this.doc.createElement("div");
+  this.editorInner.className = "html-editor-inner";
+  this.container.appendChild(this.editorInner);
+
+  this.doc.body.appendChild(this.container);
+  this.hide = this.hide.bind(this);
+  this.refresh = this.refresh.bind(this);
+
+  EventEmitter.decorate(this);
+
+  this.doc.defaultView.addEventListener("resize",
+    this.refresh, true);
+
+  let config = {
+    mode: Editor.modes.html,
+    lineWrapping: true,
+    styleActiveLine: false,
+    extraKeys: {},
+    theme: "mozilla markup-view"
+  };
+
+  config.extraKeys[ctrl("Enter")] = this.hide;
+  config.extraKeys["Esc"] = this.hide.bind(this, false);
+
+  this.container.addEventListener("click", this.hide, false);
+  this.editorInner.addEventListener("click", stopPropagation, false);
+  this.editor = new Editor(config);
+
+  this.editor.appendTo(this.editorInner).then(() => {
+    this.hide(false);
+  }).then(null, (err) => console.log(err.message));
+}
+
+HTMLEditor.prototype = {
+
+  /**
+   * Need to refresh position by manually setting CSS values, so this will
+   * need to be called on resizes and other sizing changes.
+   */
+  refresh: function() {
+    let element = this._attachedElement;
+
+    if (element) {
+      this.container.style.top = element.offsetTop + "px";
+      this.container.style.left = element.offsetLeft + "px";
+      this.container.style.width = element.offsetWidth + "px";
+      this.container.style.height = element.parentNode.offsetHeight + "px";
+      this.editor.refresh();
+    }
+  },
+
+  /**
+   * Anchor the editor to a particular element.
+   *
+   * @param DOMNode element
+   *        The element that the editor will be anchored to.
+   *        Should belong to the HTMLDocument passed into the constructor.
+   */
+  _attach: function(element)
+  {
+    this._detach();
+    this._attachedElement = element;
+    element.classList.add("html-editor-container");
+    this.refresh();
+  },
+
+  /**
+   * Unanchor the editor from an element.
+   */
+  _detach: function()
+  {
+    if (this._attachedElement) {
+      this._attachedElement.classList.remove("html-editor-container");
+      this._attachedElement = undefined;
+    }
+  },
+
+  /**
+   * Anchor the editor to a particular element, and show the editor.
+   *
+   * @param DOMNode element
+   *        The element that the editor will be anchored to.
+   *        Should belong to the HTMLDocument passed into the constructor.
+   * @param string text
+   *        Value to set the contents of the editor to
+   * @param function cb
+   *        The function to call when hiding
+   */
+  show: function(element, text)
+  {
+    if (this._visible) {
+      return;
+    }
+
+    this._originalValue = text;
+    this.editor.setText(text);
+    this._attach(element);
+    this.container.style.display = "flex";
+    this._visible = true;
+
+    this.editor.refresh();
+    this.editor.focus();
+  },
+
+  /**
+   * Hide the editor, optionally committing the changes
+   *
+   * @param bool shouldCommit
+   *             A change will be committed by default.  If this param
+   *             strictly equals false, no change will occur.
+   */
+  hide: function(shouldCommit)
+  {
+    if (!this._visible) {
+      return;
+    }
+
+    this.container.style.display = "none";
+    this._detach();
+
+    let newValue = this.editor.getText();
+    let valueHasChanged = this._originalValue !== newValue;
+    let preventCommit = shouldCommit === false || !valueHasChanged;
+    this.emit("popup-hidden", !preventCommit, newValue);
+    this._originalValue = undefined;
+    this._visible = undefined;
+  },
+
+  /**
+   * Destroy this object and unbind all event handlers
+   */
+  destroy: function()
+  {
+    this.doc.defaultView.removeEventListener("resize",
+      this.refresh, true);
+    this.container.removeEventListener("click", this.hide, false);
+    this.editorInner.removeEventListener("click", stopPropagation, false);
+
+    this.hide(false);
+    this.container.parentNode.removeChild(this.container);
+  }
+};
\ No newline at end of file
--- a/browser/devtools/markupview/markup-view.css
+++ b/browser/devtools/markupview/markup-view.css
@@ -9,16 +9,42 @@
   float: left;
   min-width: 100%;
 }
 
 #root-wrapper:after {
    content: "";
    display: block;
    clear: both;
+   position:relative;
+}
+
+.html-editor {
+  display: none;
+  position: absolute;
+  z-index: 2;
+
+  /* Use the same margin/padding trick used by .child tags to ensure that
+   * the editor covers up any content to the left (including expander arrows
+   * and hover effects). */
+  margin-left: -1000em;
+  padding-left: 1000em;
+}
+
+.html-editor-inner {
+  border: solid .1px;
+  flex: 1 1 auto;
+}
+
+.html-editor iframe {
+  height: 100%;
+  width: 100%;
+  border: none;
+  margin: 0;
+  padding: 0;
 }
 
 .children {
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
@@ -31,16 +57,21 @@
 }
 
 .tag-line {
   min-height: 1.4em;
   line-height: 1.4em;
   position: relative;
 }
 
+.html-editor-container {
+  position: relative;
+  min-height: 200px;
+}
+
 /* This extra element placed in each tag is positioned absolutely to cover the
  * whole tag line and is used for background styling (when a selection is made
  * or when the tag is flashing) */
 .tag-line .highlighter {
   position: absolute;
   left: -1000em;
   right: 0;
   height: 100%;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -13,16 +13,17 @@ const DEFAULT_MAX_CHILDREN = 100;
 const COLLAPSE_ATTRIBUTE_LENGTH = 120;
 const COLLAPSE_DATA_URL_REGEX = /^data.+base64/;
 const COLLAPSE_DATA_URL_LENGTH = 60;
 const CONTAINER_FLASHING_DURATION = 500;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {HTMLEditor} = require("devtools/markupview/html-editor");
 const {OutputParser} = require("devtools/output-parser");
 const promise = require("sdk/core/promise");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
@@ -52,16 +53,17 @@ loader.lazyGetter(this, "AutocompletePop
  */
 function MarkupView(aInspector, aFrame, aControllerWindow) {
   this._inspector = aInspector;
   this.walker = this._inspector.walker;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
   this._outputParser = new OutputParser();
+  this.htmlEditor = new HTMLEditor(this.doc);
 
   this.layoutHelpers = new LayoutHelpers(this.doc.defaultView);
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
@@ -144,16 +146,17 @@ MarkupView.prototype = {
     // Recursively update each node starting with documentElement.
     updateChildren(documentElement);
   },
 
   /**
    * Highlight the inspector selected node.
    */
   _onNewSelection: function() {
+    this.htmlEditor.hide();
     let done = this._inspector.updating("markup-view");
     if (this._inspector.selection.isNode()) {
       this.showNode(this._inspector.selection.nodeFront, true).then(() => {
         this.markNodeAsSelected(this._inspector.selection.nodeFront);
         done();
       });
     } else {
       this.unmarkSelectedNode();
@@ -331,18 +334,18 @@ MarkupView.prototype = {
    *        If falsy, keyboard focus will be moved to the container too.
    */
   navigate: function(aContainer, aIgnoreFocus) {
     if (!aContainer) {
       return;
     }
 
     let node = aContainer.node;
-    this.markNodeAsSelected(node);
-    this._inspector.selection.setNodeFront(node, "treepanel");
+    this.markNodeAsSelected(node, "treepanel");
+
     // This event won't be fired if the node is the same. But the highlighter
     // need to lock the node if it wasn't.
     this._inspector.selection.emit("new-node");
     this._inspector.selection.emit("new-node-front");
 
     if (!aIgnoreFocus) {
       aContainer.focus();
     }
@@ -385,16 +388,19 @@ MarkupView.prototype = {
     return container;
   },
 
   /**
    * Mutation observer used for included nodes.
    */
   _mutationObserver: function(aMutations) {
     let requiresLayoutChange = false;
+    let reselectParent;
+    let reselectChildIndex;
+
     for (let mutation of aMutations) {
       let type = mutation.type;
       let target = mutation.target;
 
       if (mutation.type === "documentUnload") {
         // Treat this as a childList change of the child (maybe the protocol
         // should do this).
         type = "childList";
@@ -413,30 +419,61 @@ MarkupView.prototype = {
       if (type === "attributes" || type === "characterData") {
         container.update();
 
         // Auto refresh style properties on selected node when they change.
         if (type === "attributes" && container.selected) {
           requiresLayoutChange = true;
         }
       } else if (type === "childList") {
+        let isFromOuterHTML = mutation.removed.some((n) => {
+          return n === this._outerHTMLNode;
+        });
+
+        // Keep track of which node should be reselected after mutations.
+        if (isFromOuterHTML) {
+          reselectParent = target;
+          reselectChildIndex = this._outerHTMLChildIndex;
+
+          delete this._outerHTMLNode;
+          delete this._outerHTMLChildIndex;
+        }
+
         container.childrenDirty = true;
-        // Update the children to take care of changes in the DOM
-        // Passing true as the last parameter asks for mutation flashing of the
-        // new nodes
-        this._updateChildren(container, {flash: true});
+        // Update the children to take care of changes in the markup view DOM.
+        this._updateChildren(container, {flash: !isFromOuterHTML});
       }
     }
 
     if (requiresLayoutChange) {
       this._inspector.immediateLayoutChange();
     }
-    this._waitForChildren().then(() => {
+    this._waitForChildren().then((nodes) => {
       this._flashMutatedNodes(aMutations);
-      this._inspector.emit("markupmutation");
+      this._inspector.emit("markupmutation", aMutations);
+
+      // Since the htmlEditor is absolutely positioned, a mutation may change
+      // the location in which it should be shown.
+      this.htmlEditor.refresh();
+
+      // If a node has had its outerHTML set, the parent node will be selected.
+      // Reselect the original node immediately.
+      if (this._inspector.selection.nodeFront === reselectParent) {
+        this.walker.children(reselectParent).then((o) => {
+          let node = o.nodes[reselectChildIndex];
+          let container = this._containers.get(node);
+          if (node && container) {
+            this.markNodeAsSelected(node, "outerhtml");
+            if (container.hasChildren) {
+              this.expandNode(node);
+            }
+          }
+        });
+
+      }
     });
   },
 
   /**
    * Given a list of mutations returned by the mutation observer, flash the
    * corresponding containers to attract attention.
    */
   _flashMutatedNodes: function(aMutations) {
@@ -546,40 +583,131 @@ MarkupView.prototype = {
   /**
    * Collapse the node's children.
    */
   collapseNode: function(aNode) {
     let container = this._containers.get(aNode);
     container.expanded = false;
   },
 
+  /**
+   * Retrieve the outerHTML for a remote node.
+   * @param aNode The NodeFront to get the outerHTML for.
+   * @returns A promise that will be resolved with the outerHTML.
+   */
+  getNodeOuterHTML: function(aNode) {
+    let def = promise.defer();
+    this.walker.outerHTML(aNode).then(longstr => {
+      longstr.string().then(outerHTML => {
+        longstr.release().then(null, console.error);
+        def.resolve(outerHTML);
+      });
+    });
+    return def.promise;
+  },
+
+  /**
+   * Retrieve the index of a child within its parent's children list.
+   * @param aNode The NodeFront to find the index of.
+   * @returns A promise that will be resolved with the integer index.
+   *          If the child cannot be found, returns -1
+   */
+  getNodeChildIndex: function(aNode) {
+    let def = promise.defer();
+    let parentNode = aNode.parentNode();
+
+    // Node may have been removed from the DOM, instead of throwing an error,
+    // return -1 indicating that it isn't inside of its parent children list.
+    if (!parentNode) {
+      def.resolve(-1);
+    } else {
+      this.walker.children(parentNode).then(children => {
+        def.resolve(children.nodes.indexOf(aNode));
+      });
+    }
+
+    return def.promise;
+  },
+
+  /**
+   * Retrieve the index of a child within its parent's children collection.
+   * @param aNode The NodeFront to find the index of.
+   * @param newValue The new outerHTML to set on the node.
+   * @param oldValue The old outerHTML that will be reverted to find the index of.
+   * @returns A promise that will be resolved with the integer index.
+   *          If the child cannot be found, returns -1
+   */
+  updateNodeOuterHTML: function(aNode, newValue, oldValue) {
+    let container = this._containers.get(aNode);
+    if (!container) {
+      return;
+    }
+
+    this.getNodeChildIndex(aNode).then((i) => {
+      this._outerHTMLChildIndex = i;
+      this._outerHTMLNode = aNode;
+
+      container.undo.do(() => {
+        this.walker.setOuterHTML(aNode, newValue);
+      }, () => {
+        this.walker.setOuterHTML(aNode, oldValue);
+      });
+    });
+  },
+
+  /**
+   * Open an editor in the UI to allow editing of a node's outerHTML.
+   * @param aNode The NodeFront to edit.
+   */
+  beginEditingOuterHTML: function(aNode) {
+    this.getNodeOuterHTML(aNode).then((oldValue)=> {
+      let container = this._containers.get(aNode);
+      if (!container) {
+        return;
+      }
+      this.htmlEditor.show(container.tagLine, oldValue);
+      this.htmlEditor.once("popup-hidden", (e, aCommit, aValue) => {
+        if (aCommit) {
+          this.updateNodeOuterHTML(aNode, aValue, oldValue);
+        }
+      });
+    });
+  },
+
+  /**
+   * Mark the given node expanded.
+   * @param aNode The NodeFront to mark as expanded.
+   */
   setNodeExpanded: function(aNode, aExpanded) {
     if (aExpanded) {
       this.expandNode(aNode);
     } else {
       this.collapseNode(aNode);
     }
   },
 
   /**
-   * Mark the given node selected.
+   * Mark the given node selected, and update the inspector.selection
+   * object's NodeFront to keep consistent state between UI and selection.
+   * @param aNode The NodeFront to mark as selected.
    */
-  markNodeAsSelected: function(aNode) {
+  markNodeAsSelected: function(aNode, reason) {
     let container = this._containers.get(aNode);
     if (this._selectedContainer === container) {
       return false;
     }
     if (this._selectedContainer) {
       this._selectedContainer.selected = false;
     }
     this._selectedContainer = container;
     if (aNode) {
       this._selectedContainer.selected = true;
     }
 
+    this._inspector.selection.setNodeFront(aNode, reason || "nodeselected");
     return true;
   },
 
   /**
    * Make sure that every ancestor of the selection are updated
    * and included in the list of visible children.
    */
   _ensureVisible: function(node) {
@@ -774,16 +902,19 @@ MarkupView.prototype = {
   },
 
   /**
    * Tear down the markup panel.
    */
   destroy: function() {
     gDevTools.off("pref-changed", this._handlePrefChange);
 
+    this.htmlEditor.destroy();
+    delete this.htmlEditor;
+
     this.undo.destroy();
     delete this.undo;
 
     this.popup.destroy();
     delete this.popup;
 
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -6,17 +6,17 @@
 
 <html xmlns="http://www.w3.org/1999/xhtml">
 <head>
   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
   <link rel="stylesheet" href="chrome://browser/content/devtools/markup-view.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
 
-  <script type="application/javascript;version=1.8" src="theme-switching.js"/>
+  <script type="application/javascript;version=1.8" src="chrome://browser/content/devtools/theme-switching.js"></script>
 
 </head>
 <body class="theme-body devtools-monospace" role="application">
   <div id="root-wrapper">
     <div id="root"></div>
   </div>
   <div id="templates" style="display:none">
 
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 support-files = head.js
 
 [browser_bug896181_css_mixed_completion_new_attribute.js]
 # Bug 916763 - too many intermittent failures
 skip-if = true
 [browser_inspector_markup_edit.html]
 [browser_inspector_markup_edit.js]
+[browser_inspector_markup_edit_outerhtml.js]
 [browser_inspector_markup_mutation.html]
 [browser_inspector_markup_mutation.js]
 [browser_inspector_markup_mutation_flashing.html]
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.html]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.html]
 [browser_inspector_markup_subset.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit_outerhtml.js
@@ -0,0 +1,295 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+  let inspector;
+  let doc;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+
+  let outerHTMLs = [
+    {
+      selector: "#one",
+      oldHTML: '<div id="one">First <em>Div</em></div>',
+      newHTML: '<div id="one">First Div</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode.textContent, "First Div", "New div has expected text content");
+        ok (!doc.querySelector("#one em"), "No em remaining")
+      }
+    },
+    {
+      selector: "#removedChildren",
+      oldHTML: '<div id="removedChildren">removedChild <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>',
+      newHTML: '<div id="removedChildren">removedChild</div>'
+    },
+    {
+      selector: "#addedChildren",
+      oldHTML: '<div id="addedChildren">addedChildren</div>',
+      newHTML: '<div id="addedChildren">addedChildren <i>Italic <b>Bold <u>Underline</u></b></i> Normal</div>'
+    },
+    {
+      selector: "#addedAttribute",
+      oldHTML: '<div id="addedAttribute">addedAttribute</div>',
+      newHTML: '<div id="addedAttribute" class="important" disabled checked>addedAttribute</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+        is (pageNode.outerHTML, '<div id="addedAttribute" class="important" disabled="" checked="">addedAttribute</div>',
+              "Attributes have been added");
+      }
+    },
+    {
+      selector: "#changedTag",
+      oldHTML: '<div id="changedTag">changedTag</div>',
+      newHTML: '<p id="changedTag" class="important">changedTag</p>'
+    },
+    {
+      selector: "#badMarkup1",
+      oldHTML: '<div id="badMarkup1">badMarkup1</div>',
+      newHTML: '<div id="badMarkup1">badMarkup1</div> hanging</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let textNode = pageNode.nextSibling;
+
+        is (textNode.nodeName, "#text", "Sibling is a text element");
+        is (textNode.data, " hanging", "New text node has expected text content");
+      }
+    },
+    {
+      selector: "#badMarkup2",
+      oldHTML: '<div id="badMarkup2">badMarkup2</div>',
+      newHTML: '<div id="badMarkup2">badMarkup2</div> hanging<div></div></div></div></body>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let textNode = pageNode.nextSibling;
+
+        is (textNode.nodeName, "#text", "Sibling is a text element");
+        is (textNode.data, " hanging", "New text node has expected text content");
+      }
+    },
+    {
+      selector: "#badMarkup3",
+      oldHTML: '<div id="badMarkup3">badMarkup3</div>',
+      newHTML: '<div id="badMarkup3">badMarkup3 <em>Emphasized <strong> and strong</div>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let em = doc.querySelector("#badMarkup3 em");
+        let strong = doc.querySelector("#badMarkup3 strong");
+
+        is (em.textContent, "Emphasized  and strong", "<em> was auto created");
+        is (strong.textContent, " and strong", "<strong> was auto created");
+      }
+    },
+    {
+      selector: "#badMarkup4",
+      oldHTML: '<div id="badMarkup4">badMarkup4</div>',
+      newHTML: '<div id="badMarkup4">badMarkup4</p>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let div = doc.querySelector("#badMarkup4");
+        let p = doc.querySelector("#badMarkup4 p");
+
+        is (div.textContent, "badMarkup4", "textContent is correct");
+        is (div.tagName, "DIV", "did not change to <p> tag");
+        is (p.textContent, "", "The <p> tag has no children");
+        is (p.tagName, "P", "Created an empty <p> tag");
+      }
+    },
+    {
+      selector: "#badMarkup5",
+      oldHTML: '<p id="badMarkup5">badMarkup5</p>',
+      newHTML: '<p id="badMarkup5">badMarkup5 <div>with a nested div</div></p>',
+      validate: function(pageNode, selectedNode) {
+        is (pageNode, selectedNode, "Original element is selected");
+
+        let p = doc.querySelector("#badMarkup5");
+        let nodiv = doc.querySelector("#badMarkup5 div");
+        let div = doc.querySelector("#badMarkup5 ~ div");
+
+        ok (!nodiv, "The invalid markup got created as a sibling");
+        is (p.textContent, "badMarkup5 ", "The <p> tag does not take in the <div> content");
+        is (p.tagName, "P", "Did not change to a <div> tag");
+        is (div.textContent, "with a nested div", "textContent is correct");
+        is (div.tagName, "DIV", "Did not change to <p> tag");
+      }
+    },
+    {
+      selector: "#siblings",
+      oldHTML: '<div id="siblings">siblings</div>',
+      newHTML: '<div id="siblings-before-sibling">before sibling</div>' +
+               '<div id="siblings">siblings (updated)</div>' +
+               '<div id="siblings-after-sibling">after sibling</div>',
+      validate: function(pageNode, selectedNode) {
+        let beforeSiblingNode = doc.querySelector("#siblings-before-sibling");
+        let afterSiblingNode = doc.querySelector("#siblings-after-sibling");
+
+        is (beforeSiblingNode, selectedNode, "Sibling has been selected");
+        is (pageNode.textContent, "siblings (updated)", "New div has expected text content");
+        is (beforeSiblingNode.textContent, "before sibling", "Sibling has been inserted");
+        is (afterSiblingNode.textContent, "after sibling", "Sibling has been inserted");
+      }
+    }
+  ];
+  content.location = "data:text/html," +
+    "<!DOCTYPE html>" +
+    "<head><meta charset='utf-8' /></head>" +
+    "<body>" +
+    [outer.oldHTML for (outer of outerHTMLs) ].join("\n") +
+    "</body>" +
+    "</html>";
+
+  function setupTest() {
+    var target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+      inspector = toolbox.getCurrentPanel();
+      inspector.once("inspector-updated", startTests);
+    });
+  }
+
+  function startTests() {
+    inspector.markup._frame.focus();
+    nextStep(0);
+  }
+
+  function nextStep(cursor) {
+    if (cursor >= outerHTMLs.length) {
+      testBody();
+      return;
+    }
+
+    let currentTestData = outerHTMLs[cursor];
+    let selector = currentTestData.selector;
+    let oldHTML = currentTestData.oldHTML;
+    let newHTML = currentTestData.newHTML;
+    let rawNode = doc.querySelector(selector);
+
+    inspector.selection.once("new-node", () => {
+
+      let oldNodeFront = inspector.selection.nodeFront;
+
+      // markupmutation fires once the outerHTML is set, with a target
+      // as the parent node and a type of "childList".
+      inspector.once("markupmutation", (e, aMutations) => {
+
+        // Check to make the sure the correct mutation has fired, and that the
+        // parent is selected (this will be reset to the child once the mutation is complete.
+        let node = inspector.selection.node;
+        let nodeFront = inspector.selection.nodeFront;
+        let mutation = aMutations[0];
+        let isFromOuterHTML = mutation.removed.some((n) => {
+          return n === oldNodeFront;
+        });
+
+        ok (isFromOuterHTML, "The node is in the 'removed' list of the mutation");
+        is (mutation.type, "childList", "Mutation is a childList after updating outerHTML");
+        is (mutation.target, nodeFront, "Parent node is selected immediately after setting outerHTML");
+
+        // Wait for node to be reselected after outerHTML has been set
+        inspector.selection.once("new-node", () => {
+
+          // Typically selectedNode will === pageNode, but if a new element has been injected in front
+          // of it, this will not be the case.  If this happens.
+          let selectedNode = inspector.selection.node;
+          let nodeFront = inspector.selection.nodeFront;
+          let pageNode = doc.querySelector(selector);
+
+          if (currentTestData.validate) {
+            currentTestData.validate(pageNode, selectedNode);
+          } else {
+            is (pageNode, selectedNode, "Original node (grabbed by selector) is selected");
+            is (pageNode.outerHTML, newHTML, "Outer HTML has been updated");
+          }
+
+          nextStep(cursor + 1);
+        });
+
+      });
+
+      is (inspector.selection.node, rawNode, "Selection is on the correct node");
+      inspector.markup.updateNodeOuterHTML(inspector.selection.nodeFront, newHTML, oldHTML);
+    });
+
+    inspector.selection.setNode(rawNode);
+  }
+
+  function testBody() {
+    let body = doc.querySelector("body");
+    let bodyHTML = '<body id="updated"><p></p></body>';
+    let bodyFront = inspector.markup.walker.frontForRawNode(body);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.querySelector("body").outerHTML, bodyHTML, "<body> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      testHead();
+    });
+    inspector.markup.updateNodeOuterHTML(bodyFront, bodyHTML, body.outerHTML);
+  }
+
+  function testHead() {
+    let head = doc.querySelector("head");
+    let headHTML = '<head id="updated"><title>New Title</title><script>window.foo="bar";</script></head>';
+    let headFront = inspector.markup.walker.frontForRawNode(head);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "New Title", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.querySelector("head").outerHTML, headHTML, "<head> HTML has been updated");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      testDocumentElement();
+    });
+    inspector.markup.updateNodeOuterHTML(headFront, headHTML, head.outerHTML);
+  }
+
+  function testDocumentElement() {
+    let docElement = doc.documentElement;
+    let docElementHTML = '<html id="updated" foo="bar"><head><title>Updated from document element</title><script>window.foo="bar";</script></head><body><p>Hello</p></body></html>';
+    let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "Updated from document element", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.documentElement.id, "updated", "<html> ID has been updated");
+      is (doc.documentElement.className, "", "<html> class has been updated");
+      is (doc.documentElement.getAttribute("foo"), "bar", "<html> attribute has been updated");
+      is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      is (doc.body.textContent, "Hello", "document.body.textContent has been updated");
+      testDocumentElement2();
+    });
+    inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
+  }
+
+  function testDocumentElement2() {
+    let docElement = doc.documentElement;
+    let docElementHTML = '<html class="updated" id="somethingelse"><head><title>Updated again from document element</title><script>window.foo="bar";</script></head><body><p>Hello again</p></body></html>';
+    let docElementFront = inspector.markup.walker.frontForRawNode(docElement);
+    inspector.once("markupmutation", (e, aMutations) => {
+      is (doc.title, "Updated again from document element", "New title has been added");
+      is (doc.defaultView.foo, undefined, "Script has not been executed");
+      is (doc.documentElement.id, "somethingelse", "<html> ID has been updated");
+      is (doc.documentElement.className, "updated", "<html> class has been updated");
+      is (doc.documentElement.getAttribute("foo"), null, "<html> attribute has been removed");
+      is (doc.documentElement.outerHTML, docElementHTML, "<html> HTML has been updated");
+      is (doc.querySelectorAll("head").length, 1, "no extra <head>s have been added");
+      is (doc.querySelectorAll("body").length, 1, "no extra <body>s have been added");
+      is (doc.body.textContent, "Hello again", "document.body.textContent has been updated");
+      finishUp();
+    });
+    inspector.markup.updateNodeOuterHTML(docElementFront, docElementHTML, docElement.outerHTML);
+  }
+
+  function finishUp() {
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const Cu = Components.utils;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let TargetFactory = devtools.TargetFactory;
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 
 // Clear preferences that may be set during the course of tests.
 function clearUserPrefs() {
   Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
   Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
 }
 
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -56,23 +56,25 @@ const EVENTS = {
 
   // When the response body is displayed in the UI.
   RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable"
 }
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
-Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const Editor = require("devtools/sourceeditor/editor");
+
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
   "resource://gre/modules/devtools/Loader.jsm");
 
 Object.defineProperty(this, "NetworkHelper", {
   get: function() {
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -24,32 +24,32 @@ const DEFAULT_HTTP_VERSION = "HTTP/1.1";
 const HEADERS_SIZE_DECIMALS = 3;
 const CONTENT_SIZE_DECIMALS = 2;
 const CONTENT_MIME_TYPE_ABBREVIATIONS = {
   "ecmascript": "js",
   "javascript": "js",
   "x-javascript": "js"
 };
 const CONTENT_MIME_TYPE_MAPPINGS = {
-  "/ecmascript": SourceEditor.MODES.JAVASCRIPT,
-  "/javascript": SourceEditor.MODES.JAVASCRIPT,
-  "/x-javascript": SourceEditor.MODES.JAVASCRIPT,
-  "/html": SourceEditor.MODES.HTML,
-  "/xhtml": SourceEditor.MODES.HTML,
-  "/xml": SourceEditor.MODES.HTML,
-  "/atom": SourceEditor.MODES.HTML,
-  "/soap": SourceEditor.MODES.HTML,
-  "/rdf": SourceEditor.MODES.HTML,
-  "/rss": SourceEditor.MODES.HTML,
-  "/css": SourceEditor.MODES.CSS
+  "/ecmascript": Editor.modes.js,
+  "/javascript": Editor.modes.js,
+  "/x-javascript": Editor.modes.js,
+  "/html": Editor.modes.html,
+  "/xhtml": Editor.modes.html,
+  "/xml": Editor.modes.html,
+  "/atom": Editor.modes.html,
+  "/soap": Editor.modes.html,
+  "/rdf": Editor.modes.css,
+  "/rss": Editor.modes.css,
+  "/css": Editor.modes.css
 };
 const DEFAULT_EDITOR_CONFIG = {
-  mode: SourceEditor.MODES.TEXT,
+  mode: Editor.modes.text,
   readOnly: true,
-  showLineNumbers: true
+  lineNumbers: true
 };
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   lazyEmpty: true,
   lazyEmptyDelay: 10, // ms
   searchEnabled: true,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChage: true,
@@ -151,17 +151,17 @@ let NetMonitorView = {
     }
 
     if (aTabIndex !== undefined) {
       $("#event-details-pane").selectedIndex = aTabIndex;
     }
   },
 
   /**
-   * Lazily initializes and returns a promise for a SourceEditor instance.
+   * Lazily initializes and returns a promise for a Editor instance.
    *
    * @param string aId
    *        The id of the editor placeholder node.
    * @return object
    *         A promise that is resolved when the editor is available.
    */
   editor: function(aId) {
     dumpn("Getting a NetMonitorView editor: " + aId);
@@ -170,17 +170,18 @@ let NetMonitorView = {
       return this._editorPromises.get(aId);
     }
 
     let deferred = promise.defer();
     this._editorPromises.set(aId, deferred.promise);
 
     // Initialize the source editor and store the newly created instance
     // in the ether of a resolved promise's value.
-    new SourceEditor().init($(aId), DEFAULT_EDITOR_CONFIG, deferred.resolve);
+    let editor = new Editor(DEFAULT_EDITOR_CONFIG);
+    editor.appendTo($(aId)).then(() => deferred.resolve(editor));
 
     return deferred.promise;
   },
 
   _body: null,
   _detailsPane: null,
   _detailsPaneToggleButton: null,
   _collapsePaneString: "",
@@ -1895,17 +1896,17 @@ NetworkDetailsView.prototype = {
             label: jsonScopeName,
             rawObject: jsonObject,
           });
         }
         // Malformed JSON.
         else {
           $("#response-content-textarea-box").hidden = false;
           NetMonitorView.editor("#response-content-textarea").then(aEditor => {
-            aEditor.setMode(SourceEditor.MODES.JAVASCRIPT);
+            aEditor.setMode(Editor.modes.js);
             aEditor.setText(aString);
           });
           let infoHeader = $("#response-content-info-header");
           infoHeader.setAttribute("value", parsingError);
           infoHeader.setAttribute("tooltiptext", parsingError);
           infoHeader.hidden = false;
         }
       }
@@ -1932,17 +1933,17 @@ NetworkDetailsView.prototype = {
           let dimensions = (width - 2) + " x " + (height - 2);
           $("#response-content-image-dimensions-value").setAttribute("value", dimensions);
         };
       }
       // Handle anything else.
       else {
         $("#response-content-textarea-box").hidden = false;
         NetMonitorView.editor("#response-content-textarea").then(aEditor => {
-          aEditor.setMode(SourceEditor.MODES.TEXT);
+          aEditor.setMode(Editor.modes.text);
           aEditor.setText(aString);
 
           // Maybe set a more appropriate mode in the Source Editor if possible,
           // but avoid doing this for very large files.
           if (aString.length < SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
             for (let key in CONTENT_MIME_TYPE_MAPPINGS) {
               if (mimeType.contains(key)) {
                 aEditor.setMode(CONTENT_MIME_TYPE_MAPPINGS[key]);
--- a/browser/devtools/netmonitor/test/browser_net_content-type.js
+++ b/browser/devtools/netmonitor/test/browser_net_content-type.js
@@ -4,17 +4,17 @@
 /**
  * Tests if different response content types are handled correctly.
  */
 
 function test() {
   initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 6).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=xml", {
           status: 200,
@@ -125,37 +125,37 @@ function test() {
 
         switch (aType) {
           case "xml": {
             checkVisibility("textarea");
 
             return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
               is(aEditor.getText(), "<label value='greeting'>Hello XML!</label>",
                 "The text shown in the source editor is incorrect for the xml request.");
-              is(aEditor.getMode(), SourceEditor.MODES.HTML,
+              is(aEditor.getMode(), Editor.modes.html,
                 "The mode active in the source editor is incorrect for the xml request.");
             });
           }
           case "css": {
             checkVisibility("textarea");
 
             return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
               is(aEditor.getText(), "body:pre { content: 'Hello CSS!' }",
                 "The text shown in the source editor is incorrect for the xml request.");
-              is(aEditor.getMode(), SourceEditor.MODES.CSS,
+              is(aEditor.getMode(), Editor.modes.css,
                 "The mode active in the source editor is incorrect for the xml request.");
             });
           }
           case "js": {
             checkVisibility("textarea");
 
             return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
               is(aEditor.getText(), "function() { return 'Hello JS!'; }",
                 "The text shown in the source editor is incorrect for the xml request.");
-              is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+              is(aEditor.getMode(), Editor.modes.js,
                 "The mode active in the source editor is incorrect for the xml request.");
             });
           }
           case "json": {
             checkVisibility("json");
 
             is(tabpanel.querySelectorAll(".variables-view-scope").length, 1,
               "There should be 1 json scope displayed in this tabpanel.");
@@ -183,17 +183,17 @@ function test() {
             return promise.resolve();
           }
           case "html": {
             checkVisibility("textarea");
 
             return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
               is(aEditor.getText(), "<blink>Not Found</blink>",
                 "The text shown in the source editor is incorrect for the xml request.");
-              is(aEditor.getMode(), SourceEditor.MODES.HTML,
+              is(aEditor.getMode(), Editor.modes.html,
                 "The mode active in the source editor is incorrect for the xml request.");
             });
           }
           case "png": {
             checkVisibility("image");
 
             let imageNode = tabpanel.querySelector("#response-content-image");
             let deferred = promise.defer();
--- a/browser/devtools/netmonitor/test/browser_net_cyrillic-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic-01.js
@@ -4,17 +4,17 @@
 /**
  * Tests if cyrillic text is rendered correctly in the source editor.
  */
 
 function test() {
   initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=txt", {
           status: 200,
@@ -24,17 +24,17 @@ function test() {
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.querySelectorAll("#details-pane tab")[3]);
 
       NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
         is(aEditor.getText().indexOf("\u044F"), 26, // я
           "The text shown in the source editor is incorrect.");
-        is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+        is(aEditor.getMode(), Editor.modes.text,
           "The mode active in the source editor is incorrect.");
 
         teardown(aMonitor).then(finish);
       });
     });
 
     aDebuggee.performRequests();
   });
--- a/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
+++ b/browser/devtools/netmonitor/test/browser_net_cyrillic-02.js
@@ -5,17 +5,17 @@
  * Tests if cyrillic text is rendered correctly in the source editor
  * when loaded directly from an HTML page.
  */
 
 function test() {
   initNetMonitor(CYRILLIC_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CYRILLIC_URL, {
           status: 200,
@@ -25,17 +25,17 @@ function test() {
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.querySelectorAll("#details-pane tab")[3]);
 
       NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
         is(aEditor.getText().indexOf("\u044F"), 302, // я
           "The text shown in the source editor is incorrect.");
-        is(aEditor.getMode(), SourceEditor.MODES.HTML,
+        is(aEditor.getMode(), Editor.modes.html,
           "The mode active in the source editor is incorrect.");
 
         teardown(aMonitor).then(finish);
       });
     });
 
     aDebuggee.location.reload();
   });
--- a/browser/devtools/netmonitor/test/browser_net_json-long.js
+++ b/browser/devtools/netmonitor/test/browser_net_json-long.js
@@ -8,17 +8,17 @@
 function test() {
   initNetMonitor(JSON_LONG_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     // This is receiving over 80 KB of json and will populate over 6000 items
     // in a variables view instance. Debug builds are slow.
     requestLongerTimeout(4);
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=json-long", {
           status: 200,
--- a/browser/devtools/netmonitor/test/browser_net_json-malformed.js
+++ b/browser/devtools/netmonitor/test/browser_net_json-malformed.js
@@ -4,17 +4,17 @@
 /**
  * Tests if malformed JSON responses are handled correctly.
  */
 
 function test() {
   initNetMonitor(JSON_MALFORMED_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=json-malformed", {
           status: 200,
@@ -54,17 +54,17 @@ function test() {
         "The response content textarea box doesn't have the intended visibility.");
       is(tabpanel.querySelector("#response-content-image-box")
         .hasAttribute("hidden"), true,
         "The response content image box doesn't have the intended visibility.");
 
       NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
         is(aEditor.getText(), "{ \"greeting\": \"Hello malformed JSON!\" },",
           "The text shown in the source editor is incorrect.");
-        is(aEditor.getMode(), SourceEditor.MODES.JAVASCRIPT,
+        is(aEditor.getMode(), Editor.modes.js,
           "The mode active in the source editor is incorrect.");
 
         teardown(aMonitor).then(finish);
       });
     });
 
     aDebuggee.performRequests();
   });
--- a/browser/devtools/netmonitor/test/browser_net_json_custom_mime.js
+++ b/browser/devtools/netmonitor/test/browser_net_json_custom_mime.js
@@ -4,17 +4,17 @@
 /**
  * Tests if JSON responses with unusal/custom MIME types are handled correctly.
  */
 
 function test() {
   initNetMonitor(JSON_CUSTOM_MIME_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=json-custom-mime", {
           status: 200,
--- a/browser/devtools/netmonitor/test/browser_net_jsonp.js
+++ b/browser/devtools/netmonitor/test/browser_net_jsonp.js
@@ -4,17 +4,17 @@
 /**
  * Tests if JSONP responses are handled correctly.
  */
 
 function test() {
   initNetMonitor(JSONP_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=jsonp&jsonp=$_0123Fun", {
           status: 200,
--- a/browser/devtools/netmonitor/test/browser_net_large-response.js
+++ b/browser/devtools/netmonitor/test/browser_net_large-response.js
@@ -8,33 +8,33 @@
 function test() {
   initNetMonitor(CUSTOM_GET_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     // This test could potentially be slow because over 100 KB of stuff
     // is going to be requested and displayed in the source editor.
     requestLongerTimeout(2);
 
-    let { document, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "GET", CONTENT_TYPE_SJS + "?fmt=html-long", {
           status: 200,
           statusText: "OK"
         });
 
       aMonitor.panelWin.once(aMonitor.panelWin.EVENTS.RESPONSE_BODY_DISPLAYED, () => {
         NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
           ok(aEditor.getText().match(/^<p>/),
             "The text shown in the source editor is incorrect.");
-          is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+          is(aEditor.getMode(), Editor.modes.text,
             "The mode active in the source editor is incorrect.");
 
           teardown(aMonitor).then(finish);
         });
       });
 
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
--- a/browser/devtools/netmonitor/test/browser_net_post-data-01.js
+++ b/browser/devtools/netmonitor/test/browser_net_post-data-01.js
@@ -4,17 +4,17 @@
 /**
  * Tests if the POST requests display the correct information in the UI.
  */
 
 function test() {
   initNetMonitor(POST_DATA_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu, NetworkDetails } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
     NetworkDetails._params.lazyEmpty = false;
 
     waitForNetworkEvents(aMonitor, 0, 2).then(() => {
       verifyRequestItemTarget(RequestsMenu.getItemAtIndex(0),
         "POST", SIMPLE_SJS + "?foo=bar&baz=42&type=urlencoded", {
@@ -136,17 +136,17 @@ function test() {
             ok(aEditor.getText().contains("Content-Disposition: form-data; name=\"Custom field\""),
               "The text shown in the source editor is incorrect (4.1).");
             ok(aEditor.getText().contains("Some text..."),
               "The text shown in the source editor is incorrect (2.2).");
             ok(aEditor.getText().contains("42"),
               "The text shown in the source editor is incorrect (3.2).");
             ok(aEditor.getText().contains("Extra data"),
               "The text shown in the source editor is incorrect (4.2).");
-            is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+            is(aEditor.getMode(), Editor.modes.text,
               "The mode active in the source editor is incorrect.");
           });
         }
       }
     });
 
     aDebuggee.performRequests();
   });
--- a/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
+++ b/browser/devtools/netmonitor/test/browser_net_simple-request-details.js
@@ -4,17 +4,17 @@
 /**
  * Tests if requests render correct information in the details UI.
  */
 
 function test() {
   initNetMonitor(SIMPLE_SJS).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { document, L10N, SourceEditor, NetMonitorView } = aMonitor.panelWin;
+    let { document, L10N, Editor, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu, NetworkDetails } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 1).then(() => {
       is(RequestsMenu.selectedItem, null,
         "There shouldn't be any selected item in the requests menu.");
       is(RequestsMenu.itemCount, 1,
@@ -189,17 +189,17 @@ function test() {
         "The response content textarea box should not be hidden.");
       is(tabpanel.querySelector("#response-content-image-box")
         .hasAttribute("hidden"), true,
         "The response content image box should be hidden.");
 
       return NetMonitorView.editor("#response-content-textarea").then((aEditor) => {
         is(aEditor.getText(), "Hello world!",
           "The text shown in the source editor is incorrect.");
-        is(aEditor.getMode(), SourceEditor.MODES.TEXT,
+        is(aEditor.getMode(), Editor.modes.text,
           "The mode active in the source editor is incorrect.");
       });
     }
 
     function testTimingsTab() {
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.querySelectorAll("#details-pane tab")[4]);
 
--- a/browser/devtools/sourceeditor/codemirror/README
+++ b/browser/devtools/sourceeditor/codemirror/README
@@ -35,16 +35,17 @@ in the LICENSE file:
 
  * codemirror.css
  * codemirror.js
  * comment.js
  * dialog/dialog.css
  * dialog/dialog.js
  * javascript.js
  * matchbrackets.js
+ * closebrackets.js
  * search/match-highlighter.js
  * search/search.js
  * search/searchcursor.js
  * test/codemirror.html
  * test/cm_comment_test.js
  * test/cm_driver.js
  * test/cm_mode_javascript_test.js
  * test/cm_mode_test.css
@@ -52,9 +53,9 @@ in the LICENSE file:
  * test/cm_test.js
 
 # Footnotes
 
 [1] http://codemirror.net
 [2] browser/devtools/sourceeditor/codemirror
 [3] browser/devtools/sourceeditor/test/browser_codemirror.js
 [4] browser/devtools/jar.mn
-[5] browser/devtools/sourceeditor/editor.js
\ No newline at end of file
+[5] browser/devtools/sourceeditor/editor.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/sourceeditor/codemirror/closebrackets.js
@@ -0,0 +1,82 @@
+(function() {
+  var DEFAULT_BRACKETS = "()[]{}''\"\"";
+  var DEFAULT_EXPLODE_ON_ENTER = "[]{}";
+  var SPACE_CHAR_REGEX = /\s/;
+
+  CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+    if (old != CodeMirror.Init && old)
+      cm.removeKeyMap("autoCloseBrackets");
+    if (!val) return;
+    var pairs = DEFAULT_BRACKETS, explode = DEFAULT_EXPLODE_ON_ENTER;
+    if (typeof val == "string") pairs = val;
+    else if (typeof val == "object") {
+      if (val.pairs != null) pairs = val.pairs;
+      if (val.explode != null) explode = val.explode;
+    }
+    var map = buildKeymap(pairs);
+    if (explode) map.Enter = buildExplodeHandler(explode);
+    cm.addKeyMap(map);
+  });
+
+  function charsAround(cm, pos) {
+    var str = cm.getRange(CodeMirror.Pos(pos.line, pos.ch - 1),
+                          CodeMirror.Pos(pos.line, pos.ch + 1));
+    return str.length == 2 ? str : null;
+  }
+
+  function buildKeymap(pairs) {
+    var map = {
+      name : "autoCloseBrackets",
+      Backspace: function(cm) {
+        if (cm.somethingSelected()) return CodeMirror.Pass;
+        var cur = cm.getCursor(), around = charsAround(cm, cur);
+        if (around && pairs.indexOf(around) % 2 == 0)
+          cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1));
+        else
+          return CodeMirror.Pass;
+      }
+    };
+    var closingBrackets = "";
+    for (var i = 0; i < pairs.length; i += 2) (function(left, right) {
+      if (left != right) closingBrackets += right;
+      function surround(cm) {
+        var selection = cm.getSelection();
+        cm.replaceSelection(left + selection + right);
+      }
+      function maybeOverwrite(cm) {
+        var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1));
+        if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass;
+        else cm.execCommand("goCharRight");
+      }
+      map["'" + left + "'"] = function(cm) {
+        if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment")
+          return CodeMirror.Pass;
+        if (cm.somethingSelected()) return surround(cm);
+        if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return;
+        var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1);
+        var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : "";
+        if (left == right && CodeMirror.isWordChar(curChar))
+          return CodeMirror.Pass;
+        if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar))
+          cm.replaceSelection(left + right, {head: ahead, anchor: ahead});
+        else
+          return CodeMirror.Pass;
+      };
+      if (left != right) map["'" + right + "'"] = maybeOverwrite;
+    })(pairs.charAt(i), pairs.charAt(i + 1));
+    return map;
+  }
+
+  function buildExplodeHandler(pairs) {
+    return function(cm) {
+      var cur = cm.getCursor(), around = charsAround(cm, cur);
+      if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+      cm.operation(function() {
+        var newPos = CodeMirror.Pos(cur.line + 1, 0);
+        cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input");
+        cm.indentLine(cur.line + 1, null, true);
+        cm.indentLine(cur.line + 2, null, true);
+      });
+    };
+  }
+})();
--- a/browser/devtools/sourceeditor/codemirror/mozilla.css
+++ b/browser/devtools/sourceeditor/codemirror/mozilla.css
@@ -18,13 +18,9 @@
 
 .debugLocation {
   background-image: url("chrome://browser/skin/devtools/orion-debug-location.png");
 }
 
 .breakpoint.debugLocation {
   background-image: url("chrome://browser/skin/devtools/orion-debug-location.png"),
     url("chrome://browser/skin/devtools/orion-breakpoint.png");
-}
-
-.CodeMirror-activeline-background {
-  background: #e8f2ff;
 }
\ No newline at end of file
--- a/browser/devtools/sourceeditor/debugger.js
+++ b/browser/devtools/sourceeditor/debugger.js
@@ -57,46 +57,49 @@ function getSearchCursor(cm, query, pos)
     typeof query == "string" && query == query.toLowerCase());
 }
 
 /**
  * If there's a saved search, selects the next results.
  * Otherwise, creates a new search and selects the first
  * result.
  */
-function doSearch(cm, rev, query) {
+function doSearch(ctx, rev, query) {
+  let { cm } = ctx;
   let state = getSearchState(cm);
 
   if (state.query)
-    return searchNext(cm, rev);
+    return searchNext(ctx, rev);
 
   cm.operation(function () {
     if (state.query) return;
 
     state.query = query;
     state.posFrom = state.posTo = { line: 0, ch: 0 };
-    searchNext(cm, rev);
+    searchNext(ctx, rev);
   });
 }
 
 /**
  * Selects the next result of a saved search.
  */
-function searchNext(cm, rev) {
+function searchNext(ctx, rev) {
+  let { cm, ed } = ctx;
   cm.operation(function () {
     let state = getSearchState(cm)
     let cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
 
     if (!cursor.find(rev)) {
       cursor = getSearchCursor(cm, state.query, rev ?
         { line: cm.lastLine(), ch: null } : { line: cm.firstLine(), ch: 0 });
       if (!cursor.find(rev))
         return;
     }
 
+    ed.alignLine(cursor.from().line, "center");
     cm.setSelection(cursor.from(), cursor.to());
     state.posFrom = cursor.from();
     state.posTo = cursor.to();
   });
 }
 
 /**
  * Clears the currently saved search.
@@ -231,37 +234,34 @@ function clearDebugLocation(ctx) {
     meta.debugLocation = null;
   }
 }
 
 /**
  * Starts a new search.
  */
 function find(ctx, query) {
-  let { cm } = ctx;
-  clearSearch(cm);
-  doSearch(cm, false, query);
+  clearSearch(ctx.cm);
+  doSearch(ctx, false, query);
 }
 
 /**
  * Finds the next item based on the currently saved search.
  */
 function findNext(ctx, query) {
-  let { cm } = ctx;
-  doSearch(cm, false, query);
+  doSearch(ctx, false, query);
 }
 
 /**
  * Finds the previous item based on the currently saved search.
  */
 function findPrev(ctx, query) {
-  let { cm } = ctx;
-  doSearch(cm, true, query);
+  doSearch(ctx, true, query);
 }
 
 
 // Export functions
 
 [
   initialize, hasBreakpoint, addBreakpoint, removeBreakpoint,
   getBreakpoints, setDebugLocation, getDebugLocation,
   clearDebugLocation, find, findNext, findPrev
-].forEach(function (func) { module.exports[func.name] = func; });
\ No newline at end of file
+].forEach(function (func) { module.exports[func.name] = func; });
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -7,38 +7,45 @@
 
 const { Cu, Cc, Ci, components } = require("chrome");
 
 const TAB_SIZE    = "devtools.editor.tabsize";
 const EXPAND_TAB  = "devtools.editor.expandtab";
 const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
 const XUL_NS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
+// Maximum allowed margin (in number of lines) from top or bottom of the editor
+// while shifting to a line which was initially out of view.
+const MAX_VERTICAL_OFFSET = 3;
+
 const promise = require("sdk/core/promise");
 const events  = require("devtools/shared/event-emitter");
 
 Cu.import("resource://gre/modules/Services.jsm");
 const L10N = Services.strings.createBundle(L10N_BUNDLE);
 
 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
 // JavaScript and CSS that is injected into an iframe in
 // order to initialize a CodeMirror instance.
 
 const CM_STYLES   = [
+  "chrome://browser/skin/devtools/common.css",
   "chrome://browser/content/devtools/codemirror/codemirror.css",
   "chrome://browser/content/devtools/codemirror/dialog.css",
   "chrome://browser/content/devtools/codemirror/mozilla.css"
 ];
 
 const CM_SCRIPTS  = [
+  "chrome://browser/content/devtools/theme-switching.js",
   "chrome://browser/content/devtools/codemirror/codemirror.js",
   "chrome://browser/content/devtools/codemirror/dialog.js",
   "chrome://browser/content/devtools/codemirror/searchcursor.js",
   "chrome://browser/content/devtools/codemirror/search.js",
   "chrome://browser/content/devtools/codemirror/matchbrackets.js",
+  "chrome://browser/content/devtools/codemirror/closebrackets.js",
   "chrome://browser/content/devtools/codemirror/comment.js",
   "chrome://browser/content/devtools/codemirror/javascript.js",
   "chrome://browser/content/devtools/codemirror/xml.js",
   "chrome://browser/content/devtools/codemirror/css.js",
   "chrome://browser/content/devtools/codemirror/htmlmixed.js",
   "chrome://browser/content/devtools/codemirror/activeline.js"
 ];
 
@@ -48,41 +55,43 @@ const CM_IFRAME   =
   "  <head>" +
   "    <style>" +
   "      html, body { height: 100%; }" +
   "      body { margin: 0; overflow: hidden; }" +
   "      .CodeMirror { width: 100%; height: 100% !important; }" +
   "    </style>" +
 [ "    <link rel='stylesheet' href='" + style + "'>" for (style of CM_STYLES) ].join("\n") +
   "  </head>" +
-  "  <body></body>" +
+  "  <body class='theme-body devtools-monospace'></body>" +
   "</html>";
 
 const CM_MAPPING = [
   "focus",
   "hasFocus",
-  "setCursor",
   "getCursor",
   "somethingSelected",
   "setSelection",
   "getSelection",
   "replaceSelection",
   "undo",
   "redo",
   "clearHistory",
   "openDialog",
   "cursorCoords",
-  "lineCount"
+  "lineCount",
+  "refresh"
 ];
 
 const CM_JUMP_DIALOG = [
   L10N.GetStringFromName("gotoLineCmd.promptTitle")
     + " <input type=text style='width: 10em'/>"
 ];
 
+const { cssProperties, cssValues, cssColors } = getCSSKeywords();
+
 const editors = new WeakMap();
 
 Editor.modes = {
   text: { name: "text" },
   js:   { name: "javascript" },
   html: { name: "htmlmixed" },
   css:  { name: "css" }
 };
@@ -121,17 +130,18 @@ function Editor(config) {
     value:           "",
     mode:            Editor.modes.text,
     indentUnit:      tabSize,
     tabSize:         tabSize,
     contextMenu:     null,
     matchBrackets:   true,
     extraKeys:       {},
     indentWithTabs:  useTabs,
-    styleActiveLine: true
+    styleActiveLine: true,
+    theme: "mozilla"
   };
 
   // Overwrite default config with user-provided, if needed.
   Object.keys(config).forEach((k) => this.config[k] = config[k]);
 
   // Additional shortcuts.
   this.config.extraKeys[ctrl("J")] = (cm) => this.jumpToLine();
   this.config.extraKeys[ctrl("/")] = "toggleComment";
@@ -171,40 +181,53 @@ Editor.prototype = {
    * CodeMirror and all its dependencies.
    *
    * This method is asynchronous and returns a promise.
    */
   appendTo: function (el) {
     let def = promise.defer();
     let cm  = editors.get(this);
     let doc = el.ownerDocument;
-    let env = doc.createElementNS(XUL_NS, "iframe");
+    let env = doc.createElement("iframe");
     env.flex = 1;
 
     if (cm)
       throw new Error("You can append an editor only once.");
 
     let onLoad = () => {
       // Once the iframe is loaded, we can inject CodeMirror
       // and its dependencies into its DOM.
 
       env.removeEventListener("load", onLoad, true);
       let win = env.contentWindow.wrappedJSObject;
 
       CM_SCRIPTS.forEach((url) =>
         Services.scriptloader.loadSubScript(url, win, "utf8"));
 
-      // Create a CodeMirror instance add support for context menus and
+      // Replace the propertyKeywords, colorKeywords and valueKeywords
+      // properties of the CSS MIME type with the values provided by Gecko.
+      let cssSpec = win.CodeMirror.resolveMode("text/css");
+      cssSpec.propertyKeywords = cssProperties;
+      cssSpec.colorKeywords = cssColors;
+      cssSpec.valueKeywords = cssValues;
+      win.CodeMirror.defineMIME("text/css", cssSpec);
+
+      let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
+      scssSpec.propertyKeywords = cssProperties;
+      scssSpec.colorKeywords = cssColors;
+      scssSpec.valueKeywords = cssValues;
+      win.CodeMirror.defineMIME("text/x-scss", scssSpec);
+
+      // Create a CodeMirror instance add support for context menus,
       // overwrite the default controller (otherwise items in the top and
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
-        this.emit("contextMenu");
         this.showContextMenu(doc, ev.screenX, ev.screenY);
       }, false);
 
       cm.on("change", () => this.emit("change"));
       cm.on("gutterClick", (cm, line) => this.emit("gutterClick", line));
       cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
 
       win.CodeMirror.defineExtension("l10n", (name) => {
@@ -429,16 +452,77 @@ Editor.prototype = {
 
       if (name === "initialize")
         return void funcs[name](ctx);
 
       this[name] = funcs[name].bind(null, ctx);
     });
   },
 
+  /**
+   * Gets the first visible line number in the editor.
+   */
+  getFirstVisibleLine: function () {
+    let cm = editors.get(this);
+    return cm.lineAtHeight(0, "local");
+  },
+
+  /**
+   * Scrolls the view such that the given line number is the first visible line.
+   */
+  setFirstVisibleLine: function (line) {
+    let cm = editors.get(this);
+    let { top } = cm.charCoords({line: line, ch: 0}, "local");
+    cm.scrollTo(0, top);
+  },
+
+  /**
+   * Sets the cursor to the specified {line, ch} position with an additional
+   * option to align the line at the "top", "center" or "bottom" of the editor
+   * with "top" being default value.
+   */
+  setCursor: function ({line, ch}, align) {
+    let cm = editors.get(this);
+    this.alignLine(line, align);
+    cm.setCursor({line: line, ch: ch});
+  },
+
+  /**
+   * Aligns the provided line to either "top", "center" or "bottom" of the
+   * editor view with a maximum margin of MAX_VERTICAL_OFFSET lines from top or
+   * bottom.
+   */
+  alignLine: function(line, align) {
+    let cm = editors.get(this);
+    let from = cm.lineAtHeight(0, "page");
+    let to = cm.lineAtHeight(cm.getWrapperElement().clientHeight, "page");
+    let linesVisible = to - from;
+    let halfVisible = Math.round(linesVisible/2);
+
+    // If the target line is in view, skip the vertical alignment part.
+    if (line <= to && line >= from) {
+      return;
+    }
+
+    // Setting the offset so that the line always falls in the upper half
+    // of visible lines (lower half for bottom aligned).
+    // MAX_VERTICAL_OFFSET is the maximum allowed value.
+    let offset = Math.min(halfVisible, MAX_VERTICAL_OFFSET);
+
+    let topLine = {
+      "center": Math.max(line - halfVisible, 0),
+      "bottom": Math.max(line - linesVisible + offset, 0),
+      "top": Math.max(line - offset, 0)
+    }[align || "top"] || offset;
+
+    // Bringing down the topLine to total lines in the editor if exceeding.
+    topLine = Math.min(topLine, this.lineCount());
+    this.setFirstVisibleLine(topLine);
+  },
+
   destroy: function () {
     this.container = null;
     this.config = null;
     this.version = null;
     this.emit("destroy");
   }
 };
 
@@ -447,16 +531,54 @@ Editor.prototype = {
 
 CM_MAPPING.forEach(function (name) {
   Editor.prototype[name] = function (...args) {
     let cm = editors.get(this);
     return cm[name].apply(cm, args);
   };
 });
 
+// Since Gecko already provide complete and up to date list of CSS property
+// names, values and color names, we compute them so that they can replace
+// the ones used in CodeMirror while initiating an editor object. This is done
+// here instead of the file codemirror/css.js so as to leave that file untouched
+// and easily upgradable.
+function getCSSKeywords() {
+  function keySet(array) {
+    var keys = {};
+    for (var i = 0; i < array.length; ++i) {
+      keys[array[i]] = true;
+    }
+    return keys;
+  }
+
+  let domUtils = Cc["@mozilla.org/inspector/dom-utils;1"]
+                   .getService(Ci.inIDOMUtils);
+  let cssProperties = domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES);
+  let cssColors = {};
+  let cssValues = {};
+  cssProperties.forEach(property => {
+    if (property.contains("color")) {
+      domUtils.getCSSValuesForProperty(property).forEach(value => {
+        cssColors[value] = true;
+      });
+    }
+    else {
+      domUtils.getCSSValuesForProperty(property).forEach(value => {
+        cssValues[value] = true;
+      });
+    }
+  });
+  return {
+    cssProperties: keySet(cssProperties),
+    cssValues: cssValues,
+    cssColors: cssColors
+  };
+}
+
 /**
  * Returns a controller object that can be used for
  * editor-specific commands such as find, jump to line,
  * copy/paste, etc.
  */
 function controller(ed, view) {
   return {
     supportsCommand: function (cmd) {
@@ -525,9 +647,9 @@ function controller(ed, view) {
       if (cmd == "cmd_gotoLine")
         ed.jumpToLine(cm);
     },
 
     onEvent: function () {}
   };
 }
 
-module.exports = Editor;
\ No newline at end of file
+module.exports = Editor;
--- a/browser/devtools/styleeditor/StyleEditorUI.jsm
+++ b/browser/devtools/styleeditor/StyleEditorUI.jsm
@@ -68,17 +68,17 @@ StyleEditorUI.prototype = {
    * @return boolean
    */
   get isDirty()
   {
     if (this._markedDirty === true) {
       return true;
     }
     return this.editors.some((editor) => {
-      return editor.sourceEditor && editor.sourceEditor.dirty;
+      return editor.sourceEditor && !editor.sourceEditor.isClean();
     });
   },
 
   /*
    * Mark the style editor as having or not having unsaved changes.
    */
   set isDirty(value) {
     this._markedDirty = value;
@@ -146,18 +146,18 @@ StyleEditorUI.prototype = {
 
   /**
    * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors.
    */
   _onStyleSheetsCleared: function() {
     // remember selected sheet and line number for next load
     if (this.selectedEditor && this.selectedEditor.sourceEditor) {
       let href = this.selectedEditor.styleSheet.href;
-      let {line, col} = this.selectedEditor.sourceEditor.getCaretPosition();
-      this.selectStyleSheet(href, line, col);
+      let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
+      this.selectStyleSheet(href, line, ch);
     }
 
     this._clearStyleSheetEditors();
     this._view.removeAll();
 
     this.selectedEditor = null;
 
     this._root.classList.add("loading");
@@ -360,17 +360,17 @@ StyleEditorUI.prototype = {
    * @param  {number} col
    *         Column number to jump to
    */
   _selectEditor: function(editor, line, col) {
     line = line || 0;
     col = col || 0;
 
     editor.getSourceEditor().then(() => {
-      editor.sourceEditor.setCaretPosition(line, col);
+      editor.sourceEditor.setCursor({line: line, ch: col});
     });
 
     this._view.activeSummary = editor.summary;
   },
 
   /**
    * selects a stylesheet and optionally moves the cursor to a selected line
    *
@@ -382,24 +382,37 @@ StyleEditorUI.prototype = {
    * @param {Number} [line]
    *        Line to which the caret should be moved (zero-indexed).
    * @param {Number} [col]
    *        Column to which the caret should be moved (zero-indexed).
    */
   selectStyleSheet: function(href, line, col)
   {
     let alreadyCalled = !!this._styleSheetToSelect;
+    let originalHref;
+
+    if (alreadyCalled) {
+      originalHref = this._styleSheetToSelect.href;
+    }
 
     this._styleSheetToSelect = {
       href: href,
       line: line,
       col: col,
     };
 
     if (alreadyCalled) {
+      // Just switch to the correct line and columns if the editor is already
+      // selected for the requested stylesheet.
+      for each (let editor in this.editors) {
+        if (editor.styleSheet.href == originalHref) {
+          editor.sourceEditor.setCursor({line: line, ch: col})
+          break;
+        }
+      }
       return;
     }
 
     /* Switch to the editor for this sheet, if it exists yet.
        Otherwise each editor will be checked when it's created. */
     this.switchToSelectedSheet();
   },
 
--- a/browser/devtools/styleeditor/StyleSheetEditor.jsm
+++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm
@@ -6,31 +6,36 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
-let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
+const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const Editor  = require("devtools/sourceeditor/editor");
+const promise = require("sdk/core/promise");
+
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
-Cu.import("resource:///modules/devtools/sourceeditor/source-editor.jsm");
 Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
 
-
 const SAVE_ERROR = "error-save";
 
 // max update frequency in ms (avoid potential typing lag and/or flicker)
 // @see StyleEditor.updateStylesheet
 const UPDATE_STYLESHEET_THROTTLE_DELAY = 500;
 
+function ctrl(k) {
+  return (Services.appinfo.OS == "Darwin" ? "Cmd-" : "Ctrl-") + k;
+}
+
 /**
  * StyleSheetEditor controls the editor linked to a particular StyleSheet
  * object.
  *
  * Emits events:
  *   'source-load': The source of the stylesheet has been fetched
  *   'property-change': A property on the underlying stylesheet has changed
  *   'source-editor-load': The source editor for this editor has been loaded
@@ -53,17 +58,20 @@ function StyleSheetEditor(styleSheet, wi
   this._window = win;
   this._isNew = isNew;
   this.savedFile = file;
 
   this.errorMessage = null;
 
   this._state = {   // state to use when inputElement attaches
     text: "",
-    selection: {start: 0, end: 0},
+    selection: {
+      start: {line: 0, ch: 0},
+      end: {line: 0, ch: 0}
+    },
     readOnly: false,
     topIndex: 0,              // the first visible line
   };
 
   this._styleSheetFilePath = null;
   if (styleSheet.href &&
       Services.io.extractScheme(this.styleSheet.href) == "file") {
     this._styleSheetFilePath = this.styleSheet.href;
@@ -87,17 +95,17 @@ StyleSheetEditor.prototype = {
   get sourceEditor() {
     return this._sourceEditor;
   },
 
   /**
    * Whether there are unsaved changes in the editor
    */
   get unsaved() {
-    return this._sourceEditor && this._sourceEditor.dirty;
+    return this._sourceEditor && !this._sourceEditor.isClean();
   },
 
   /**
    * Whether the editor is for a stylesheet created by the user
    * through the style editor UI.
    */
   get isNew() {
     return this._isNew;
@@ -195,63 +203,61 @@ StyleSheetEditor.prototype = {
   /**
    * Create source editor and load state into it.
    * @param  {DOMElement} inputElement
    *         Element to load source editor in
    */
   load: function(inputElement) {
     this._inputElement = inputElement;
 
-    let sourceEditor = new SourceEditor();
     let config = {
-      initialText: this._state.text,
-      showLineNumbers: true,
-      mode: SourceEditor.MODES.CSS,
+      value: this._state.text,
+      lineNumbers: true,
+      mode: Editor.modes.css,
       readOnly: this._state.readOnly,
-      keys: this._getKeyBindings()
+      autoCloseBrackets: "{}()[]",
+      extraKeys: this._getKeyBindings()
     };
+    let sourceEditor = new Editor(config);
 
-    sourceEditor.init(inputElement, config, function onSourceEditorReady() {
-      setupBracketCompletion(sourceEditor);
-      sourceEditor.addEventListener(SourceEditor.EVENTS.TEXT_CHANGED,
-                                    function onTextChanged(event) {
+    sourceEditor.appendTo(inputElement).then(() => {
+      sourceEditor.on("change", () => {
         this.updateStyleSheet();
-      }.bind(this));
+      });
 
       this._sourceEditor = sourceEditor;
 
       if (this._focusOnSourceEditorReady) {
         this._focusOnSourceEditorReady = false;
         sourceEditor.focus();
       }
 
-      sourceEditor.setTopIndex(this._state.topIndex);
+      sourceEditor.setFirstVisibleLine(this._state.topIndex);
       sourceEditor.setSelection(this._state.selection.start,
                                 this._state.selection.end);
 
       this.emit("source-editor-load");
-    }.bind(this));
+    });
 
-    sourceEditor.addEventListener(SourceEditor.EVENTS.DIRTY_CHANGED,
-                                  this._onPropertyChange);
+    sourceEditor.on("change", this._onPropertyChange);
   },
 
   /**
    * Get the source editor for this editor.
    *
    * @return {Promise}
    *         Promise that will resolve with the editor.
    */
   getSourceEditor: function() {
     let deferred = promise.defer();
 
     if (this.sourceEditor) {
       return promise.resolve(this);
     }
-    this.on("source-editor-load", (event) => {
+    this.on("source-editor-load", () => {
       deferred.resolve(this);
     });
     return deferred.promise;
   },
 
   /**
    * Focus the Style Editor input.
    */
@@ -263,17 +269,17 @@ StyleSheetEditor.prototype = {
     }
   },
 
   /**
    * Event handler for when the editor is shown.
    */
   onShow: function() {
     if (this._sourceEditor) {
-      this._sourceEditor.setTopIndex(this._state.topIndex);
+      this._sourceEditor.setFirstVisibleLine(this._state.topIndex);
     }
     this.focus();
   },
 
   /**
    * Toggled the disabled state of the underlying stylesheet.
    */
   toggleDisabled: function() {
@@ -365,52 +371,39 @@ StyleSheetEditor.prototype = {
         FileUtils.closeSafeFileOutputStream(ostream);
         // remember filename for next save if any
         this._friendlyName = null;
         this.savedFile = returnFile;
 
         if (callback) {
           callback(returnFile);
         }
-        this.sourceEditor.dirty = false;
+        this.sourceEditor.markClean();
       }.bind(this));
     };
 
     showFilePicker(file || this._styleSheetFilePath, true, this._window, onFile);
   },
 
   /**
     * Retrieve custom key bindings objects as expected by SourceEditor.
     * SourceEditor action names are not displayed to the user.
     *
     * @return {array} key binding objects for the source editor
     */
   _getKeyBindings: function() {
-    let bindings = [];
+    let bindings = {};
 
-    bindings.push({
-      action: "StyleEditor.save",
-      code: _("saveStyleSheet.commandkey"),
-      accel: true,
-      callback: function save() {
-        this.saveToFile(this.savedFile);
-        return true;
-      }.bind(this)
-    });
+    bindings[ctrl(_("saveStyleSheet.commandkey"))] = () => {
+      this.saveToFile(this.savedFile);
+    };
 
-    bindings.push({
-      action: "StyleEditor.saveAs",
-      code: _("saveStyleSheet.commandkey"),
-      accel: true,
-      shift: true,
-      callback: function saveAs() {
-        this.saveToFile();
-        return true;
-      }.bind(this)
-    });
+    bindings["Shift-" + ctrl(_("saveStyleSheet.commandkey"))] = () => {
+      this.saveToFile();
+    };
 
     return bindings;
   },
 
   /**
    * Clean up for this editor.
    */
   destroy: function() {
@@ -422,28 +415,16 @@ StyleSheetEditor.prototype = {
 
 
 const TAB_CHARS = "\t";
 
 const OS = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 const LINE_SEPARATOR = OS === "WINNT" ? "\r\n" : "\n";
 
 /**
-  * Return string that repeats text for aCount times.
-  *
-  * @param string text
-  * @param number aCount
-  * @return string
-  */
-function repeat(text, aCount)
-{
-  return (new Array(aCount + 1)).join(text);
-}
-
-/**
  * Prettify minified CSS text.
  * This prettifies CSS code where there is no indentation in usual places while
  * keeping original indentation as-is elsewhere.
  *
  * @param string text
  *        The CSS source to prettify.
  * @return string
  *         Prettified CSS source
@@ -464,17 +445,17 @@ function prettifyCSS(text)
 
     switch (c) {
       case "}":
         if (i - partStart > 1) {
           // there's more than just } on the line, add line
           parts.push(indent + text.substring(partStart, i));
           partStart = i;
         }
-        indent = repeat(TAB_CHARS, --indentLevel);
+        indent = TAB_CHARS.repeat(--indentLevel);
         /* fallthrough */
       case ";":
       case "{":
         shouldIndent = true;
         break;
     }
 
     if (shouldIndent) {
@@ -488,63 +469,14 @@ function prettifyCSS(text)
         }
         partStart = i + 1;
       } else {
         return text; // assume it is not minified, early exit
       }
     }
 
     if (c == "{") {
-      indent = repeat(TAB_CHARS, ++indentLevel);
+      indent = TAB_CHARS.repeat(++indentLevel);
     }
   }
   return parts.join(LINE_SEPARATOR);
 }
 
-
-/**
- * Set up bracket completion on a given SourceEditor.
- * This automatically closes the following CSS brackets: "{", "(", "["
- *
- * @param SourceEditor sourceEditor
- */
-function setupBracketCompletion(sourceEditor)
-{
-  let editorElement = sourceEditor.editorElement;
-  let pairs = {
-    123: { // {
-      closeString: "}",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
-    },
-    40: { // (
-      closeString: ")",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_0
-    },
-    91: { // [
-      closeString: "]",
-      closeKeyCode: Ci.nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET
-    },
-  };
-
-  editorElement.addEventListener("keypress", function onKeyPress(event) {
-    let pair = pairs[event.charCode];
-    if (!pair || event.ctrlKey || event.metaKey ||
-        event.accelKey || event.altKey) {
-      return true;
-    }
-
-    // We detected an open bracket, sending closing character
-    let keyCode = pair.closeKeyCode;
-    let charCode = pair.closeString.charCodeAt(0);
-    let modifiers = 0;
-    let utils = editorElement.ownerDocument.defaultView.
-                  QueryInterface(Ci.nsIInterfaceRequestor).
-                  getInterface(Ci.nsIDOMWindowUtils);
-                  
-    if (utils.sendKeyEvent("keydown", keyCode, 0, modifiers)) {
-      utils.sendKeyEvent("keypress", 0, charCode, modifiers);
-    }
-    utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
-    // and rewind caret
-    sourceEditor.setCaretOffset(sourceEditor.getCaretOffset() - 1);
-  }, false);
-}
-
--- a/browser/devtools/styleeditor/test/browser_styleeditor_new.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_new.js
@@ -91,42 +91,34 @@ function testEditor(aEditor) {
   let ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
   is(parseInt(ruleCount), 0,
      "new editor initially shows 0 rules");
 
   let computedStyle = content.getComputedStyle(content.document.body, null);
   is(computedStyle.backgroundColor, "rgb(255, 255, 255)",
      "content's background color is initially white");
 
-  EventUtils.synthesizeKey("[", {accelKey: true}, gPanelWindow);
-  is(aEditor.sourceEditor.getText(), "",
-     "Nothing happened as it is a known shortcut in source editor");
-
-  EventUtils.synthesizeKey("]", {accelKey: true}, gPanelWindow);
-  is(aEditor.sourceEditor.getText(), "",
-     "Nothing happened as it is a known shortcut in source editor");
-
   for each (let c in TESTCASE_CSS_SOURCE) {
     EventUtils.synthesizeKey(c, {}, gPanelWindow);
   }
 
-  is(aEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
-     "rule bracket has been auto-closed");
-
   ok(aEditor.unsaved,
      "new editor has unsaved flag");
 
   // we know that the testcase above will start a CSS transition
   content.addEventListener("transitionend", onTransitionEnd, false);
 }, gPanelWindow) ;
 }
 
 function onTransitionEnd() {
   content.removeEventListener("transitionend", onTransitionEnd, false);
 
+  is(gNewEditor.sourceEditor.getText(), TESTCASE_CSS_SOURCE + "}",
+     "rule bracket has been auto-closed");
+
   let computedStyle = content.getComputedStyle(content.document.body, null);
   is(computedStyle.backgroundColor, "rgb(255, 0, 0)",
      "content's background color has been updated to red");
 
   if (gNewEditor) {
     is(gNewEditor.styleSheet.href, gOriginalHref,
        "style sheet href did not change");
   }
--- a/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_reload.js
@@ -49,19 +49,19 @@ function runTests()
   });
   gUI.selectStyleSheet(gUI.editors[1].styleSheet.href, LINE_NO, COL_NO);
 }
 
 function testRemembered()
 {
   is(gUI.selectedEditor, gUI.editors[1], "second editor is selected");
 
-  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
   is(line, LINE_NO, "correct line selected");
-  is(col, COL_NO, "correct column selected");
+  is(ch, COL_NO, "correct column selected");
 
   testNewPage();
 }
 
 function testNewPage()
 {
   let count = 0;
   gUI.on("editor-added", function editorAdded(event, editor) {
@@ -75,25 +75,25 @@ function testNewPage()
   info("navigating to a different page");
   navigatePage();
 }
 
 function testNotRemembered()
 {
   is(gUI.selectedEditor, gUI.editors[0], "first editor is selected");
 
-  let {line, col} = gUI.selectedEditor.sourceEditor.getCaretPosition();
+  let {line, ch} = gUI.selectedEditor.sourceEditor.getCursor();
   is(line, 0, "first line is selected");
-  is(col, 0, "first column is selected");
+  is(ch, 0, "first column is selected");
 
   gUI = null;
   finish();
 }
 
 function reloadPage()
 {
   gContentWin.location.reload();
 }
 
 function navigatePage()
 {
   gContentWin.location = NEW_URI;
-}
\ No newline at end of file
+}
--- a/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sv_resize.js
@@ -24,32 +24,34 @@ function test()
   content.location = TESTCASE_URI;
 }
 
 function runTests(aUI)
 {
   is(aUI.editors.length, 2,
      "there is 2 stylesheets initially");
 
-  aUI.editors[0].getSourceEditor().then(function onEditorAttached(aEditor) {
+  aUI.editors[0].getSourceEditor().then(aEditor => {
     executeSoon(function () {
       waitForFocus(function () {
         // queue a resize to inverse aspect ratio
         // this will trigger a detach and reattach (to workaround bug 254144)
         let originalSourceEditor = aEditor.sourceEditor;
-        aEditor.sourceEditor.setCaretOffset(4); // to check the caret is preserved
+        let editor = aEditor.sourceEditor;
+        editor.setCursor(editor.getPosition(4)); // to check the caret is preserved
 
         gOriginalWidth = gPanelWindow.outerWidth;
         gOriginalHeight = gPanelWindow.outerHeight;
         gPanelWindow.resizeTo(120, 480);
 
         executeSoon(function () {
           is(aEditor.sourceEditor, originalSourceEditor,
              "the editor still references the same SourceEditor instance");
-          is(aEditor.sourceEditor.getCaretOffset(), 4,
+          let editor = aEditor.sourceEditor;
+          is(editor.getOffset(editor.getCursor()), 4,
              "the caret position has been preserved");
 
           // queue a resize to original aspect ratio
           waitForFocus(function () {
             gPanelWindow.resizeTo(gOriginalWidth, gOriginalHeight);
             executeSoon(function () {
               finish();
             });
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -229,9 +229,10 @@ support-files =
 [browser_webconsole_netlogging.js]
 [browser_webconsole_network_panel.js]
 [browser_webconsole_notifications.js]
 [browser_webconsole_output_copy_newlines.js]
 [browser_webconsole_output_order.js]
 [browser_webconsole_property_provider.js]
 [browser_webconsole_scratchpad_panel_link.js]
 [browser_webconsole_view_source.js]
+[browser_webconsole_reflow.js]
 [browser_webconsole_log_file_filter.js]
--- a/browser/devtools/webconsole/test/browser_console.js
+++ b/browser/devtools/webconsole/test/browser_console.js
@@ -57,17 +57,17 @@ function consoleOpened(hud)
   let xhrRequest = false;
 
   let output = hud.outputNode;
   function performChecks()
   {
     let text = output.textContent;
     chromeConsole = text.indexOf("bug587757a");
     contentConsole = text.indexOf("bug587757b");
-    execValue = text.indexOf("webconsole.xul");
+    execValue = text.indexOf("browser.xul");
     exception = text.indexOf("foobarExceptionBug587757");
     xhrRequest = text.indexOf("test-console.html");
   }
 
   function showResults()
   {
     isnot(chromeConsole, -1, "chrome window console.log() is displayed");
     isnot(contentConsole, -1, "content window console.log() is displayed");
--- a/browser/devtools/webconsole/test/browser_console_dead_objects.js
+++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js
@@ -22,17 +22,18 @@ function test()
   {
     hud = aHud;
     ok(hud, "browser console opened");
 
     hud.jsterm.clearOutput();
     hud.jsterm.execute("Cu = Components.utils;" +
                        "Cu.import('resource://gre/modules/Services.jsm');" +
                        "chromeWindow = Services.wm.getMostRecentWindow('navigator:browser');" +
-                       "foobarzTezt = chromeWindow.content.document", onAddVariable);
+                       "foobarzTezt = chromeWindow.content.document;" +
+                       "delete chromeWindow", onAddVariable);
   }
 
   function onAddVariable()
   {
     gBrowser.removeCurrentTab();
 
     hud.jsterm.execute("foobarzTezt", onReadVariable);
   }
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_622303_persistent_filters.js
@@ -4,17 +4,18 @@
 let prefs = {
   "net": [
     "network",
     "netwarn",
     "networkinfo",
   ],
   "css": [
     "csserror",
-    "cssparser"
+    "cssparser",
+    "csslog"
   ],
   "js": [
     "exception",
     "jswarn",
     "jslog",
   ],
   "logging": [
      "error",
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js
@@ -126,17 +126,17 @@ function checkStyleEditorForSheetAndLine
     failureFn: finishTest,
   });
 }
 
 function performLineCheck(aEditor, aLine, aCallback)
 {
   function checkForCorrectState()
   {
-    is(aEditor.sourceEditor.getCaretPosition().line, aLine,
+    is(aEditor.sourceEditor.getCursor().line, aLine,
        "correct line is selected");
     is(StyleEditorUI.selectedStyleSheetIndex, aEditor.styleSheet.styleSheetIndex,
        "correct stylesheet is selected in the editor");
 
     aCallback && executeSoon(aCallback);
   }
 
   waitForSuccess({
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_reflow.js
@@ -0,0 +1,35 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test()
+{
+  addTab("data:text/html;charset=utf-8,Web Console test for reflow activity");
+
+  browser.addEventListener("load", function onLoad() {
+    browser.removeEventListener("load", onLoad, true);
+    openConsole(gBrowser.selectedTab, function(hud) {
+
+      function onReflowListenersReady(aType, aPacket) {
+        browser.contentDocument.body.style.display = "none";
+        browser.contentDocument.body.clientTop;
+      }
+
+      Services.prefs.setBoolPref("devtools.webconsole.filter.csslog", true);
+      hud.ui._updateReflowActivityListener(onReflowListenersReady);
+      Services.prefs.clearUserPref("devtools.webconsole.filter.csslog");
+
+      waitForMessages({
+        webconsole: hud,
+        messages: [{
+          text: /reflow: /,
+          category: CATEGORY_CSS,
+          severity: SEVERITY_LOG,
+        }],
+      }).then(() => {
+        finishTest();
+      });
+    });
+  }, true);
+}
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -101,24 +101,24 @@ const SEVERITY_CLASS_FRAGMENTS = [
 ];
 
 // The preference keys to use for each category/severity combination, indexed
 // first by category (rows) and then by severity (columns).
 //
 // Most of these rather idiosyncratic names are historical and predate the
 // division of message type into "category" and "severity".
 const MESSAGE_PREFERENCE_KEYS = [
-//  Error         Warning   Info    Log
-  [ "network",    "netwarn",    null,   "networkinfo", ],  // Network
-  [ "csserror",   "cssparser",  null,   null,          ],  // CSS
-  [ "exception",  "jswarn",     null,   "jslog",       ],  // JS
-  [ "error",      "warn",       "info", "log",         ],  // Web Developer
-  [ null,         null,         null,   null,          ],  // Input
-  [ null,         null,         null,   null,          ],  // Output
-  [ "secerror",   "secwarn",    null,   null,          ],  // Security
+//  Error         Warning       Info      Log
+  [ "network",    "netwarn",    null,     "networkinfo", ],  // Network
+  [ "csserror",   "cssparser",  null,     "csslog",      ],  // CSS
+  [ "exception",  "jswarn",     null,     "jslog",       ],  // JS
+  [ "error",      "warn",       "info",   "log",         ],  // Web Developer
+  [ null,         null,         null,     null,          ],  // Input
+  [ null,         null,         null,     null,          ],  // Output
+  [ "secerror",   "secwarn",    null,     null,          ],  // Security
 ];
 
 // A mapping from the console API log event levels to the Web Console
 // severities.
 const LEVELS = {
   error: SEVERITY_ERROR,
   warn: SEVERITY_WARNING,
   info: SEVERITY_INFO,
@@ -558,26 +558,48 @@ WebConsoleFrame.prototype = {
   },
 
   /**
    * Initialize the default filter preferences.
    * @private
    */
   _initDefaultFilterPrefs: function WCF__initDefaultFilterPrefs()
   {
-    let prefs = ["network", "networkinfo", "csserror", "cssparser", "exception",
-                 "jswarn", "jslog", "error", "info", "warn", "log", "secerror",
-                 "secwarn", "netwarn"];
+    let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
+                 "exception", "jswarn", "jslog", "error", "info", "warn", "log",
+                 "secerror", "secwarn", "netwarn"];
     for (let pref of prefs) {
       this.filterPrefs[pref] = Services.prefs
                                .getBoolPref(this._filterPrefsPrefix + pref);
     }
   },
 
   /**
+   * Attach / detach reflow listeners depending on the checked status
+   * of the `CSS > Log` menuitem.
+   *
+   * @param function [aCallback=null]
+   *        Optional function to invoke when the listener has been
+   *        added/removed.
+   *
+   */
+  _updateReflowActivityListener:
+    function WCF__updateReflowActivityListener(aCallback)
+  {
+    if (this.webConsoleClient) {
+      let pref = this._filterPrefsPrefix + "csslog";
+      if (Services.prefs.getBoolPref(pref)) {
+        this.webConsoleClient.startListeners(["ReflowActivity"], aCallback);
+      } else {
+        this.webConsoleClient.stopListeners(["ReflowActivity"], aCallback);
+      }
+    }
+  },
+
+  /**
    * Sets the events for the filter input field.
    * @private
    */
   _setFilterTextBoxEvents: function WCF__setFilterTextBoxEvents()
   {
     let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     let timerEvent = this.adjustVisibilityOnSearchStringChange.bind(this);
 
@@ -782,16 +804,17 @@ WebConsoleFrame.prototype = {
    * @param boolean aState
    * @returns void
    */
   setFilterState: function WCF_setFilterState(aToggleType, aState)
   {
     this.filterPrefs[aToggleType] = aState;
     this.adjustVisibilityForMessageType(aToggleType, aState);
     Services.prefs.setBoolPref(this._filterPrefsPrefix + aToggleType, aState);
+    this._updateReflowActivityListener();
   },
 
   /**
    * Get the filter state for a specific toggle button.
    *
    * @param string aToggleType
    * @returns boolean
    */
@@ -1530,16 +1553,52 @@ WebConsoleFrame.prototype = {
    *        The file URI that was requested.
    */
   handleFileActivity: function WCF_handleFileActivity(aFileURI)
   {
     this.outputMessage(CATEGORY_NETWORK, this.logFileActivity, [aFileURI]);
   },
 
   /**
+   * Handle the reflow activity messages coming from the remote Web Console.
+   *
+   * @param object aMessage
+   *        An object holding information about a reflow batch.
+   */
+  logReflowActivity: function WCF_logReflowActivity(aMessage)
+  {
+    let {start, end, sourceURL, sourceLine} = aMessage;
+    let duration = Math.round((end - start) * 100) / 100;
+    let node = this.document.createElementNS(XHTML_NS, "span");
+    if (sourceURL) {
+      node.textContent = l10n.getFormatStr("reflow.messageWithLink", [duration]);
+      let a = this.document.createElementNS(XHTML_NS, "a");
+      a.href = "#";
+      a.draggable = "false";
+      let filename = WebConsoleUtils.abbreviateSourceURL(sourceURL);
+      let functionName = aMessage.functionName || l10n.getStr("stacktrace.anonymousFunction");
+      a.textContent = l10n.getFormatStr("reflow.messageLinkText",
+                         [functionName, filename, sourceLine]);
+      this._addMessageLinkCallback(a, () => {
+        this.owner.viewSourceInDebugger(sourceURL, sourceLine);
+      });
+      node.appendChild(a);
+    } else {
+      node.textContent = l10n.getFormatStr("reflow.messageWithNoLink", [duration]);
+    }
+    return this.createMessageNode(CATEGORY_CSS, SEVERITY_LOG, node);
+  },
+
+
+  handleReflowActivity: function WCF_handleReflowActivity(aMessage)
+  {
+    this.outputMessage(CATEGORY_CSS, this.logReflowActivity, [aMessage]);
+  },
+
+  /**
    * Inform user that the window.console API has been replaced by a script
    * in a content page.
    */
   logWarningAboutReplacedAPI: function WCF_logWarningAboutReplacedAPI()
   {
     let node = this.createMessageNode(CATEGORY_JS, SEVERITY_WARNING,
                                       l10n.getStr("ConsoleAPIDisabled"));
     this.outputMessage(CATEGORY_JS, node);
@@ -2344,18 +2403,20 @@ WebConsoleFrame.prototype = {
       if (str !== undefined) {
         aBody = this.document.createTextNode(str);
         bodyNode.appendChild(aBody);
       }
     }
 
     // Add the message repeats node only when needed.
     let repeatNode = null;
-    if (aCategory != CATEGORY_INPUT && aCategory != CATEGORY_OUTPUT &&
-        aCategory != CATEGORY_NETWORK) {
+    if (aCategory != CATEGORY_INPUT &&
+        aCategory != CATEGORY_OUTPUT &&
+        aCategory != CATEGORY_NETWORK &&
+        !(aCategory == CATEGORY_CSS && aSeverity == SEVERITY_LOG)) {
       repeatNode = this.document.createElementNS(XHTML_NS, "span");
       repeatNode.setAttribute("value", "1");
       repeatNode.className = "repeats";
       repeatNode.textContent = 1;
       repeatNode._uid = [bodyNode.textContent, aCategory, aSeverity, aLevel,
                          aSourceURL, aSourceLine].join(":");
     }
 
@@ -4651,16 +4712,17 @@ function WebConsoleConnectionProxy(aWebC
   this.target = aTarget;
 
   this._onPageError = this._onPageError.bind(this);
   this._onLogMessage = this._onLogMessage.bind(this);
   this._onConsoleAPICall = this._onConsoleAPICall.bind(this);
   this._onNetworkEvent = this._onNetworkEvent.bind(this);
   this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
   this._onFileActivity = this._onFileActivity.bind(this);
+  this._onReflowActivity = this._onReflowActivity.bind(this);
   this._onTabNavigated = this._onTabNavigated.bind(this);
   this._onAttachConsole = this._onAttachConsole.bind(this);
   this._onCachedMessages = this._onCachedMessages.bind(this);
   this._connectionTimeout = this._connectionTimeout.bind(this);
   this._onLastPrivateContextExited = this._onLastPrivateContextExited.bind(this);
 }
 
 WebConsoleConnectionProxy.prototype = {
@@ -4757,16 +4819,17 @@ WebConsoleConnectionProxy.prototype = {
     let client = this.client = this.target.client;
 
     client.addListener("logMessage", this._onLogMessage);
     client.addListener("pageError", this._onPageError);
     client.addListener("consoleAPICall", this._onConsoleAPICall);
     client.addListener("networkEvent", this._onNetworkEvent);
     client.addListener("networkEventUpdate", this._onNetworkEventUpdate);
     client.addListener("fileActivity", this._onFileActivity);
+    client.addListener("reflowActivity", this._onReflowActivity);
     client.addListener("lastPrivateContextExited", this._onLastPrivateContextExited);
     this.target.on("will-navigate", this._onTabNavigated);
     this.target.on("navigate", this._onTabNavigated);
 
     this._consoleActor = this.target.form.consoleActor;
     if (!this.target.chrome) {
       let tab = this.target.form;
       this.owner.onLocationChange(tab.url, tab.title);
@@ -4822,16 +4885,18 @@ WebConsoleConnectionProxy.prototype = {
     }
 
     this.webConsoleClient = aWebConsoleClient;
 
     this._hasNativeConsoleAPI = aResponse.nativeConsoleAPI;
 
     let msgs = ["PageError", "ConsoleAPI"];
     this.webConsoleClient.getCachedMessages(msgs, this._onCachedMessages);
+
+    this.owner._updateReflowActivityListener();
   },
 
   /**
    * The "cachedMessages" response handler.
    *
    * @private
    * @param object aResponse
    *        The JSON response object received from the server.
@@ -4959,16 +5024,23 @@ WebConsoleConnectionProxy.prototype = {
    */
   _onFileActivity: function WCCP__onFileActivity(aType, aPacket)
   {
     if (this.owner && aPacket.from == this._consoleActor) {
       this.owner.handleFileActivity(aPacket.uri);
     }
   },
 
+  _onReflowActivity: function WCCP__onReflowActivity(aType, aPacket)
+  {
+    if (this.owner && aPacket.from == this._consoleActor) {
+      this.owner.handleReflowActivity(aPacket);
+    }
+  },
+
   /**
    * The "lastPrivateContextExited" message type handler. When this message is
    * received the Web Console UI is cleared.
    *
    * @private
    * @param string aType
    *        Message type.
    * @param object aPacket
@@ -5034,16 +5106,17 @@ WebConsoleConnectionProxy.prototype = {
     }
 
     this.client.removeListener("logMessage", this._onLogMessage);
     this.client.removeListener("pageError", this._onPageError);
     this.client.removeListener("consoleAPICall", this._onConsoleAPICall);
     this.client.removeListener("networkEvent", this._onNetworkEvent);
     this.client.removeListener("networkEventUpdate", this._onNetworkEventUpdate);
     this.client.removeListener("fileActivity", this._onFileActivity);
+    this.client.removeListener("reflowActivity", this._onReflowActivity);
     this.client.removeListener("lastPrivateContextExited", this._onLastPrivateContextExited);
     this.target.off("will-navigate", this._onTabNavigated);
     this.target.off("navigate", this._onTabNavigated);
 
     this.client = null;
     this.webConsoleClient = null;
     this.target = null;
     this.connected = false;
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -104,16 +104,18 @@ function goUpdateConsoleCommands() {
                        tooltiptext="&btnPageCSS.tooltip;"
                        accesskey="&btnPageCSS.accesskey;"
                        tabindex="4">
           <menupopup>
             <menuitem label="&btnConsoleErrors;" type="checkbox" autocheck="false"
                       prefKey="csserror"/>
             <menuitem label="&btnConsoleWarnings;" type="checkbox"
                       autocheck="false" prefKey="cssparser"/>
+            <menuitem label="&btnConsoleLog;" type="checkbox"
+                      autocheck="false" prefKey="csslog"/>
           </menupopup>
         </toolbarbutton>
         <toolbarbutton label="&btnPageJS.label;" type="menu-button"
                        category="js" class="devtools-toolbarbutton webconsole-filter-button"
                        tooltiptext="&btnPageJS.tooltip;"
                        accesskey="&btnPageJS.accesskey;"
                        tabindex="5">
           <menupopup>
--- a/browser/extensions/Makefile.in
+++ b/browser/extensions/Makefile.in
@@ -14,28 +14,34 @@ exclude_files = \
   bootstrap.js \
   icon.png \
   icon64.png \
   $(NULL)
 
 $(FINAL_TARGET)/chrome/pdfjs.manifest: $(GLOBAL_DEPS)
 	printf "manifest pdfjs/chrome.manifest" > $@
 
+libs:: $(FINAL_TARGET)/chrome/pdfjs.manifest
+	$(PYTHON) $(topsrcdir)/config/nsinstall.py \
+	  $(srcdir)/pdfjs \
+          $(foreach exclude,$(exclude_files), -X $(srcdir)/pdfjs/$(exclude)) \
+          $(FINAL_TARGET)/chrome
+	$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/pdfjs.manifest")
+
+ifdef NIGHTLY_BUILD
 $(FINAL_TARGET)/chrome/shumway.manifest: $(GLOBAL_DEPS)
 	printf "manifest shumway/chrome.manifest" > $@
 
-libs:: $(FINAL_TARGET)/chrome/pdfjs.manifest $(FINAL_TARGET)/chrome/shumway.manifest
+libs:: $(FINAL_TARGET)/chrome/shumway.manifest
 	$(PYTHON) $(topsrcdir)/config/nsinstall.py \
-	  $(srcdir)/pdfjs \
-          $(foreach exclude,$(exclude_files), -X $(srcdir)/pdfjs/$(exclude)) \
 	  $(srcdir)/shumway \
           $(foreach exclude,$(exclude_files), -X $(srcdir)/shumway/$(exclude)) \
           $(FINAL_TARGET)/chrome
-	$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/pdfjs.manifest")
 	$(call py_action,buildlist,$(FINAL_TARGET)/chrome.manifest "manifest chrome/shumway.manifest")
+endif
 
 ifdef MOZ_METRO
 $(DIST)/bin/metro/chrome/pdfjs.manifest: $(GLOBAL_DEPS)
 	printf "manifest pdfjs/chrome.manifest" > $@
 
 libs:: $(DIST)/bin/metro/chrome/pdfjs.manifest
 	$(PYTHON) $(topsrcdir)/config/nsinstall.py \
 	  $(srcdir)/pdfjs \
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -568,18 +568,20 @@
 #endif
 
 ; [Browser Chrome Files]
 @BINPATH@/browser/chrome.manifest
 @BINPATH@/browser/chrome/browser@JAREXT@
 @BINPATH@/browser/chrome/browser.manifest
 @BINPATH@/browser/chrome/pdfjs.manifest
 @BINPATH@/browser/chrome/pdfjs/*
+#ifdef NIGHTLY_BUILD
 @BINPATH@/browser/chrome/shumway.manifest
 @BINPATH@/browser/chrome/shumway/*
+#endif
 @BINPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/install.rdf
 @BINPATH@/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}/icon.png
 @BINPATH@/chrome/toolkit@JAREXT@
 @BINPATH@/chrome/toolkit.manifest
 @BINPATH@/chrome/recording.manifest
 @BINPATH@/chrome/recording/*
 #ifdef MOZ_GTK
 @BINPATH@/browser/chrome/icons/default/default16.png
--- a/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
@@ -69,16 +69,19 @@
 <!ENTITY projects.removeAppFromList "Remove this app from the list of apps you are working on. This will not remove it from a device or a simulator.">
 <!ENTITY projects.updateApp "Update">
 <!ENTITY projects.updateAppTooltip "Execute validation checks and update the app to the connected device">
 <!ENTITY projects.debugApp "Debug">
 <!ENTITY projects.debugAppTooltip "Open Developer Tools connected to this app">
 <!ENTITY projects.hostedManifestPlaceHolder2 "http://example.com/app/manifest.webapp">
 <!ENTITY projects.noProjects "No projects. Add a new packaged app below (local directory) or a hosted app (link to a manifest file).">
 <!ENTITY projects.manifestEditor "Manifest Editor">
+<!ENTITY projects.manifestEditorTooltip "Edit your app's manifest in the panel below. The Update button will save your changes and update the app.">
+<!ENTITY projects.manifestViewer "Manifest Viewer">
+<!ENTITY projects.manifestViewerTooltip "Examine your app's manifest in the panel below.">
 
 <!ENTITY help.title "App Manager">
 <!ENTITY help.close "Close">
 <!ENTITY help.intro "This tool will help you build and install web apps on compatible devices (i.e. Firefox OS). The <strong>Apps</strong> tab will assist you in the validation and installation process of your app. The <strong>Device</strong> tab will give you information about the connected device. Use the bottom toolbar to connect to a device or start the simulator.">
 <!ENTITY help.usefullLinks "Useful links:">
 <!ENTITY help.appMgrDoc "Documentation: Using the App Manager">
 <!ENTITY help.configuringDevice "How to setup your Firefox OS device">
 <!ENTITY help.troubleShooting "Troubleshooting">
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.dtd
@@ -1,8 +1,11 @@
+<!ENTITY inspectorHTMLEdit.label       "Edit As HTML">
+<!ENTITY inspectorHTMLEdit.accesskey   "E">
+
 <!ENTITY inspectorHTMLCopyInner.label       "Copy Inner HTML">
 <!ENTITY inspectorHTMLCopyInner.accesskey   "I">
 
 <!ENTITY inspectorHTMLCopyOuter.label       "Copy Outer HTML">
 <!ENTITY inspectorHTMLCopyOuter.accesskey   "O">
 
 <!ENTITY inspectorCopyUniqueSelector.label       "Copy Unique Selector">
 <!ENTITY inspectorCopyUniqueSelector.accesskey   "U">
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -84,16 +84,27 @@ webConsoleMoreInfoLabel=Learn More
 # indicate how to jump into scratchpad mode.
 scratchpad.linkText=Shift+RETURN - Open in Scratchpad
 
 # LOCALIZATION NOTE (gcliterm.instanceLabel): the console displays objects
 # using their type (from the constructor function) in this descriptive string.
 # Parameters: %S is the object type.
 gcliterm.instanceLabel=Instance of %S
 
+# LOCALIZATION NOTE (reflow.*): the console displays reflow activity.
+# We can get 2 kind of lines: with JS link or without JS link. It looks like
+# that:
+# reflow: 12ms
+# reflow: 12ms function foobar, file.js line 42
+# The 2nd line, from "function" to the end of the line, is a link to the
+# JavaScript debugger.
+reflow.messageWithNoLink=reflow: %Sms
+reflow.messageWithLink=reflow: %Sms\u0020
+reflow.messageLinkText=function %1$S, %2$S line %3$S
+
 # LOCALIZATION NOTE (stacktrace.anonymousFunction): this string is used to
 # display JavaScript functions that have no given name - they are said to be
 # anonymous. See also stacktrace.outputMessage.
 stacktrace.anonymousFunction=<anonymous>
 
 # LOCALIZATION NOTE (stacktrace.outputMessage): this string is used in the Web
 # Console output to identify a web developer call to console.trace(). The
 # stack trace of JavaScript function calls is displayed. In this minimal
--- a/browser/metro/base/content/ContextCommands.js
+++ b/browser/metro/base/content/ContextCommands.js
@@ -363,17 +363,18 @@ var ContextCommands = {
     // prefered file extension
     let fileExtension = mediaURL.substring(mediaURL.lastIndexOf(".") + 1);
     if (fileExtension.length)
       picker.defaultExtension = fileExtension;
     picker.appendFilters(Ci.nsIFilePicker.filterImages);
 
     // prefered save location
     Task.spawn(function() {
-      picker.displayDirectory = yield Downloads.getPreferredDownloadsDirectory();
+      let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+      picker.displayDirectory = new FileUtils.File(preferredDir);
 
       try {
         let lastDir = Services.prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
         if (this.isAccessibleDirectory(lastDir))
           picker.displayDirectory = lastDir;
       }
       catch (e) { }
 
--- a/browser/metro/base/content/apzc.js
+++ b/browser/metro/base/content/apzc.js
@@ -56,23 +56,24 @@ var APZCObserver = {
         }
         windowUtils.setDisplayPortForElement(0, 0, ContentAreaObserver.width,
                                              ContentAreaObserver.height,
                                              element);
         break;
       case 'TabOpen': {
         let browser = aEvent.originalTarget.linkedBrowser;
         browser.addEventListener("pageshow", this, true);
-        browser.messageManager.addMessageListener("scroll", this);
+        // Register for notifications from content about scroll actions.
+        browser.messageManager.addMessageListener("Browser:ContentScroll", this);
         break;
       }
       case 'TabClose': {
         let browser = aEvent.originalTarget.linkedBrowser;
         browser.removeEventListener("pageshow", this, true);
-        browser.messageManager.removeMessageListener("scroll", this);
+        browser.messageManager.removeMessageListener("Browser:ContentScroll", this);
         break;
       }
     }
   },
   shutdown: function shutdown() {
     if (!this._enabled) {
       return;
     }
@@ -94,17 +95,17 @@ var APZCObserver = {
       let resolution = frameMetrics.resolution;
       let compositedRect = frameMetrics.compositedRect;
 
       let windowUtils = Browser.selectedBrowser.contentWindow.
                                 QueryInterface(Ci.nsIInterfaceRequestor).
                                 getInterface(Ci.nsIDOMWindowUtils);
       windowUtils.setScrollPositionClampingScrollPortSize(compositedRect.width,
                                                           compositedRect.height);
-      Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
+      Browser.selectedBrowser.messageManager.sendAsyncMessage("Content:SetDisplayPort", {
         scrollX: scrollTo.x,
         scrollY: scrollTo.y,
         x: displayPort.x + scrollTo.x,
         y: displayPort.y + scrollTo.y,
         w: displayPort.width,
         h: displayPort.height,
         scale: resolution,
         id: scrollId
@@ -128,16 +129,21 @@ var APZCObserver = {
         InputSourceHelper._imprecise();
       }
     }
   },
 
   receiveMessage: function(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
-      case "scroll": {
+       // Content notifies us here (syncronously) if it has scrolled
+       // independent of the apz. This can happen in a lot of
+       // cases: keyboard shortcuts, scroll wheel, or content script.
+       // Let the apz know about this change so that it can update
+       // its scroll offset data.
+      case "Browser:ContentScroll": {
         let data = json.viewId + " " + json.presShellId + " (" + json.scrollOffset.x + ", " + json.scrollOffset.y + ")";
         Services.obs.notifyObservers(null, "scroll-offset-changed", data);
         break;
       }
     }
   }
 };
--- a/browser/metro/base/content/bindings/browser.js
+++ b/browser/metro/base/content/bindings/browser.js
@@ -547,17 +547,17 @@ let DOMEvents =  {
 
 DOMEvents.init();
 
 let ContentScroll =  {
   // The most recent offset set by APZC for the root scroll frame
   _scrollOffset: { x: 0, y: 0 },
 
   init: function() {
-    addMessageListener("Content:SetCacheViewport", this);
+    addMessageListener("Content:SetDisplayPort", this);
     addMessageListener("Content:SetWindowSize", this);
 
     if (Services.prefs.getBoolPref("layers.async-pan-zoom.enabled")) {
       addEventListener("scroll", this, false);
     }
     addEventListener("pagehide", this, false);
     addEventListener("MozScrolledAreaChanged", this, false);
   },
@@ -582,17 +582,19 @@ let ContentScroll =  {
       aElement.scrollLeft = aLeft;
       aElement.scrollTop = aTop;
     }
   },
 
   receiveMessage: function(aMessage) {
     let json = aMessage.json;
     switch (aMessage.name) {
-      case "Content:SetCacheViewport": {
+      // Sent to us from chrome when the the apz has requested that the
+      // display port be updated and that content should repaint.
+      case "Content:SetDisplayPort": {
         // Set resolution for root view
         let rootCwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
         if (json.id == 1) {
           rootCwu.setResolution(json.scale, json.scale);
           if (!WebProgressListener._firstPaint)
             break;
         }
 
@@ -654,17 +656,17 @@ let ContentScroll =  {
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "pagehide":
         this._scrollOffset = { x: 0, y: 0 };
         break;
 
       case "scroll": {
-        this.sendScroll(aEvent.target);
+        this.notifyChromeAboutContentScroll(aEvent.target);
         break;
       }
 
       case "MozScrolledAreaChanged": {
         let doc = aEvent.originalTarget;
         if (content != doc.defaultView) // We are only interested in root scroll pane changes
           return;
 
@@ -681,17 +683,23 @@ let ContentScroll =  {
           sendAsyncMessage("Content:UpdateDisplayPort");
         }, false);
 
         break;
       }
     }
   },
 
-  sendScroll: function sendScroll(target) {
+  /*
+  * DOM scroll handler - if we receive this, content or the dom scrolled
+  * content without going through the apz. This can happen in a lot of
+  * cases, keyboard shortcuts, scroll wheel, or content script. Messages
+  * chrome via a sync call which messages the apz about the update.
+  */
+  notifyChromeAboutContentScroll: function (target) {
     let isRoot = false;
     if (target instanceof Ci.nsIDOMDocument) {
       var window = target.defaultView;
       var scrollOffset = this.getScrollOffset(window);
       var element = target.documentElement;
 
       if (target == content.document) {
         if (this._scrollOffset.x == scrollOffset.x && this._scrollOffset.y == scrollOffset.y) {
@@ -708,21 +716,23 @@ let ContentScroll =  {
       var scrollOffset = this.getScrollOffsetForElement(target);
       var element = target;
     }
 
     let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
     let presShellId = {};
     utils.getPresShellId(presShellId);
     let viewId = utils.getViewId(element);
-
-    sendAsyncMessage("scroll", { presShellId: presShellId.value,
-                                 viewId: viewId,
-                                 scrollOffset: scrollOffset,
-                                 isRoot: isRoot });
+    // Must be synchronous to prevent redraw getting out of sync from
+    // composition.
+    sendSyncMessage("Browser:ContentScroll",
+      { presShellId: presShellId.value,
+        viewId: viewId,
+        scrollOffset: scrollOffset,
+        isRoot: isRoot });
   }
 };
 
 ContentScroll.init();
 
 let ContentActive =  {
   init: function() {
     addMessageListener("Content:Activate", this);
--- a/browser/metro/base/content/browser-scripts.js
+++ b/browser/metro/base/content/browser-scripts.js
@@ -11,16 +11,19 @@ Cu.import("resource://gre/modules/Servic
  */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                   "resource://gre/modules/Downloads.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                   "resource://gre/modules/FormHistory.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
                                   "resource://gre/modules/PageThumbs.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
--- a/browser/themes/shared/devtools/app-manager/projects.css
+++ b/browser/themes/shared/devtools/app-manager/projects.css
@@ -453,16 +453,25 @@ strong {
   flex-direction: column;
   flex-grow: 1;
   background-color: #E1E1E1;
 }
 
 .manifest-editor > h2 {
   font-size: 18px;
   margin: 1em 30px;
+  display: none;
+}
+
+[type="packaged"] > .editable {
+  display: block;
+}
+
+[type="hosted"] > .viewable {
+  display: block;
 }
 
 .variables-view {
   flex-grow: 1;
   border: 0;
   border-top: 5px solid #C9C9C9;
 }
 
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -3,17 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* According to:
  * https://bugzilla.mozilla.org/show_bug.cgi?id=715472#c17
  */
 .theme-body {
   background: #131c26;
-  color: #8fa1b2
+  color: #8fa1b2;
 }
 
 .theme-twisty {
   cursor: pointer;
   width: 14px;
   height: 14px;
   background-repeat: no-repeat;
   background-image: url("chrome://browser/skin/devtools/controls.png");
@@ -42,71 +42,133 @@
 .theme-checkbox[checked] {
   background-position: -42px 0;
 }
 
 .theme-selected {
   background: #26394D;
 }
 
-.theme-bg-darker {
+.theme-bg-darker,
+.cm-s-mozilla .CodeMirror-gutters {
   background-color: rgba(0,0,0,0.5);
 }
 
 .theme-bg-contrast { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
-.theme-link { /* blue */
+.theme-link,
+.cm-s-mozilla .cm-link { /* blue */
   color: #3689b2;
 }
 
-.theme-comment { /* grey */
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr { /* grey */
   color: #5c6773;
 }
 
 .theme-gutter {
   background-color: #0f171f;
   color: #667380;
   border-color: #303b47;
 }
 
 .theme-separator { /* grey */
   border-color: #303b47;
 }
 
-.theme-fg-color1 { /* green */
+.theme-fg-color1,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
   color: #5c9966;
 }
 
-.theme-fg-color2 { /* blue */
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-variable,
+.cm-s-mozilla .cm-def,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-property,
+.cm-s-mozilla .cm-qualifier { /* blue */
   color: #3689b2;
 }
 
-.theme-fg-color3 { /* pink/lavender */
+.theme-fg-color3,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header { /* pink/lavender */
   color: #a673bf;
 }
 
-.theme-fg-color4 { /* purple/violet */
+.theme-fg-color4,
+.cm-s-mozilla .cm-comment { /* purple/violet */
   color: #6270b2;
 }
 
-.theme-fg-color5 { /* Yellow */
+.theme-fg-color5,
+.cm-s-mozilla .cm-bracket,
+.cm-s-mozilla .cm-atom,
+.cm-s-mozilla .cm-keyword { /* Yellow */
   color: #a18650;
 }
 
-.theme-fg-color6 { /* Orange */
+.theme-fg-color6,
+.cm-s-mozilla .cm-string  { /* Orange */
   color: #b26b47;
 }
 
-.theme-fg-color7 { /* Red */
+.theme-fg-color7,
+.cm-s-mozilla .CodeMirror-nonmatchingbracket,
+.cm-s-mozilla .cm-string-2,
+.cm-s-mozilla .cm-error { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
 }
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror { /* Inherit platform specific font sizing and styles */
+  font-family: inherit;
+  font-size: inherit;
+  background: transparent;
+}
+
+.CodeMirror pre,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special,
+.cm-s-mozilla .cm-number { /* theme-body color */
+  color: #8fa1b2;
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+  border-left: solid 1px #fff;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+  background: rgb(185, 215, 253);
+}
+
+.dcm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+  background: rgb(176, 176, 176);
+}
+
+.CodeMirror-activeline-background { /* selected color with alpha */
+  background: rgba(185, 215, 253, .05);
+}
+
+.cm-s-markup-view pre {
+  line-height: 1.4em;
+  min-height: 1.4em;
+}
--- a/browser/themes/shared/devtools/highlighter.inc.css
+++ b/browser/themes/shared/devtools/highlighter.inc.css
@@ -15,117 +15,94 @@
 .highlighter-outline[locked]  {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.3);
   outline-color: rgba(255,255,255,0.7);
 }
 
 /* Highlighter - Node Infobar */
 
 .highlighter-nodeinfobar {
-  color: hsl(200, 100%, 65%);
-  border: 1px solid hsla(210, 19%, 63%, .5);
+  color: hsl(216,33%,97%);
   border-radius: 3px;
-  background: linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box;
+  background: hsl(214,13%,24%) no-repeat padding-box;
 }
 
 /* Highlighter - Node Infobar - text */
 
 .highlighter-nodeinfobar-text {
   /* 100% - size of the buttons and margins */
   max-width: calc(100% - 2 * (26px + 6px));
   padding-bottom: 1px;
 }
 
 html|*.highlighter-nodeinfobar-tagname {
-  color: white;
+  color: hsl(285,100%,75%);
 }
 
 html|*.highlighter-nodeinfobar-id {
-  color: hsl(90, 79%, 52%);
+  color: hsl(103,46%,54%);
 }
 
+html|*.highlighter-nodeinfobar-classes,
 html|*.highlighter-nodeinfobar-pseudo-classes {
-  color: hsl(20, 100%, 70%);
+  color: hsl(200,74%,57%);
 }
 
 /* Highlighter - Node Infobar - buttons */
 
 .highlighter-nodeinfobar-button {
   -moz-appearance: none;
-  border: 0 solid hsla(210,8%,5%,.45);
   padding: 0;
   width: 26px;
   min-height: 26px;
 %ifndef XP_LINUX
   background-color: transparent;
 %endif
 }
 
 .highlighter-nodeinfobar-inspectbutton {
-  -moz-border-end-width: 1px;
-  box-shadow: 1px 0 0 hsla(210,16%,76%,.15), -1px 0 0 hsla(210,16%,76%,.15) inset;
   -moz-margin-end: 6px;
   list-style-image: url("chrome://browser/skin/devtools/inspect-button.png");
   -moz-image-region: rect(0px 16px 16px 0px);
 }
 
-.highlighter-nodeinfobar-inspectbutton:-moz-locale-dir(rtl) {
-  box-shadow: -1px 0 0 hsla(210,16%,76%,.15), 1px 0 0 hsla(210,16%,76%,.15) inset;
-}
-
 .highlighter-nodeinfobar-inspectbutton:active:hover,
 .highlighter-nodeinfobar-container:not([locked]) >  .highlighter-nodeinfobar >  .highlighter-nodeinfobar-inspectbutton {
   -moz-image-region: rect(0px 32px 16px 16px);
 }
 
 .highlighter-nodeinfobar-menu {
-  -moz-border-start-width: 1px;
-  box-shadow: -1px 0 0 hsla(210,16%,76%,.15), 1px 0 0 hsla(210,16%,76%,.15) inset;
   -moz-margin-start: 6px;
 }
 
-.highlighter-nodeinfobar-menu:-moz-locale-dir(rtl) {
-  box-shadow: 1px 0 0 hsla(210,16%,76%,.15), -1px 0 0 hsla(210,16%,76%,.15) inset;
-}
-
 .highlighter-nodeinfobar-menu > .toolbarbutton-menu-dropmarker {
   -moz-appearance: none !important;
   list-style-image: url("chrome://browser/skin/devtools/dropmarker.png");
   -moz-box-align: center;
   -moz-margin-start: -1px;
 }
 
 /* Highlighter - Node Infobar - box & arrow */
 
 .highlighter-nodeinfobar-arrow {
   width: 14px;
   height: 14px;
   -moz-margin-start: calc(50% - 7px);
   transform: rotate(-45deg);
-  border: 1px solid transparent;
   background-clip: padding-box;
   background-repeat: no-repeat;
 }
 
 .highlighter-nodeinfobar-arrow-top {
   margin-bottom: -8px;
   margin-top: 8px;
-  border-right-color: hsla(210, 19%, 63%, .5);
-  border-top-color: hsla(210, 19%, 63%, .5);
-  background-image: linear-gradient(to top right, transparent 50%, hsl(209, 18%, 30%) 50%);
+  background-image: linear-gradient(to top right, transparent 50%, hsl(210,2%,22%) 50%);
 }
 
 .highlighter-nodeinfobar-arrow-bottom {
   margin-top: -8px;
   margin-bottom: 8px;
-  border-left-color: hsla(210, 19%, 63%, .5);
-  border-bottom-color: hsla(210, 19%, 63%, .5);
-  background-image: linear-gradient(to bottom left, transparent 50%, hsl(210, 24%, 16%) 50%);
-}
-
-.highlighter-nodeinfobar-container[position="top"] > .highlighter-nodeinfobar,
-.highlighter-nodeinfobar-container[position="overlap"] > .highlighter-nodeinfobar {
-  box-shadow: 0 1px 0 hsla(0, 0%, 100%, .1) inset;
+  background-image: linear-gradient(to bottom left, transparent 50%, hsl(210,2%,22%) 50%);
 }
 
 .highlighter-nodeinfobar-container[hide-arrow] > .highlighter-nodeinfobar {
   margin: 7px 0;
 }
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -42,71 +42,133 @@
 .theme-checkbox[checked] {
   background-position: -14px 0;
 }
 
 .theme-selected {
   background-color: #CCC;
 }
 
-.theme-bg-darker {
+.theme-bg-darker,
+.cm-s-mozilla .CodeMirror-gutters {
   background: #EFEFEF;
 }
 
 .theme-bg-contrast { /* contrast bg color to attract attention on a container */
   background: #a18650;
 }
 
-.theme-link { /* blue */
+.theme-link,
+.cm-s-mozilla .cm-link { /* blue */
   color: hsl(208,56%,40%);
 }
 
-.theme-comment { /* grey */
+.theme-comment,
+.cm-s-mozilla .cm-meta,
+.cm-s-mozilla .cm-hr { /* grey */
   color: hsl(90,2%,46%);
 }
 
 .theme-gutter {
   background-color: hsl(0,0%,90%);
   color: #667380;
   border-color: hsl(0,0%,65%);
 }
 
 .theme-separator { /* grey */
   border-color: #cddae5;
 }
 
-.theme-fg-color1 { /* green */
+.theme-fg-color1,
+.cm-s-mozilla .cm-variable-2,
+.cm-s-mozilla .cm-quote,
+.cm-s-mozilla .CodeMirror-matchingbracket { /* green */
   color: hsl(72,100%,27%);
 }
 
-.theme-fg-color2 { /* blue */
+.theme-fg-color2,
+.cm-s-mozilla .cm-attribute,
+.cm-s-mozilla .cm-builtin,
+.cm-s-mozilla .cm-variable,
+.cm-s-mozilla .cm-def,
+.cm-s-mozilla .cm-variable-3,
+.cm-s-mozilla .cm-property,
+.cm-s-mozilla .cm-qualifier { /* blue */
   color: hsl(208,56%,40%);
 }
 
-.theme-fg-color3 { /* dark blue */
+.theme-fg-color3,
+.cm-s-mozilla .cm-tag,
+.cm-s-mozilla .cm-header { /* dark blue */
   color: hsl(208,81%,21%)
 }
 
-.theme-fg-color4 { /* Orange */
+.theme-fg-color4,
+.cm-s-mozilla .cm-comment { /* Orange */
   color: hsl(24,85%,39%);
 }
 
-.theme-fg-color5 { /* Yellow */
+.theme-fg-color5,
+.cm-s-mozilla .cm-bracket,
+.cm-s-mozilla .cm-keyword,
+.cm-s-mozilla .cm-atom { /* Yellow */
   color: #a18650;
 }
 
-.theme-fg-color6 { /* Orange */
+.theme-fg-color6,
+.cm-s-mozilla .cm-string { /* Orange */
   color: hsl(24,85%,39%);
 }
 
-.theme-fg-color7 { /* Red */
+.theme-fg-color7,
+.cm-s-mozilla .CodeMirror-nonmatchingbracket,
+.cm-s-mozilla .cm-string-2,
+.cm-s-mozilla .cm-error { /* Red */
   color: #bf5656;
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .ruleview-colorswatch,
 .computedview-colorswatch,
 .markupview-colorswatch {
   box-shadow: 0 0 0 1px #EFEFEF;
 }
+
+/* CodeMirror specific styles.
+ * Best effort to match the existing theme, some of the colors
+ * are duplicated here to prevent weirdness in the main theme. */
+
+.CodeMirror { /* Inherit platform specific font sizing and styles */
+  font-family: inherit;
+  font-size: inherit;
+  background: transparent;
+}
+
+.CodeMirror pre,
+.cm-s-mozilla .cm-operator,
+.cm-s-mozilla .cm-special,
+.cm-s-mozilla .cm-number { /* theme-body color */
+  color: black;
+}
+
+.cm-s-mozilla .CodeMirror-lines .CodeMirror-cursor {
+  border-left: solid 1px black;
+}
+
+.cm-s-mozilla.CodeMirror-focused .CodeMirror-selected { /* selected text (focused) */
+  background: rgb(185, 215, 253);
+}
+
+.cm-s-mozilla .CodeMirror-selected { /* selected text (unfocused) */
+  background: rgb(176, 176, 176);
+}
+
+.CodeMirror-activeline-background { /* selected color with alpha */
+  background: rgba(185, 215, 253, .4);
+}
+
+.cm-s-markup-view pre {
+  line-height: 1.4em;
+  min-height: 1.4em;
+}
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -167,17 +167,16 @@ abstract public class GeckoApp
     public List<GeckoAppShell.AppStateListener> mAppStateListeners;
     private static GeckoApp sAppContext;
     protected MenuPanel mMenuPanel;
     protected Menu mMenu;
     protected GeckoProfile mProfile;
     public static int mOrientation;
     protected boolean mIsRestoringActivity;
     private String mCurrentResponse = "";
-    public static boolean sIsUsingCustomProfile = false;
 
     private ContactService mContactService;
     private PromptService mPromptService;
     private TextSelection mTextSelection;
 
     protected DoorHangerPopup mDoorHangerPopup;
     protected FormAssistPopup mFormAssistPopup;
     protected TabsPanel mTabsPanel;
@@ -1170,17 +1169,17 @@ abstract public class GeckoApp
                     if (m.find()) {
                         profilePath =  m.group(1);
                     }
                     if (profileName == null) {
                         profileName = getDefaultProfileName();
                         if (profileName == null)
                             profileName = "default";
                     }
-                    GeckoApp.sIsUsingCustomProfile = true;
+                    GeckoProfile.sIsUsingCustomProfile = true;
                 }
 
                 if (profileName != null || profilePath != null) {
                     mProfile = GeckoProfile.get(this, profileName, profilePath);
                 }
             }
         }
 
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -30,16 +30,17 @@ public final class GeckoProfile {
 
     private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
     private static String sDefaultProfileName = null;
 
     private final Context mContext;
     private final String mName;
     private File mMozDir;
     private File mDir;
+    public static boolean sIsUsingCustomProfile = false;
 
     // Constants to cache whether or not a profile is "locked".
     private enum LockState {
         LOCKED,
         UNLOCKED,
         UNDEFINED
     };
     // Caches whether or not a profile is "locked". Only used by the guest profile to determine if it should
@@ -55,31 +56,37 @@ public final class GeckoProfile {
     static private INIParser getProfilesINI(Context context) {
       File filesDir = context.getFilesDir();
       File mozillaDir = new File(filesDir, "mozilla");
       File profilesIni = new File(mozillaDir, "profiles.ini");
       return new INIParser(profilesIni);
     }
 
     public static GeckoProfile get(Context context) {
-        if (context instanceof GeckoApp) {
+        boolean isGeckoApp = false;
+        try {
+            isGeckoApp = context instanceof GeckoApp;
+        } catch (NoClassDefFoundError ex) {}
+        
+
+        if (isGeckoApp) {
             // Check for a cached profile on this context already
             // TODO: We should not be caching profile information on the Activity context
             if (((GeckoApp)context).mProfile != null) {
                 return ((GeckoApp)context).mProfile;
             }
         }
 
         // If the guest profile exists and is locked, return it
         GeckoProfile guest = GeckoProfile.getGuestProfile(context);
         if (guest != null && guest.locked()) {
             return guest;
         }
 
-        if (context instanceof GeckoApp) {
+        if (isGeckoApp) {
             // Otherwise, get the default profile for the Activity
             return get(context, ((GeckoApp)context).getDefaultProfileName());
         }
 
         return get(context, "");
     }
 
     public static GeckoProfile get(Context context, String profileName) {
--- a/mobile/android/base/GeckoThread.java
+++ b/mobile/android/base/GeckoThread.java
@@ -142,17 +142,17 @@ public class GeckoThread extends Thread 
             if (GeckoAppShell.getGeckoInterface().getProfile().inGuestMode()) {
                 try {
                     profile = " -profile " + GeckoAppShell.getGeckoInterface().getProfile().getDir().getCanonicalPath();
                 } catch (IOException ioe) { Log.e(LOGTAG, "error getting guest profile path", ioe); }
 
                 if (args == null || !args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
                     guest = " " + BrowserApp.GUEST_BROWSING_ARG;
                 }
-            } else if (!GeckoApp.sIsUsingCustomProfile) {
+            } else if (!GeckoProfile.sIsUsingCustomProfile) {
                 // If nothing was passed in in the intent, force Gecko to use the default profile for
                 // for this activity
                 profile = " -P " + GeckoAppShell.getGeckoInterface().getProfile().getName();
             }
         }
 
         return (args != null ? args : "") + profile + guest;
     }
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -41,30 +41,35 @@ public class GeckoView extends LayerView
         boolean doInit = a.getBoolean(R.styleable.GeckoView_doinit, true);
         a.recycle();
 
         if (!doInit)
             return;
 
         // If running outside of a GeckoActivity (eg, from a library project),
         // load the native code and disable content providers
-        if (!(context instanceof GeckoActivity)) {
+        boolean isGeckoActivity = false;
+        try {
+            isGeckoActivity = context instanceof GeckoActivity;
+        } catch (NoClassDefFoundError ex) {}
+
+        if (!isGeckoActivity) {
             // Set the GeckoInterface if the context is an activity and the GeckoInterface
             // has not already been set
             if (context instanceof Activity && getGeckoInterface() == null) {
                 setGeckoInterface(new BaseGeckoInterface(context));
             }
 
             Clipboard.init(context);
             HardwareUtils.init(context);
             GeckoNetworkManager.getInstance().init(context);
 
             GeckoLoader.loadMozGlue();
             BrowserDB.setEnableContentProviders(false);
-        }
+         }
 
         if (url != null) {
             GeckoThread.setUri(url);
             GeckoThread.setAction(Intent.ACTION_VIEW);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(url));
         }
         GeckoAppShell.setContextGetter(this);
         if (context instanceof Activity) {
--- a/mobile/android/base/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/gfx/GeckoLayerClient.java
@@ -891,26 +891,30 @@ public class GeckoLayerClient implements
     }
 
     public interface OnMetricsChangedListener {
         public void onMetricsChanged(ImmutableViewportMetrics viewport);
         public void onPanZoomStopped();
     }
 
     private void setShadowVisibility() {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (BrowserApp.mBrowserToolbar == null) {
-                    return;
+        try {
+            if (BrowserApp.mBrowserToolbar == null) // this will throw if we don't have BrowserApp
+                return;
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (BrowserApp.mBrowserToolbar == null) {
+                        return;
+                    }
+                    ImmutableViewportMetrics m = mViewportMetrics;
+                    BrowserApp.mBrowserToolbar.setShadowVisibility(m.viewportRectTop >= m.pageRectTop);
                 }
-                ImmutableViewportMetrics m = mViewportMetrics;
-                BrowserApp.mBrowserToolbar.setShadowVisibility(m.viewportRectTop >= m.pageRectTop);
-            }
-        });
+            });
+        } catch (NoClassDefFoundError ex) {}
     }
 
     /** Implementation of PanZoomTarget */
     @Override
     public void forceRedraw(DisplayPortMetrics displayPort) {
         mForceRedraw = true;
         if (mGeckoIsReady) {
             geometryChanged(displayPort);
--- a/mobile/android/modules/HelperApps.jsm
+++ b/mobile/android/modules/HelperApps.jsm
@@ -70,17 +70,21 @@ var HelperApps =  {
     return results;
   },
 
   getAppsForUri: function getAppsForUri(uri, flags = { filterHttp: true }) {
     flags.filterHttp = "filterHttp" in flags ? flags.filterHttp : true;
 
     // Query for apps that can/can't handle the mimetype
     let msg = this._getMessage("Intent:GetHandlers", uri, flags);
-    let apps = this._parseApps(this._sendMessage(msg).apps);
+    let data = this._sendMessage(msg);
+    if (!data)
+      return [];
+
+    let apps = this._parseApps(data.apps);
 
     if (flags.filterHttp) {
       apps = apps.filter(function(app) {
         return app.name && !this.defaultHttpHandlers[app.name];
       }, this);
     }
 
     return apps;
@@ -127,13 +131,12 @@ var HelperApps =  {
       packageName: app.packageName,
       className: app.activityName
     });
 
     this._sendMessage(msg);
   },
 
   _sendMessage: function(msg) {
-    Services.console.logStringMessage("Sending: " + JSON.stringify(msg));
     let res = Services.androidBridge.handleGeckoMessage(JSON.stringify(msg));
     return JSON.parse(res);
   },
 };
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -819,30 +819,35 @@ Connection::internalClose()
 #ifdef PR_LOGGING
   nsAutoCString leafName(":memory");
   if (mDatabaseFile)
       (void)mDatabaseFile->GetNativeLeafName(leafName);
   PR_LOG(gStorageLog, PR_LOG_NOTICE, ("Closing connection to '%s'",
                                       leafName.get()));
 #endif
 
+  // Set the property to null before closing the connection, otherwise the other
+  // functions in the module may try to use the connection after it is closed.
+  sqlite3 *dbConn = mDBConn;
+  mDBConn = nullptr;
+
   // At this stage, we may still have statements that need to be
   // finalized. Attempt to close the database connection. This will
   // always disconnect any virtual tables and cleanly finalize their
   // internal statements. Once this is done, closing may fail due to
   // unfinalized client statements, in which case we need to finalize
   // these statements and close again.
 
-  int srv = sqlite3_close(mDBConn);
+  int srv = sqlite3_close(dbConn);
 
   if (srv == SQLITE_BUSY) {
     // We still have non-finalized statements. Finalize them.
 
     sqlite3_stmt *stmt = nullptr;
-    while ((stmt = ::sqlite3_next_stmt(mDBConn, stmt))) {
+    while ((stmt = ::sqlite3_next_stmt(dbConn, stmt))) {
       PR_LOG(gStorageLog, PR_LOG_NOTICE,
              ("Auto-finalizing SQL statement '%s' (%x)",
               ::sqlite3_sql(stmt),
               stmt));
 
 #ifdef DEBUG
       char *msg = ::PR_smprintf("SQL statement '%s' (%x) should have been finalized before closing the connection",
                                 ::sqlite3_sql(stmt),
@@ -866,27 +871,26 @@ Connection::internalClose()
       // Ensure that the loop continues properly, whether closing has succeeded
       // or not.
       if (srv == SQLITE_OK) {
         stmt = nullptr;
       }
     }
 
     // Now that all statements have been finalized, we
-    // shoudl be able to close.
-    srv = ::sqlite3_close(mDBConn);
+    // should be able to close.
+    srv = ::sqlite3_close(dbConn);
 
   }
 
   if (srv != SQLITE_OK) {
     MOZ_ASSERT(srv == SQLITE_OK,
                "sqlite3_close failed. There are probably outstanding statements that are listed above!");
   }
 
-  mDBConn = nullptr;
   return convertResultCode(srv);
 }
 
 nsCString
 Connection::getFilename()
 {
   nsCString leafname(":memory:");
   if (mDatabaseFile) {
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -180,17 +180,17 @@ class MochitestRunner(MozbuildObject):
         options.logcat_dir = self.mochitest_dir
         options.httpdPath = self.mochitest_dir
         options.xrePath = xre_path
         return mochitest.run_remote_mochitests(parser, options)
 
     def run_desktop_test(self, suite=None, test_file=None, debugger=None,
         debugger_args=None, shuffle=False, keep_open=False, rerun_failures=False,
         no_autorun=False, repeat=0, run_until_failure=False, slow=False,
-        chunk_by_dir=0, total_chunks=None, this_chunk=None):
+        chunk_by_dir=0, total_chunks=None, this_chunk=None, jsdebugger=False):
         """Runs a mochitest.
 
         test_file is a path to a test file. It can be a relative path from the
         top source directory, an absolute filename, or a directory containing
         test files.
 
         suite is the type of mochitest to run. It can be one of ('plain',
         'chrome', 'browser', 'metro', 'a11y').
@@ -280,16 +280,17 @@ class MochitestRunner(MozbuildObject):
         options.runUntilFailure = run_until_failure
         options.runSlower = slow
         options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
         options.extraProfileFiles.append(os.path.join(self.distdir, 'plugins'))
         options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
         options.chunkByDir = chunk_by_dir
         options.totalChunks = total_chunks
         options.thisChunk = this_chunk
+        options.jsdebugger = jsdebugger
 
         options.failureFile = failure_file_path
 
         if test_path:
             test_root = runner.getTestRoot(options)
             test_root_file = mozpack.path.join(self.mochitest_dir, test_root, test_path)
             if not os.path.exists(test_root_file):
                 print('Specified test path does not exist: %s' % test_root_file)
@@ -399,16 +400,20 @@ def MochitestCommand(func):
     chunk_total = CommandArgument('--total-chunks', type=int,
         help='Total number of chunks to split tests into.')
     func = chunk_total(func)
 
     this_chunk = CommandArgument('--this-chunk', type=int,
         help='If running tests by chunks, the number of the chunk to run.')
     func = this_chunk(func)
 
+    jsdebugger = CommandArgument('--jsdebugger', action='store_true',
+        help='Start the browser JS debugger before running the test. Implies --no-autorun.')
+    func = jsdebugger(func)
+
     path = CommandArgument('test_file', default=None, nargs='?',
         metavar='TEST',
         help='Test to run. Can be specified as a single file, a ' \
             'directory, or omitted. If omitted, the entire test suite is ' \
             'executed.')
     func = path(func)
 
     return func
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -315,16 +315,22 @@ class MochitestOptions(optparse.OptionPa
         [["--setpref"],
         { "action": "append",
           "type": "string",
           "default": [],
           "dest": "extraPrefs",
           "metavar": "PREF=VALUE",
           "help": "defines an extra user preference",
         }],
+        [["--jsdebugger"],
+        { "action": "store_true",
+          "default": False,
+          "dest": "jsdebugger",
+          "help": "open the browser debugger",
+        }],
     ]
 
     def __init__(self, **kwargs):
 
         optparse.OptionParser.__init__(self, **kwargs)
         for option, value in self.mochitest_options:
             self.add_option(*option, **value)
         addCommonOptions(self)
@@ -408,16 +414,25 @@ class MochitestOptions(optparse.OptionPa
             options.runOnlyTests = None
 
         if options.manifestFile and options.testManifest:
             self.error("Unable to support both --manifest and --test-manifest/--run-only-tests at the same time")
 
         if options.webapprtContent and options.webapprtChrome:
             self.error("Only one of --webapprt-content and --webapprt-chrome may be given.")
 
+        if options.jsdebugger:
+            options.extraPrefs += [
+                "devtools.debugger.remote-enabled=true",
+                "devtools.debugger.chrome-enabled=true",
+                "devtools.chrome.enabled=true",
+                "devtools.debugger.prompt-connection=false"
+            ]
+            options.autorun = False
+
         # Try to guess the testing modules directory.
         # This somewhat grotesque hack allows the buildbot machines to find the
         # modules directory without having to configure the buildbot hosts. This
         # code should never be executed in local runs because the build system
         # should always set the flag that populates this variable. If buildbot ever
         # passes this argument, this code can be deleted.
         if options.testingModulesDir is None:
             possible = os.path.join(os.getcwd(), os.path.pardir, 'modules')
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -970,16 +970,19 @@ class Mochitest(MochitestUtilsMixin):
     if options.webapprtContent:
       options.browserArgs.extend(('-test-mode', testURL))
       testURL = None
 
     if options.immersiveMode:
       options.browserArgs.extend(('-firefoxpath', options.app))
       options.app = self.immersiveHelperPath
 
+    if options.jsdebugger:
+      options.browserArgs.extend(['-jsdebugger'])
+
     # Remove the leak detection file so it can't "leak" to the tests run.
     # The file is not there if leak logging was not enabled in the application build.
     if os.path.exists(self.leak_report_file):
       os.remove(self.leak_report_file)
 
     # then again to actually run mochitest
     if options.timeout:
       timeout = options.timeout + 30
--- a/toolkit/components/jsdownloads/src/DownloadImport.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -101,17 +101,17 @@ this.DownloadImport.prototype = {
             let maxBytes = row.getResultByName("maxBytes");
             let mimeType = row.getResultByName("mimeType");
             let preferredApplication = row.getResultByName("preferredApplication");
             let preferredAction = row.getResultByName("preferredAction");
             let entityID = row.getResultByName("entityID");
 
             let autoResume = false;
             try {
-              autoResume = row.getResultByName("autoResume");
+              autoResume = (row.getResultByName("autoResume") == 1);
             } catch (ex) {
               // autoResume wasn't present in schema version 7
             }
 
             if (!source) {
               throw new Error("Attempted to import a row with an empty " +
                               "source column.");
             }
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -250,130 +250,130 @@ this.DownloadIntegration = {
     // presence of a ".part" file are only retained in the browser history.
     return aDownload.hasPartialData || !aDownload.stopped;
   },
 
   /**
    * Returns the system downloads directory asynchronously.
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getSystemDownloadsDirectory: function DI_getSystemDownloadsDirectory() {
     return Task.spawn(function() {
       if (this._downloadsDirectory) {
         // This explicitly makes this function a generator for Task.jsm. We
         // need this because calls to the "yield" operator below may be
         // preprocessed out on some platforms.
         yield undefined;
         throw new Task.Result(this._downloadsDirectory);
       }
 
-      let directory = null;
+      let directoryPath = null;
 #ifdef XP_MACOSX
-      directory = this._getDirectory("DfltDwnld");
+      directoryPath = this._getDirectory("DfltDwnld");
 #elifdef XP_WIN
       // For XP/2K, use My Documents/Downloads. Other version uses
       // the default Downloads directory.
       let version = parseFloat(Services.sysinfo.getProperty("version"));
       if (version < 6) {
-        directory = yield this._createDownloadsDirectory("Pers");
+        directoryPath = yield this._createDownloadsDirectory("Pers");
       } else {
-        directory = this._getDirectory("DfltDwnld");
+        directoryPath = this._getDirectory("DfltDwnld");
       }
 #elifdef XP_UNIX
 #ifdef ANDROID
       // Android doesn't have a $HOME directory, and by default we only have
       // write access to /data/data/org.mozilla.{$APP} and /sdcard
-      let directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
+      directoryPath = gEnvironment.get("DOWNLOADS_DIRECTORY");
       if (!directoryPath) {
         throw new Components.Exception("DOWNLOADS_DIRECTORY is not set.",
                                        Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH);
       }
-      directory = new FileUtils.File(directoryPath);
 #else
       // For Linux, use XDG download dir, with a fallback to Home/Downloads
       // if the XDG user dirs are disabled.
       try {
-        directory = this._getDirectory("DfltDwnld");
+        directoryPath = this._getDirectory("DfltDwnld");
       } catch(e) {
-        directory = yield this._createDownloadsDirectory("Home");
+        directoryPath = yield this._createDownloadsDirectory("Home");
       }
 #endif
 #else
-      directory = yield this._createDownloadsDirectory("Home");
+      directoryPath = yield this._createDownloadsDirectory("Home");
 #endif
-      this._downloadsDirectory = directory;
+      this._downloadsDirectory = directoryPath;
       throw new Task.Result(this._downloadsDirectory);
     }.bind(this));
   },
   _downloadsDirectory: null,
 
   /**
    * Returns the user downloads directory asynchronously.
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getPreferredDownloadsDirectory: function DI_getPreferredDownloadsDirectory() {
     return Task.spawn(function() {
-      let directory = null;
+      let directoryPath = null;
       let prefValue = 1;
 
       try {
         prefValue = Services.prefs.getIntPref("browser.download.folderList");
       } catch(e) {}
 
       switch(prefValue) {
         case 0: // Desktop
-          directory = this._getDirectory("Desk");
+          directoryPath = this._getDirectory("Desk");
           break;
         case 1: // Downloads
-          directory = yield this.getSystemDownloadsDirectory();
+          directoryPath = yield this.getSystemDownloadsDirectory();
           break;
         case 2: // Custom
           try {
-            directory = Services.prefs.getComplexValue("browser.download.dir",
-                                                       Ci.nsIFile);
-            yield OS.File.makeDir(directory.path, { ignoreExisting: true });
+            let directory = Services.prefs.getComplexValue("browser.download.dir",
+                                                           Ci.nsIFile);
+            directoryPath = directory.path;
+            yield OS.File.makeDir(directoryPath, { ignoreExisting: true });
           } catch(ex) {
             // Either the preference isn't set or the directory cannot be created.
-            directory = yield this.getSystemDownloadsDirectory();
+            directoryPath = yield this.getSystemDownloadsDirectory();
           }
           break;
         default:
-          directory = yield this.getSystemDownloadsDirectory();
+          directoryPath = yield this.getSystemDownloadsDirectory();
       }
-      throw new Task.Result(directory);
+      throw new Task.Result(directoryPath);
     }.bind(this));
   },
 
   /**
    * Returns the temporary downloads directory asynchronously.
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getTemporaryDownloadsDirectory: function DI_getTemporaryDownloadsDirectory() {
     return Task.spawn(function() {
-      let directory = null;
+      let directoryPath = null;
 #ifdef XP_MACOSX
-      directory = yield this.getPreferredDownloadsDirectory();
+      directoryPath = yield this.getPreferredDownloadsDirectory();
 #elifdef ANDROID
-      directory = yield this.getSystemDownloadsDirectory();
+      directoryPath = yield this.getSystemDownloadsDirectory();
 #else
       // For Metro mode on Windows 8,  we want searchability for documents
       // that the user chose to open with an external application.
       if (this._isImmersiveProcess()) {
-        directory = yield this.getSystemDownloadsDirectory();
+        directoryPath = yield this.getSystemDownloadsDirectory();
       } else {
-        directory = this._getDirectory("TmpD");
+        directoryPath = this._getDirectory("TmpD");
       }
 #endif
-      throw new Task.Result(directory);
+      throw new Task.Result(directoryPath);
     }.bind(this));
   },
 
   /**
    * Checks to determine whether to block downloads for parental controls.
    *
    * aParam aDownload
    *        The download object.
@@ -644,41 +644,40 @@ this.DownloadIntegration = {
     return deferred;
   },
 
   /**
    * Calls the directory service, create a downloads directory and returns an
    * nsIFile for the downloads directory.
    *
    * @return {Promise}
-   * @resolves The nsIFile directory.
+   * @resolves The directory string path.
    */
   _createDownloadsDirectory: function DI_createDownloadsDirectory(aName) {
-    let directory = this._getDirectory(aName);
-
     // We read the name of the directory from the list of translated strings
     // that is kept by the UI helper module, even if this string is not strictly
     // displayed in the user interface.
-    directory.append(DownloadUIHelper.strings.downloadsFolder);
+    let directoryPath = OS.Path.join(this._getDirectory(aName),
+                                     DownloadUIHelper.strings.downloadsFolder);
 
     // Create the Downloads folder and ignore if it already exists.
-    return OS.File.makeDir(directory.path, { ignoreExisting: true }).
+    return OS.File.makeDir(directoryPath, { ignoreExisting: true }).
              then(function() {
-               return directory;
+               return directoryPath;
              });
   },
 
   /**
    * Calls the directory service and returns an nsIFile for the requested
    * location name.
    *
-   * @return The nsIFile directory.
+   * @return The directory string path.
    */
   _getDirectory: function DI_getDirectory(aName) {
-    return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile);
+    return Services.dirsvc.get(this.testMode ? "TmpD" : aName, Ci.nsIFile).path;
   },
 
   /**
    * Register the downloads interruption observers.
    *
    * @param aList
    *        The public or private downloads list.
    * @param aIsPrivate
--- a/toolkit/components/jsdownloads/src/Downloads.jsm
+++ b/toolkit/components/jsdownloads/src/Downloads.jsm
@@ -255,41 +255,41 @@ this.Downloads = {
    *   Vista and others:
    *     User downloads directory
    *   Linux:
    *     XDG user dir spec, with a fallback to Home/Downloads
    *   Android:
    *     standard downloads directory i.e. /sdcard
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getSystemDownloadsDirectory: function D_getSystemDownloadsDirectory() {
     return DownloadIntegration.getSystemDownloadsDirectory();
   },
 
   /**
    * Returns the preferred downloads directory based on the user preferences
    * in the current profile asynchronously.
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getPreferredDownloadsDirectory: function D_getPreferredDownloadsDirectory() {
     return DownloadIntegration.getPreferredDownloadsDirectory();
   },
 
   /**
    * Returns the temporary directory where downloads are placed before the
    * final location is chosen, or while the document is opened temporarily
    * with an external application. This may or may not be the system temporary
    * directory, based on the platform asynchronously.
    *
    * @return {Promise}
-   * @resolves The nsIFile of downloads directory.
+   * @resolves The downloads directory string path.
    */
   getTemporaryDownloadsDirectory: function D_getTemporaryDownloadsDirectory() {
     return DownloadIntegration.getTemporaryDownloadsDirectory();
   },
 
   /**
    * Constructor for a DownloadError object.  When you catch an exception during
    * a download, you can use this to verify if "ex instanceof Downloads.Error",
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -1620,19 +1620,20 @@ add_task(function test_platform_integrat
 
   for (let isPrivate of [false, true]) {
     DownloadIntegration.downloadDoneCalled = false;
 
     // Some platform specific operations only operate on files outside the
     // temporary directory or in the Downloads directory (such as setting
     // the Windows searchable attribute, and the Mac Downloads icon bouncing),
     // so use the system Downloads directory for the target file.
-    let targetFile = yield DownloadIntegration.getSystemDownloadsDirectory();
-    targetFile = targetFile.clone();
-    targetFile.append("test" + (Math.floor(Math.random() * 1000000)));
+    let targetFilePath = yield DownloadIntegration.getSystemDownloadsDirectory();
+    targetFilePath = OS.Path.join(targetFilePath,
+                                  "test" + (Math.floor(Math.random() * 1000000)));
+    let targetFile = new FileUtils.File(targetFilePath);
     downloadFiles.push(targetFile);
 
     let download;
     if (gUseLegacySaver) {
       download = yield promiseStartLegacyDownload(httpUrl("source.txt"),
                                                   { targetFile: targetFile });
     }
     else {
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -461,16 +461,41 @@ function promiseStartExternalHelperAppSe
       },
     }, null);
   }.bind(this)).then(null, do_report_unexpected_exception);
 
   return deferred.promise;
 }
 
 /**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ *        The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+  if (!aDownload.stopped) {
+    // The download is in progress, wait for the current attempt to finish and
+    // report any errors that may occur.
+    return aDownload.start();
+  }
+
+  if (aDownload.succeeded) {
+    return Promise.resolve();
+  }
+
+  // The download failed or was canceled.
+  return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
  * Waits for a download to reach half of its progress, in case it has not
  * reached the expected progress already.
  *
  * @param aDownload
  *        The Download object to wait upon.
  *
  * @return {Promise}
  * @resolves When the download has reached half of its progress.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
@@ -0,0 +1,701 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadImport object.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+                                  "resource://gre/modules/DownloadImport.jsm");
+
+// Importable states
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// Non importable states
+const DOWNLOAD_FAILED = 2;
+const DOWNLOAD_CANCELED = 3;
+const DOWNLOAD_BLOCKED_PARENTAL = 6;
+const DOWNLOAD_SCANNING = 7;
+const DOWNLOAD_DIRTY = 8;
+const DOWNLOAD_BLOCKED_POLICY = 9;
+
+// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
+// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
+// We use this to test that the entityID is properly imported and the download
+// can be resumed from where it was paused.
+// For simplification purposes, the test requires that TEST_DATA_SHORT and
+// TEST_DATA_TAINTED have the same length.
+const TEST_DATA_REPLACEMENT = "-changed- ";
+const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
+                          TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
+const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
+
+// The length of the partial file that we'll write to disk as an existing
+// ongoing download.
+const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
+
+// The value of the "maxBytes" column stored in the DB about the downloads.
+// It's intentionally different than TEST_DATA_LENGTH to test that each value
+// is seen when expected.
+const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
+
+let gDownloadsRowToImport;
+let gDownloadsRowNonImportable;
+
+/**
+ * Creates a database with an empty moz_downloads table and leaves an
+ * open connection to it.
+ *
+ * @param aPath
+ *        String containing the path of the database file to be created.
+ * @param aSchemaVersion
+ *        Number with the version of the database schema to set.
+ *
+ * @return {Promise}
+ * @resolves The open connection to the database.
+ * @rejects If an error occurred during the database creation.
+ */
+function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
+  return Task.spawn(function () {
+    let connection = yield Sqlite.openConnection({ path: aPath });
+
+    yield connection.execute("CREATE TABLE moz_downloads ("
+                             + "id INTEGER PRIMARY KEY,"
+                             + "name TEXT,"
+                             + "source TEXT,"
+                             + "target TEXT,"
+                             + "tempPath TEXT,"
+                             + "startTime INTEGER,"
+                             + "endTime INTEGER,"
+                             + "state INTEGER,"
+                             + "referrer TEXT,"
+                             + "entityID TEXT,"
+                             + "currBytes INTEGER NOT NULL DEFAULT 0,"
+                             + "maxBytes INTEGER NOT NULL DEFAULT -1,"
+                             + "mimeType TEXT,"
+                             + "preferredApplication TEXT,"
+                             + "preferredAction INTEGER NOT NULL DEFAULT 0,"
+                             + "autoResume INTEGER NOT NULL DEFAULT 0,"
+                             + "guid TEXT)");
+
+    yield connection.setSchemaVersion(aSchemaVersion);
+
+    throw new Task.Result(connection);
+  });
+}
+
+/**
+ * Inserts a new entry in the database with the given columns' values.
+ *
+ * @param aConnection
+ *        The database connection.
+ * @param aDownloadRow
+ *        An object representing the values for each column of the row
+ *        being inserted.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects If there's an error inserting the row.
+ */
+function promiseInsertRow(aConnection, aDownloadRow) {
+  // We can't use the aDownloadRow obj directly in the execute statement
+  // because the obj bind code in Sqlite.jsm doesn't allow objects
+  // with extra properties beyond those being binded. So we might as well
+  // use an array as it is simpler.
+  let values = [
+    aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
+    aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
+    aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
+    aDownloadRow.mimeType, aDownloadRow.preferredApplication,
+    aDownloadRow.preferredAction, aDownloadRow.autoResume
+  ];
+
+  return aConnection.execute("INSERT INTO moz_downloads ("
+                            + "name, source, target, tempPath, startTime,"
+                            + "endTime, state, referrer, entityID, currBytes,"
+                            + "maxBytes, mimeType, preferredApplication,"
+                            + "preferredAction, autoResume, guid)"
+                            + "VALUES ("
+                            + "'', ?, ?, ?, ?, " //name,
+                            + "0, ?, ?, ?, 0, "  //endTime, currBytes
+                            + " ?, ?, ?, "       //
+                            + " ?, ?, '')",      //and guid are not imported
+                            values);
+}
+
+/**
+ * Retrieves the number of rows in the moz_downloads table of the
+ * database.
+ *
+ * @param aConnection
+ *        The database connection.
+ *
+ * @return {Promise}
+ * @resolves With the number of rows.
+ * @rejects Never.
+ */
+function promiseTableCount(aConnection) {
+  return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
+                    .then(res => res[0].getResultByName("COUNT(*)"))
+                    .then(null, Cu.reportError);
+}
+
+/**
+ * Briefly opens a network channel to a given URL to retrieve
+ * the entityID of this url, as generated by the network code.
+ *
+ * @param aUrl
+ *        The URL to retrieve the entityID.
+ *
+ * @return {Promise}
+ * @resolves The EntityID of the given URL.
+ * @rejects When there's a problem accessing the URL.
+ */
+function promiseEntityID(aUrl) {
+  let deferred = Promise.defer();
+  let entityID = "";
+  let channel = NetUtil.newChannel(NetUtil.newURI(aUrl));
+
+  channel.asyncOpen({
+    onStartRequest: function (aRequest) {
+      if (aRequest instanceof Ci.nsIResumableChannel) {
+        entityID = aRequest.entityID;
+      }
+      aRequest.cancel(Cr.NS_BINDING_ABORTED);
+    },
+
+    onStopRequest: function (aRequest, aContext, aStatusCode) {
+      if (aStatusCode == Cr.NS_BINDING_ABORTED) {
+        deferred.resolve(entityID);
+      } else {
+        deferred.reject("Unexpected status code received");
+      }
+    },
+
+    onDataAvailable: function () {}
+  }, null);
+
+  return deferred.promise;
+}
+
+/**
+ * Gets a file path to a temporary writeable download target, in the
+ * correct format as expected to be stored in the downloads database,
+ * which is file:///absolute/path/to/file
+ *
+ * @param aLeafName
+ *        A hint leaf name for the file.
+ *
+ * @return String The path to the download target.
+ */
+function getDownloadTarget(aLeafName) {
+  return NetUtil.newURI(getTempFile(aLeafName)).spec;
+}
+
+/**
+ * Generates a temporary partial file to use as an in-progress
+ * download. The file is written to disk with a part of the total expected
+ * download content pre-written.
+ *
+ * @param aLeafName
+ *        A hint leaf name for the file.
+ * @param aTainted
+ *        A boolean value. When true, the partial content of the file
+ *        will be different from the expected content of the original source
+ *        file. See the declaration of TEST_DATA_TAINTED for more information.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes, and returns a string with the path
+ *           to the generated file.
+ * @rejects If there's an error writing the file.
+ */
+function getPartialFile(aLeafName, aTainted = false) {
+  let tempDownload = getTempFile(aLeafName);
+  let partialContent = aTainted
+                     ? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
+                     : TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
+
+  return OS.File.writeAtomic(tempDownload.path, partialContent,
+                             { tmpPath: tempDownload.path + ".tmp",
+                               flush: true })
+                .then(() => tempDownload.path);
+}
+
+/**
+ * Generates a Date object to be used as the startTime for the download rows
+ * in the DB. A date that is obviously different from the current time is
+ * generated to make sure this stored data and a `new Date()` can't collide.
+ *
+ * @param aOffset
+ *        A offset from the base generated date is used to differentiate each
+ *        row in the database.
+ *
+ * @return A Date object.
+ */
+function getStartTime(aOffset) {
+  return new Date(1000000 + (aOffset * 10000));
+}
+
+/**
+ * Performs various checks on an imported Download object to make sure
+ * all properties are properly set as expected from the import procedure.
+ *
+ * @param aDownload
+ *        The Download object to be checked.
+ * @param aDownloadRow
+ *        An object that represents a row from the original database table,
+ *        with extra properties describing expected values that are not
+ *        explictly part of the database.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes
+ * @rejects Never
+ */
+function checkDownload(aDownload, aDownloadRow) {
+  return Task.spawn(function() {
+    do_check_eq(aDownload.source.url, aDownloadRow.source);
+    do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
+
+    do_check_eq(aDownload.target.path,
+                NetUtil.newURI(aDownloadRow.target)
+                       .QueryInterface(Ci.nsIFileURL).file.path);
+
+    do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
+
+    if (aDownloadRow.expectedResume) {
+      do_check_true(!aDownload.stopped || aDownload.succeeded);
+      yield promiseDownloadStopped(aDownload);
+
+      do_check_true(aDownload.succeeded);
+      do_check_eq(aDownload.progress, 100);
+      // If the download has resumed, a new startTime will be set.
+      // By calling toJSON we're also testing that startTime is a Date object.
+      do_check_neq(aDownload.startTime.toJSON(),
+                   aDownloadRow.startTime.toJSON());
+    } else {
+      do_check_false(aDownload.succeeded);
+      do_check_eq(aDownload.startTime.toJSON(),
+                  aDownloadRow.startTime.toJSON());
+    }
+
+    do_check_eq(aDownload.stopped, true);
+
+    let serializedSaver = aDownload.saver.toSerializable();
+    if (typeof(serializedSaver) == "object") {
+      do_check_eq(serializedSaver.type, "copy");
+    } else {
+      do_check_eq(serializedSaver, "copy");
+    }
+
+    if (aDownloadRow.entityID) {
+      do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
+    }
+
+    do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
+    do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
+
+    if (aDownloadRow.expectedContent) {
+      let fileToCheck = aDownloadRow.expectedResume
+                        ? aDownload.target.path
+                        : aDownload.target.partFilePath;
+      yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
+    }
+
+    do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
+    do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
+
+    do_check_eq(aDownload.launchWhenSucceeded,
+                aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+  });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Preparation tasks
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * be imported by the import procedure.
+ */
+add_task(function prepareDownloadsToImport() {
+
+  let sourceUrl = httpUrl("source.txt");
+  let sourceEntityId = yield promiseEntityID(sourceUrl);
+
+  gDownloadsRowToImport = [
+    // Paused download with autoResume and a partial file. By
+    // setting the correct entityID the download can resume from
+    // where it stopped, and to test that this works properly we
+    // intentionally set different data in the beginning of the
+    // partial file to make sure it was not replaced.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress1.txt"),
+      tempPath: yield getPartialFile("inprogress1.txt.part", true),
+      startTime: getStartTime(1),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer1"),
+      entityID: sourceEntityId,
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType1",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication1",
+      autoResume: 1,
+
+      // Even though the information stored in the DB said
+      // maxBytes was MAXBYTES_IN_DB, the download turned out to be
+      // a different length. Here we make sure the totalBytes property
+      // was correctly set with the actual value. The same consideration
+      // applies to the contentType.
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_TAINTED,
+    },
+
+    // Paused download with autoResume and a partial file,
+    // but missing entityID. This means that the download will
+    // start from beginning, and the entire original content of the
+    // source file should replace the different data that was stored
+    // in the partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress2.txt"),
+      tempPath: yield getPartialFile("inprogress2.txt.part", true),
+      startTime: getStartTime(2),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer2"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType2",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication2",
+      autoResume: 1,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Paused download with no autoResume and a partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress3.txt"),
+      tempPath: yield getPartialFile("inprogress3.txt.part"),
+      startTime: getStartTime(3),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer3"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType3",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication3",
+      autoResume: 0,
+
+      // Since this download has not been resumed, the actual data
+      // about its total size and content type is not known.
+      // Therefore, we're going by the information imported from the DB.
+      expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
+      expectedTotalBytes: MAXBYTES_IN_DB,
+      expectedResume: false,
+      expectedContentType: "mimeType3",
+      expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
+    },
+
+    // Paused download with autoResume and no partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress4.txt"),
+      tempPath: "",
+      startTime: getStartTime(4),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer4"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication4",
+      autoResume: 1,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Paused download with no autoResume and no partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress5.txt"),
+      tempPath: "",
+      startTime: getStartTime(5),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer4"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
+      preferredApplication: "prerredApplication5",
+      autoResume: 0,
+
+      expectedCurrentBytes: 0,
+      expectedTotalBytes: MAXBYTES_IN_DB,
+      expectedResume: false,
+      expectedContentType: "text/plain",
+    },
+
+    // Queued download with no autoResume and no partial file.
+    // Even though autoResume=0, queued downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress6.txt"),
+      tempPath: "",
+      startTime: getStartTime(6),
+      state: DOWNLOAD_QUEUED,
+      referrer: httpUrl("referrer6"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication6",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Notstarted download with no autoResume and no partial file.
+    // Even though autoResume=0, notstarted downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress7.txt"),
+      tempPath: "",
+      startTime: getStartTime(7),
+      state: DOWNLOAD_NOTSTARTED,
+      referrer: httpUrl("referrer7"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication7",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Downloading download with no autoResume and a partial file.
+    // Even though autoResume=0, downloading downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress8.txt"),
+      tempPath: yield getPartialFile("inprogress8.txt.part", true),
+      startTime: getStartTime(8),
+      state: DOWNLOAD_DOWNLOADING,
+      referrer: httpUrl("referrer8"),
+      entityID: sourceEntityId,
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication8",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_TAINTED
+    },
+  ];
+});
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * *not* be imported by the import procedure.
+ */
+add_task(function prepareNonImportableDownloads()
+{
+  gDownloadsRowNonImportable = [
+    // Download with no source (should never happen in normal circumstances).
+    {
+      source: "",
+      target: "nonimportable1.txt",
+      tempPath: "",
+      startTime: getStartTime(1),
+      state: DOWNLOAD_PAUSED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType1",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication1",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_FAILED
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable2.txt",
+      tempPath: "",
+      startTime: getStartTime(2),
+      state: DOWNLOAD_FAILED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType2",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication2",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_CANCELED
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable3.txt",
+      tempPath: "",
+      startTime: getStartTime(3),
+      state: DOWNLOAD_CANCELED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType3",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication3",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_BLOCKED_PARENTAL
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable4.txt",
+      tempPath: "",
+      startTime: getStartTime(4),
+      state: DOWNLOAD_BLOCKED_PARENTAL,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType4",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication4",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_SCANNING
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable5.txt",
+      tempPath: "",
+      startTime: getStartTime(5),
+      state: DOWNLOAD_SCANNING,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType5",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication5",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_DIRTY
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable6.txt",
+      tempPath: "",
+      startTime: getStartTime(6),
+      state: DOWNLOAD_DIRTY,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType6",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication6",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_BLOCKED_POLICY
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable7.txt",
+      tempPath: "",
+      startTime: getStartTime(7),
+      state: DOWNLOAD_BLOCKED_POLICY,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType7",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication7",
+      autoResume: 1
+    },
+  ];
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test
+
+/**
+ * Creates a temporary Sqlite database with download data and perform an
+ * import of that data to the new Downloads API to verify that the import
+ * worked correctly.
+ */
+add_task(function test_downloadImport()
+{
+  let connection = null;
+  let downloadsSqlite = getTempFile("downloads.sqlite").path;
+
+  try {
+    // Set up the database.
+    connection = yield promiseEmptyDatabaseConnection({
+      aPath: downloadsSqlite,
+      aSchemaVersion: 9
+    });
+
+    // Insert both the importable and non-importable
+    // downloads together.
+    for (let downloadRow of gDownloadsRowToImport) {
+      yield promiseInsertRow(connection, downloadRow);
+    }
+
+    for (let downloadRow of gDownloadsRowNonImportable) {
+      yield promiseInsertRow(connection, downloadRow);
+    }
+
+    // Check that every item was inserted.
+    do_check_eq((yield promiseTableCount(connection)),
+                gDownloadsRowToImport.length +
+                gDownloadsRowNonImportable.length);
+  } finally {
+    // Close the connection so that DownloadImport can open it.
+    yield connection.close();
+  }
+
+  // Import items.
+  let list = yield promiseNewList(false);
+  yield new DownloadImport(list, downloadsSqlite).import();
+  let items = yield list.getAll();
+
+  do_check_eq(items.length, gDownloadsRowToImport.length);
+
+  for (let i = 0; i < gDownloadsRowToImport.length; i++) {
+    yield checkDownload(items[i], gDownloadsRowToImport[i]);
+  }
+})
--- a/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadIntegration.js
@@ -60,18 +60,18 @@ function notifyPromptObservers(aIsPrivat
 //// Tests
 
 XPCOMUtils.defineLazyGetter(this, "gStringBundle", function() {
   return Services.strings.
     createBundle("chrome://mozapps/locale/downloads/downloads.properties");
 });
 
 /**
- * Tests that the getSystemDownloadsDirectory returns a valid nsFile
- * download directory object.
+ * Tests that the getSystemDownloadsDirectory returns a valid download
+ * directory string path.
  */
 add_task(function test_getSystemDownloadsDirectory()
 {
   // Enable test mode for the getSystemDownloadsDirectory method to return
   // temp directory instead so we can check whether the desired directory
   // is created or not.
   DownloadIntegration.testMode = true;
   function cleanup() {
@@ -83,121 +83,120 @@ add_task(function test_getSystemDownload
   let downloadDir;
 
   // OSX / Linux / Windows but not XP/2k
   if (Services.appinfo.OS == "Darwin" ||
       Services.appinfo.OS == "Linux" ||
       (Services.appinfo.OS == "WINNT" &&
        parseFloat(Services.sysinfo.getProperty("version")) >= 6)) {
     downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
-    do_check_true(downloadDir instanceof Ci.nsIFile);
-    do_check_eq(downloadDir.path, tempDir.path);
-    do_check_true(yield OS.File.exists(downloadDir.path));
+    do_check_eq(downloadDir, tempDir.path);
+    do_check_true(yield OS.File.exists(downloadDir));
 
-    let info = yield OS.File.stat(downloadDir.path);
+    let info = yield OS.File.stat(downloadDir);
     do_check_true(info.isDir);
   } else {
     let targetPath = OS.Path.join(tempDir.path,
                        gStringBundle.GetStringFromName("downloadsFolder"));
     try {
       yield OS.File.removeEmptyDir(targetPath);
     } catch(e) {}
     downloadDir = yield DownloadIntegration.getSystemDownloadsDirectory();
-    do_check_eq(downloadDir.path, targetPath);
-    do_check_true(yield OS.File.exists(downloadDir.path));
+    do_check_eq(downloadDir, targetPath);
+    do_check_true(yield OS.File.exists(downloadDir));
 
-    let info = yield OS.File.stat(downloadDir.path);
+    let info = yield OS.File.stat(downloadDir);
     do_check_true(info.isDir);
     yield OS.File.removeEmptyDir(targetPath);
   }
 
   let downloadDirBefore = yield DownloadIntegration.getSystemDownloadsDirectory();
   cleanup();
   let downloadDirAfter = yield DownloadIntegration.getSystemDownloadsDirectory();
-  do_check_false(downloadDirBefore.equals(downloadDirAfter));
+  do_check_neq(downloadDirBefore, downloadDirAfter);
 });
 
 /**
- * Tests that the getPreferredDownloadsDirectory returns a valid nsFile
- * download directory object.
+ * Tests that the getPreferredDownloadsDirectory returns a valid download
+ * directory string path.
  */
 add_task(function test_getPreferredDownloadsDirectory()
 {
   let folderListPrefName = "browser.download.folderList";
   let dirPrefName = "browser.download.dir";
   function cleanup() {
     Services.prefs.clearUserPref(folderListPrefName);
     Services.prefs.clearUserPref(dirPrefName);
   }
   do_register_cleanup(cleanup);
 
   // Should return the system downloads directory.
   Services.prefs.setIntPref(folderListPrefName, 1);
   let systemDir = yield DownloadIntegration.getSystemDownloadsDirectory();
   let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
-  do_check_eq(downloadDir.path, systemDir.path);
+  do_check_neq(downloadDir, "");
+  do_check_eq(downloadDir, systemDir);
 
   // Should return the desktop directory.
   Services.prefs.setIntPref(folderListPrefName, 0);
   downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
-  do_check_eq(downloadDir.path, Services.dirsvc.get("Desk", Ci.nsIFile).path);
+  do_check_neq(downloadDir, "");
+  do_check_eq(downloadDir, Services.dirsvc.get("Desk", Ci.nsIFile).path);
 
   // Should return the system downloads directory because the dir preference
   // is not set.
   Services.prefs.setIntPref(folderListPrefName, 2);
   let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
-  do_check_eq(downloadDir.path, systemDir.path);
+  do_check_neq(downloadDir, "");
+  do_check_eq(downloadDir, systemDir);
 
   // Should return the directory which is listed in the dir preference.
   let time = (new Date()).getTime();
   let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
   tempDir.append(time);
   Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
   downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
-  do_check_eq(downloadDir.path,  tempDir.path);
-  do_check_true(yield OS.File.exists(downloadDir.path));
+  do_check_neq(downloadDir, "");
+  do_check_eq(downloadDir,  tempDir.path);
+  do_check_true(yield OS.File.exists(downloadDir));
   yield OS.File.removeEmptyDir(tempDir.path);
 
   // Should return the system downloads directory beacause the path is invalid
   // in the dir preference.
   tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
   tempDir.append("dir_not_exist");
   tempDir.append(time);
   Services.prefs.setComplexValue("browser.download.dir", Ci.nsIFile, tempDir);
   downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_eq(downloadDir.path, systemDir.path);
+  do_check_eq(downloadDir, systemDir);
 
   // Should return the system downloads directory because the folderList
   // preference is invalid
   Services.prefs.setIntPref(folderListPrefName, 999);
   let downloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-  do_check_eq(downloadDir.path, systemDir.path);
+  do_check_eq(downloadDir, systemDir);
 
   cleanup();
 });
 
 /**
- * Tests that the getTemporaryDownloadsDirectory returns a valid nsFile 
- * download directory object.
+ * Tests that the getTemporaryDownloadsDirectory returns a valid download
+ * directory string path.
  */
 add_task(function test_getTemporaryDownloadsDirectory()
 {
   let downloadDir = yield DownloadIntegration.getTemporaryDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
+  do_check_neq(downloadDir, "");
 
   if ("nsILocalFileMac" in Ci) {
     let preferredDownloadDir = yield DownloadIntegration.getPreferredDownloadsDirectory();
-    do_check_eq(downloadDir.path, preferredDownloadDir.path);
+    do_check_eq(downloadDir, preferredDownloadDir);
   } else {
     let tempDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
-    do_check_eq(downloadDir.path, tempDir.path);
+    do_check_eq(downloadDir, tempDir.path);
   }
 });
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Tests DownloadObserver
 
 /**
  * Tests notifications prompts when observers are notified if there are public
--- a/toolkit/components/jsdownloads/test/unit/test_Downloads.js
+++ b/toolkit/components/jsdownloads/test/unit/test_Downloads.js
@@ -131,42 +131,42 @@ add_task(function test_getSummary()
 
   do_check_eq(publicSummaryOne, publicSummaryTwo);
   do_check_eq(privateSummaryOne, privateSummaryTwo);
 
   do_check_neq(publicSummaryOne, privateSummaryOne);
 });
 
 /**
- * Tests that the getSystemDownloadsDirectory returns a valid nsFile
- * download directory object.
+ * Tests that the getSystemDownloadsDirectory returns a non-empty download
+ * directory string.
  */
 add_task(function test_getSystemDownloadsDirectory()
 {
   let downloadDir = yield Downloads.getSystemDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
+  do_check_neq(downloadDir, "");
 });
 
 /**
- * Tests that the getPreferredDownloadsDirectory returns a valid nsFile
- * download directory object.
+ * Tests that the getPreferredDownloadsDirectory returns a non-empty download
+ * directory string.
  */
 add_task(function test_getPreferredDownloadsDirectory()
 {
   let downloadDir = yield Downloads.getPreferredDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
+  do_check_neq(downloadDir, "");
 });
 
 /**
- * Tests that the getTemporaryDownloadsDirectory returns a valid nsFile
- * download directory object.
+ * Tests that the getTemporaryDownloadsDirectory returns a non-empty download
+ * directory string.
  */
 add_task(function test_getTemporaryDownloadsDirectory()
 {
   let downloadDir = yield Downloads.getTemporaryDownloadsDirectory();
-  do_check_true(downloadDir instanceof Ci.nsIFile);
+  do_check_neq(downloadDir, "");
 });
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Termination
 
 let tailFile = do_get_file("tail.js");
 Services.scriptloader.loadSubScript(NetUtil.newURI(tailFile).spec);
--- a/toolkit/components/jsdownloads/test/unit/xpcshell.ini
+++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
@@ -1,14 +1,17 @@
 [DEFAULT]
 head = head.js
 tail =
+
+# Note: The "tail.js" file is not defined in the "tail" key because it calls
+#       the "add_test_task" function, that does not work properly in tail files.
 support-files =
   common_test_Download.js
-# tail.js should quite possibly be in the tail key.
   tail.js
 
 [test_DownloadCore.js]
+[test_DownloadImport.js]
 [test_DownloadIntegration.js]
 [test_DownloadLegacy.js]
 [test_DownloadList.js]
 [test_Downloads.js]
 [test_DownloadStore.js]
--- a/toolkit/components/social/FrameWorker.jsm
+++ b/toolkit/components/social/FrameWorker.jsm
@@ -66,16 +66,21 @@ this.getFrameWorkerHandle =
 
 // A "_Worker" is an internal representation of a worker.  It's never returned
 // directly to consumers.
 function _Worker(browserPromise, options) {
   this.browserPromise = browserPromise;
   this.options = options;
   this.ports = new Map();
   browserPromise.then(browser => {
+    browser.addEventListener("oop-browser-crashed", () => {
+      Cu.reportError("FrameWorker remote process crashed");
+      notifyWorkerError(options.origin);
+    });
+
     let mm = browser.messageManager;
     // execute the content script and send the message to bootstrap the content
     // side of the world.
     mm.loadFrameScript("resource://gre/modules/FrameWorkerContent.js", true);
     mm.sendAsyncMessage("frameworker:init", this.options);
     mm.addMessageListener("frameworker:port-message", this);
     mm.addMessageListener("frameworker:notify-worker-error", this);
   });
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2571,23 +2571,16 @@
     "description": "Firefox: Time in ms till a tab switch is complete including the first paint"
   },
   "FX_TAB_CLICK_MS": {
     "kind": "exponential",
     "high": "1000",
     "n_buckets": 20,
     "description": "Firefox: Time in ms spent on switching tabs in response to a tab click"
   },
-  "FX_IDENTITY_POPUP_OPEN_MS": {
-    "kind": "exponential",
-    "high": "1000",
-    "n_buckets": 10,
-    "extended_statistics_ok": true,
-    "description": "Firefox: Time taken by the identity popup to open in milliseconds"
-  },
   "FX_APP_MENU_OPEN_MS": {
     "kind": "exponential",
     "high": "1000",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "Firefox: Time taken by the app-menu opening in milliseconds"
   },
   "FX_BOOKMARKS_TOOLBAR_INIT_MS": {
--- a/toolkit/devtools/DevToolsUtils.jsm
+++ b/toolkit/devtools/DevToolsUtils.jsm
@@ -18,10 +18,11 @@ Components.classes["@mozilla.org/moz/jss
   .getService(Components.interfaces.mozIJSSubScriptLoader)
   .loadSubScript("resource://gre/modules/devtools/DevToolsUtils.js", this);
 
 this.DevToolsUtils = {
   safeErrorString: safeErrorString,
   reportException: reportException,
   makeInfallible: makeInfallible,
   yieldingEach: yieldingEach,
+  reportingDisabled: false , // Used by tests.
   defineLazyPrototypeGetter: defineLazyPrototypeGetter
 };
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -19,17 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
 
 let SourceMap = {};
 Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap);
 
 let loader = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {}).Loader;
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise;
 
-this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools"];
+this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider",
+                         "SrcdirProvider"];
 
 /**
  * Providers are different strategies for loading the devtools.
  */
 
 let loaderGlobals = {
   btoa: btoa,
   console: console,
@@ -99,16 +100,17 @@ var SrcdirProvider = {
     let toolkitDir = OS.Path.join(srcdir, "toolkit", "devtools");
     let mainURI = this.fileURI(OS.Path.join(devtoolsDir, "main.js"));
     let devtoolsURI = this.fileURI(devtoolsDir);
     let serverURI = this.fileURI(OS.Path.join(toolkitDir, "server"));
     let webconsoleURI = this.fileURI(OS.Path.join(toolkitDir, "webconsole"));
     let appActorURI = this.fileURI(OS.Path.join(toolkitDir, "apps", "app-actor-front.js"));
     let cssLogicURI = this.fileURI(OS.Path.join(toolkitDir, "styleinspector", "css-logic"));
     let cssColorURI = this.fileURI(OS.Path.join(toolkitDir, "css-color"));
+    let outputParserURI = this.fileURI(OS.Path.join(toolkitDir, "output-parser"));
     let touchEventsURI = this.fileURI(OS.Path.join(toolkitDir, "touch-events"));
     let clientURI = this.fileURI(OS.Path.join(toolkitDir, "client"));
     let escodegenURI = this.fileURI(OS.Path.join(toolkitDir, "escodegen"));
     let estraverseURI = this.fileURI(OS.Path.join(toolkitDir, "escodegen", "estraverse"));
     this.loader = new loader.Loader({
       modules: {
         "toolkit/loader": loader,
         "source-map": SourceMap,
@@ -117,16 +119,17 @@ var SrcdirProvider = {
         "": "resource://gre/modules/commonjs/",
         "main": mainURI,
         "devtools": devtoolsURI,
         "devtools/server": serverURI,
         "devtools/toolkit/webconsole": webconsoleURI,
         "devtools/app-actor-front": appActorURI,
         "devtools/styleinspector/css-logic": cssLogicURI,
         "devtools/css-color": cssColorURI,
+        "devtools/output-parser": outputParserURI,
         "devtools/touch-events": touchEventsURI,
         "devtools/client": clientURI,
         "escodegen": escodegenURI,
         "estraverse": estraverseURI
       },
       globals: loaderGlobals
     });
 
--- a/toolkit/devtools/apps/app-actor-front.js
+++ b/toolkit/devtools/apps/app-actor-front.js
@@ -10,16 +10,18 @@ const promise = require("sdk/core/promis
 
 const PR_USEC_PER_MSEC = 1000;
 const PR_RDWR = 0x04;
 const PR_CREATE_FILE = 0x08;
 const PR_TRUNCATE = 0x20;
 
 const CHUNK_SIZE = 10000;
 
+const appTargets = new Map();
+
 function addDirToZip(writer, dir, basePath) {
   let files = dir.directoryEntries;
 
   while (files.hasMoreElements()) {
     let file = files.getNext().QueryInterface(Ci.nsIFile);
 
     if (file.isHidden() ||
         file.isSymlink() ||
@@ -207,16 +209,23 @@ function installHosted(client, webappsAc
     else
       deferred.resolve({appId: res.appId});
   });
   return deferred.promise;
 }
 exports.installHosted = installHosted;
 
 function getTargetForApp(client, webappsActor, manifestURL) {
+  // Ensure always returning the exact same JS object for a target
+  // of the same app in order to show only one toolbox per app and
+  // avoid re-creating lot of objects twice.
+  let existingTarget = appTargets.get(manifestURL);
+  if (existingTarget)
+    return promise.resolve(existingTarget);
+
   let deferred = promise.defer();
   let request = {
     to: webappsActor,
     type: "getAppActor",
     manifestURL: manifestURL,
   }
   client.request(request, (res) => {
     if (res.error) {
@@ -225,16 +234,20 @@ function getTargetForApp(client, webapps
       let options = {
         form: res.actor,
         client: client,
         chrome: false
       };
 
       devtools.TargetFactory.forRemoteTab(options).then((target) => {
         target.isApp = true;
+        appTargets.set(manifestURL, target);
+        target.on("close", () => {
+          appTargets.delete(manifestURL);
+        });
         deferred.resolve(target)
       }, (error) => {
         deferred.reject(error);
       });
     }
   });
   return deferred.promise;
 }
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -190,16 +190,17 @@ const UnsolicitedNotifications = {
   "logMessage": "logMessage",
   "networkEvent": "networkEvent",
   "networkEventUpdate": "networkEventUpdate",
   "newGlobal": "newGlobal",
   "newScript": "newScript",
   "newSource": "newSource",
   "tabDetached": "tabDetached",
   "tabListChanged": "tabListChanged",
+  "reflowActivity": "reflowActivity",
   "addonListChanged": "addonListChanged",
   "tabNavigated": "tabNavigated",
   "pageError": "pageError",
   "documentLoad": "documentLoad",
   "enteredFrame": "enteredFrame",
   "exitedFrame": "exitedFrame",
   "appOpen": "appOpen",
   "appClose": "appClose",
@@ -235,16 +236,17 @@ this.DebuggerClient = function DebuggerC
 
   this._pendingRequests = [];
   this._activeRequests = new Map;
   this._eventsEnabled = true;
 
   this.compat = new ProtocolCompatibility(this, [
     new SourcesShim(),
   ]);
+  this.traits = {};
 
   this.request = this.request.bind(this);
   this.localTransport = this._transport.onOutputStreamReady === undefined;
 
   /*
    * As the first thing on the connection, expect a greeting packet from
    * the connection's root actor.
    */
@@ -354,21 +356,22 @@ DebuggerClient.prototype = {
   /**
    * Connect to the server and start exchanging protocol messages.
    *
    * @param aOnConnected function
    *        If specified, will be called when the greeting packet is
    *        received from the debugging server.
    */
   connect: function DC_connect(aOnConnected) {
-    if (aOnConnected) {
-      this.addOneTimeListener("connected", function(aName, aApplicationType, aTraits) {
+    this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => {
+      this.traits = aTraits;
+      if (aOnConnected) {
         aOnConnected(aApplicationType, aTraits);
-      });
-    }
+      }
+    });
 
     this._transport.ready();
   },
 
   /**
    * Shut down communication with the debugging server.
    *
    * @param aOnClosed function
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -71,16 +71,20 @@ const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-
 const HIGHLIGHTED_TIMEOUT = 2000;
 
 let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
 HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
+loader.lazyGetter(this, "DOMParser", function() {
+ return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+});
+
 exports.register = function(handle) {
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeTabActor(InspectorActor);
 };
 
@@ -138,16 +142,21 @@ var NodeActor = protocol.ActorClass({
   },
 
   /**
    * Instead of storing a connection object, the NodeActor gets its connection
    * from its associated walker.
    */
   get conn() this.walker.conn,
 
+  isDocumentElement: function() {
+    return this.rawNode.ownerDocument &&
+        this.rawNode.ownerDocument.documentElement === this.rawNode;
+  },
+
   // Returns the JSON representation of this object over the wire.
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let parentNode = this.walker.parentNode(this);
 
@@ -172,18 +181,17 @@ var NodeActor = protocol.ActorClass({
       publicId: this.rawNode.publicId,
       systemId: this.rawNode.systemId,
 
       attrs: this.writeAttrs(),
 
       pseudoClassLocks: this.writePseudoClassLocks(),
     };
 
-    if (this.rawNode.ownerDocument &&
-        this.rawNode.ownerDocument.documentElement === this.rawNode) {
+    if (this.isDocumentElement()) {
       form.isDocumentElement = true;
     }
 
     if (this.rawNode.nodeValue) {
       // We only include a short version of the value if it's longer than
       // gValueSummaryLength
       if (this.rawNode.nodeValue.length > gValueSummaryLength) {
         form.shortValue = this.rawNode.nodeValue.substring(0, gValueSummaryLength);
@@ -1543,16 +1551,72 @@ var WalkerActor = protocol.ActorClass({
       node: Arg(0, "domnode")
     },
     response: {
       value: RetVal("longstring")
     }
   }),
 
   /**
+   * Set a node's outerHTML property.
+   */
+  setOuterHTML: method(function(node, value) {
+    let parsedDOM = DOMParser.parseFromString(value, "text/html");
+    let rawNode = node.rawNode;
+    let parentNode = rawNode.parentNode;
+
+    // Special case for head and body.  Setting document.body.outerHTML
+    // creates an extra <head> tag, and document.head.outerHTML creates
+    // an extra <body>.  So instead we will call replaceChild with the
+    // parsed DOM, assuming that they aren't trying to set both tags at once.
+    if (rawNode.tagName === "BODY") {
+      if (parsedDOM.head.innerHTML === "") {
+        parentNode.replaceChild(parsedDOM.body, rawNode);
+      } else {
+        rawNode.outerHTML = value;
+      }
+    } else if (rawNode.tagName === "HEAD") {
+      if (parsedDOM.body.innerHTML === "") {
+        parentNode.replaceChild(parsedDOM.head, rawNode);
+      } else {
+        rawNode.outerHTML = value;
+      }
+    } else if (node.isDocumentElement()) {
+      // Unable to set outerHTML on the document element.  Fall back by
+      // setting attributes manually, then replace the body and head elements.
+      let finalAttributeModifications = [];
+      let attributeModifications = {};
+      for (let attribute of rawNode.attributes) {
+        attributeModifications[attribute.name] = null;
+      }
+      for (let attribute of parsedDOM.documentElement.attributes) {
+        attributeModifications[attribute.name] = attribute.value;
+      }
+      for (let key in attributeModifications) {
+        finalAttributeModifications.push({
+          attributeName: key,
+          newValue: attributeModifications[key]
+        });
+      }
+      node.modifyAttributes(finalAttributeModifications);
+      rawNode.replaceChild(parsedDOM.head, rawNode.querySelector("head"));
+      rawNode.replaceChild(parsedDOM.body, rawNode.querySelector("body"));
+    } else {
+      rawNode.outerHTML = value;
+    }
+  }, {
+    request: {
+      node: Arg(0, "domnode"),
+      value: Arg(1),
+    },
+    response: {
+    }
+  }),
+
+  /**
    * Removes a node from its parent node.
    *
    * @returns The node's nextSibling before it was removed.
    */
   removeNode: method(function(node) {
     if ((node.rawNode.ownerDocument &&
          node.rawNode.ownerDocument.documentElement === this.rawNode) ||
          node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -170,17 +170,18 @@ RootActor.prototype = {
    */
   sayHello: function() {
     return {
       from: this.actorID,
       applicationType: this.applicationType,
       /* This is not in the spec, but it's used by tests. */
       testConnectionPrefix: this.conn.prefix,
       traits: {
-        sources: true
+        sources: true,
+        editOuterHTML: true
       }
     };
   },
 
   /**
    * This is true for the root actor only, used by some child actors
    */
   get isRootActor() true,
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -3951,16 +3951,22 @@ ThreadSources.prototype = {
             line: line,
             column: column
           });
           return {
             url: aSourceURL,
             line: aLine,
             column: aColumn
           };
+        })
+        .then(null, error => {
+          if (!DevToolsUtils.reportingDisabled) {
+            DevToolsUtils.reportException(error);
+          }
+          return { url: null, line: null, column: null };
         });
     }
 
     // No source map
     return resolve({
       url: url,
       line: line,
       column: column
--- a/toolkit/devtools/server/actors/webapps.js
+++ b/toolkit/devtools/server/actors/webapps.js
@@ -817,17 +817,25 @@ WebappsActor.prototype = {
           childTransport.close();
           this.conn.cancelForwarding(prefix);
         } else {
           // Otherwise, the app has been closed before the actor
           // had a chance to be created, so we are not able to create
           // the actor.
           deferred.resolve(null);
         }
-        this._appActorsMap.delete(mm);
+        let actor = this._appActorsMap.get(mm);
+        if (actor) {
+          // The ContentAppActor within the child process doesn't necessary
+          // have to time to uninitialize itself when the app is closed/killed.
+          // So ensure telling the client that the related actor is detached.
+          this.conn.send({ from: actor.actor,
+                           type: "tabDetached" });
+          this._appActorsMap.delete(mm);
+        }
       }
     }).bind(this);
     Services.obs.addObserver(onMessageManagerDisconnect,
                              "message-manager-disconnect", false);
 
     let prefixStart = this.conn.prefix + "child";
     mm.sendAsyncMessage("debug:connect", { prefix: prefixStart });
 
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -17,17 +17,18 @@ let devtools = Cu.import("resource://gre
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPIStorage",
                                   "resource://gre/modules/ConsoleAPIStorage.jsm");
 
 for (let name of ["WebConsoleUtils", "ConsoleServiceListener",
                   "ConsoleAPIListener", "ConsoleProgressListener",
-                  "JSTermHelpers", "JSPropertyProvider", "NetworkMonitor"]) {
+                  "JSTermHelpers", "JSPropertyProvider", "NetworkMonitor",
+                  "ConsoleReflowListener"]) {
   Object.defineProperty(this, name, {
     get: function(prop) {
       if (prop == "WebConsoleUtils") {
         prop = "Utils";
       }
       return devtools.require("devtools/toolkit/webconsole/utils")[prop];
     }.bind(null, name),
     configurable: true,
@@ -63,16 +64,18 @@ function WebConsoleActor(aConnection, aP
 
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 }
 
+WebConsoleActor.l10n = new WebConsoleUtils.l10n("chrome://global/locale/console.properties");
+
 WebConsoleActor.prototype =
 {
   /**
    * Debugger instance.
    *
    * @see jsdebugger.jsm
    */
   dbg: null,
@@ -118,25 +121,92 @@ WebConsoleActor.prototype =
   conn: null,
 
   /**
    * The window we work with.
    * @type nsIDOMWindow
    */
   get window() {
     if (this.parentActor.isRootActor) {
-      // Try to find the Browser Console window, otherwise use the window of
-      // the root actor.
-      let window = Services.wm.getMostRecentWindow("devtools:webconsole");
-      return window || this.parentActor.window;
+      return this._getWindowForBrowserConsole();
     }
     return this.parentActor.window;
   },
 
   /**
+   * Get a window to use for the browser console.
+   *
+   * @private
+   * @return nsIDOMWindow
+   *         The window to use, or null if no window could be found.
+   */
+  _getWindowForBrowserConsole: function WCA__getWindowForBrowserConsole()
+  {
+    // Check if our last used chrome window is still live.
+    let window = this._lastChromeWindow && this._lastChromeWindow.get();
+    // If not, look for a new one.
+    if (!window || window.closed) {
+      window = this.parentActor.window;
+      if (!window) {
+        // Try to find the Browser Console window to use instead.
+        window = Services.wm.getMostRecentWindow("devtools:webconsole");
+        // We prefer the normal chrome window over the console window,
+        // so we'll look for those windows in order to replace our reference.
+        let onChromeWindowOpened = () => {
+          // We'll look for this window when someone next requests window()
+          Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened");
+          this._lastChromeWindow = null;
+        };
+        Services.obs.addObserver(onChromeWindowOpened, "domwindowopened", false);
+      }
+
+      this._handleNewWindow(window);
+    }
+
+    return window;
+  },
+
+  /**
+   * Store a newly found window on the actor to be used in the future.
+   *
+   * @private
+   * @param nsIDOMWindow window
+   *        The window to store on the actor (can be null).
+   */
+  _handleNewWindow: function WCA__handleNewWindow(window)
+  {
+    if (window) {
+      if (this._hadChromeWindow) {
+        let contextChangedMsg = WebConsoleActor.l10n.getStr("evaluationContextChanged");
+        Services.console.logStringMessage(contextChangedMsg);
+      }
+      this._lastChromeWindow = Cu.getWeakReference(window);
+      this._hadChromeWindow = true;
+    } else {
+      this._lastChromeWindow = null;
+    }
+  },
+
+  /**
+   * Whether we've been using a window before.
+   *
+   * @private
+   * @type boolean
+   */
+  _hadChromeWindow: false,
+
+  /**
+   * A weak reference to the last chrome window we used to work with.
+   *
+   * @private
+   * @type nsIWeakReference
+   */
+  _lastChromeWindow: null,
+
+  /**
    * The ConsoleServiceListener instance.
    * @type object
    */
   consoleServiceListener: null,
 
   /**
    * The ConsoleAPIListener instance.
    */
@@ -148,16 +218,21 @@ WebConsoleActor.prototype =
   networkMonitor: null,
 
   /**
    * The ConsoleProgressListener instance.
    */
   consoleProgressListener: null,
 
   /**
+   * The ConsoleReflowListener instance.
+   */
+  consoleReflowListener: null,
+
+  /**
    * Getter for the NetworkMonitor.saveRequestAndResponseBodies preference.
    * @type boolean
    */
   get saveRequestAndResponseBodies()
     this._prefs["NetworkMonitor.saveRequestAndResponseBodies"],
 
   actorPrefix: "console",
 
@@ -189,16 +264,20 @@ WebConsoleActor.prototype =
     if (this.networkMonitor) {
       this.networkMonitor.destroy();
       this.networkMonitor = null;
     }
     if (this.consoleProgressListener) {
       this.consoleProgressListener.destroy();
       this.consoleProgressListener = null;
     }
+    if (this.consoleReflowListener) {
+      this.consoleReflowListener.destroy();
+      this.consoleReflowListener = null;
+    }
     this.conn.removeActorPool(this._actorPool);
     if (this.parentActor.isRootActor) {
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
     this._actorPool = null;
 
     this._netEvents.clear();
@@ -394,16 +473,23 @@ WebConsoleActor.prototype =
           if (!this.consoleProgressListener) {
             this.consoleProgressListener =
               new ConsoleProgressListener(this.window, this);
           }
           this.consoleProgressListener.startMonitor(this.consoleProgressListener.
                                                     MONITOR_FILE_ACTIVITY);
           startedListeners.push(listener);
           break;
+        case "ReflowActivity":
+          if (!this.consoleReflowListener) {
+            this.consoleReflowListener =
+              new ConsoleReflowListener(this.window, this);
+          }
+          startedListeners.push(listener);
+          break;
       }
     }
     return {
       startedListeners: startedListeners,
       nativeConsoleAPI: this.hasNativeConsoleAPI(this.window),
     };
   },
 
@@ -453,16 +539,23 @@ WebConsoleActor.prototype =
         case "FileActivity":
           if (this.consoleProgressListener) {
             this.consoleProgressListener.stopMonitor(this.consoleProgressListener.
                                                      MONITOR_FILE_ACTIVITY);
             this.consoleProgressListener = null;
           }
           stoppedListeners.push(listener);
           break;
+        case "ReflowActivity":
+          if (this.consoleReflowListener) {
+            this.consoleReflowListener.destroy();
+            this.consoleReflowListener = null;
+          }
+          stoppedListeners.push(listener);
+          break;
       }
     }
 
     return { stoppedListeners: stoppedListeners };
   },
 
   /**
    * Handler for the "getCachedMessages" request. This method sends the cached
@@ -1063,16 +1156,39 @@ WebConsoleActor.prototype =
     let packet = {
       from: this.actorID,
       type: "fileActivity",
       uri: aFileURI,
     };
     this.conn.send(packet);
   },
 
+  /**
+   * Handler for reflow activity. This method forwards reflow events to the
+   * remote Web Console client.
+   *
+   * @see ConsoleReflowListener
+   * @param Object aReflowInfo
+   */
+  onReflowActivity: function WCA_onReflowActivity(aReflowInfo)
+  {
+    let packet = {
+      from: this.actorID,
+      type: "reflowActivity",
+      interruptible: aReflowInfo.interruptible,
+      start: aReflowInfo.start,
+      end: aReflowInfo.end,
+      sourceURL: aReflowInfo.sourceURL,
+      sourceLine: aReflowInfo.sourceLine,
+      functionName: aReflowInfo.functionName
+    };
+
+    this.conn.send(packet);
+  },
+
   //////////////////
   // End of event handlers for various listeners.
   //////////////////
 
   /**
    * Prepare a message from the console API to be sent to the remote Web Console
    * instance.
    *
--- a/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
+++ b/toolkit/devtools/server/tests/mochitest/inspector-traversal-data.html
@@ -45,10 +45,11 @@
     <div id="w">w</div>
     <div id="x">x</div>
     <div id="y">y</div>
     <div id="z">z</div>
   </div>
   <div id="longlist-sibling">
     <div id="longlist-sibling-firstchild"></div>
   </div>
+  <p id="edit-html"></p>
 </body>
 </html>
--- a/toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
+++ b/toolkit/devtools/server/tests/mochitest/test_inspector-traversal.html
@@ -68,16 +68,35 @@ addTest(function testOuterHTML() {
     return gWalker.outerHTML(docElement);
   }).then(longstring => {
     return longstring.string();
   }).then(outerHTML => {
     ok(outerHTML === gInspectee.documentElement.outerHTML, "outerHTML should match");
   }).then(runNextTest));
 });
 
+addTest(function testSetOuterHTMLNode() {
+  let newHTML = "<p id=\"edit-html-done\">after edit</p>";
+  promiseDone(gWalker.querySelector(gWalker.rootNode, "#edit-html").then(node => {
+    return gWalker.setOuterHTML(node, newHTML);
+  }).then(() => {
+    return gWalker.querySelector(gWalker.rootNode, "#edit-html-done");
+  }).then(node => {
+    return gWalker.outerHTML(node);
+  }).then(longstring => {
+    return longstring.string();
+  }).then(outerHTML => {
+    is(outerHTML, newHTML, "outerHTML has been updated");
+  }).then(() => {
+    return gWalker.querySelector(gWalker.rootNode, "#edit-html");
+  }).then(node => {
+    ok(!node, "The node with the old ID cannot be selected anymore");
+  }).then(runNextTest));
+});
+
 addTest(function testQuerySelector() {
   promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(node => {
     is(node.getAttribute("data-test"), "exists", "should have found the right node");
     assertOwnership();
   }).then(() => {
     return gWalker.querySelector(gWalker.rootNode, "unknownqueryselector").then(node => {
       ok(!node, "Should not find a node here.");
       assertOwnership();
--- a/toolkit/devtools/server/transport.js
+++ b/toolkit/devtools/server/transport.js
@@ -63,19 +63,17 @@ DebuggerTransport.prototype = {
    * Transmit a packet.
    *
    * This method returns immediately, without waiting for the entire
    * packet to be transmitted, registering event handlers as needed to
    * transmit the entire packet. Packets are transmitted in the order
    * they are passed to this method.
    */
   send: function DT_send(aPacket) {
-    let data = wantLogging
-      ? JSON.stringify(aPacket, null, 2)
-      : JSON.stringify(aPacket);
+    let data = JSON.stringify(aPacket);
     data = this._converter.ConvertFromUnicode(data);
     data = data.length + ':' + data;
     this._outgoing += data;
     this._flushOutgoing();
   },
 
   /**
    * Close the transport.
@@ -187,17 +185,19 @@ DebuggerTransport.prototype = {
       let msg = "Error parsing incoming packet: " + packet + " (" + e + " - " + e.stack + ")";
       if (Cu.reportError) {
         Cu.reportError(msg);
       }
       dump(msg + "\n");
       return true;
     }
 
-    dumpn("Got: " + packet);
+    if (wantLogging) {
+      dumpn("Got: " + JSON.stringify(parsed, null, 2));
+    }
     let self = this;
     Services.tm.currentThread.dispatch(makeInfallible(function() {
       // Ensure the hooks are still around by the time this runs (they will go
       // away when the transport is closed).
       if (self.hooks) {
         self.hooks.onPacket(parsed);
       }
     }, "DebuggerTransport instance's this.hooks.onPacket"), 0);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/mochitest/chrome.ini
@@ -0,0 +1,1 @@
+[test_loader_paths.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tests/mochitest/test_loader_paths.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+
+<html>
+
+  <head>
+    <meta charset="utf8">
+    <title></title>
+
+    <script type="application/javascript"
+            src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+    <link rel="stylesheet" type="text/css"
+          href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  </head>
+
+  <body>
+
+    <script type="application/javascript;version=1.8">
+      const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+      const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+
+      const SRCDIR_PREF = "devtools.loader.srcdir";
+      let srcDir = Cc["@mozilla.org/file/directory_service;1"]
+                   .getService(Components.interfaces.nsIProperties)
+                   .get("CurWorkD", Components.interfaces.nsIFile).path;
+
+      let srcDirStr = Cc["@mozilla.org/supports-string;1"]
+                      .createInstance(Ci.nsISupportsString);
+      srcDirStr.data = srcDir;
+      Services.prefs.setComplexValue(SRCDIR_PREF, Ci.nsISupportsString,
+                                     srcDirStr);
+
+      const { BuiltinProvider, SrcdirProvider } =
+        Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+      BuiltinProvider.load();
+      SrcdirProvider.load();
+
+      is(BuiltinProvider.loader.mapping.length,
+         SrcdirProvider.loader.mapping.length + 1,
+         "The built-in loader should have only one more mapping for testing.");
+
+      Services.prefs.clearUserPref(SRCDIR_PREF);
+    </script>
+  </body>
+</html>
--- a/toolkit/devtools/tests/moz.build
+++ b/toolkit/devtools/tests/moz.build
@@ -1,9 +1,10 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MODULE = 'test_devtools'
 
+MOCHITEST_CHROME_MANIFESTS += ['mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -5,16 +5,17 @@ support-files =
   data.json^headers^
   network_requests_iframe.html
 
 [test_basics.html]
 [test_bug819670_getter_throws.html]
 [test_cached_messages.html]
 [test_consoleapi.html]
 [test_file_uri.html]
+[test_reflow.html]
 [test_jsterm.html]
 [test_network_get.html]
 [test_network_longstring.html]
 [test_network_post.html]
 [test_nsiconsolemessage.html]
 [test_object_actor.html]
 [test_object_actor_native_getters.html]
 [test_object_actor_native_getters_lenient_this.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_reflow.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the Reflow Activity</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for reflow events</p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let client;
+
+function generateReflow()
+{
+  top.document.documentElement.style.display = "none";
+  top.document.documentElement.getBoundingClientRect();
+  top.document.documentElement.style.display = "block";
+}
+
+function startTest()
+{
+  removeEventListener("load", startTest);
+  attachConsole(["ReflowActivity"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  client = aState.dbgClient;
+
+  onReflowActivity = onReflowActivity.bind(null, aState);
+  client.addListener("reflowActivity", onReflowActivity);
+  generateReflow();
+}
+
+// We are expecting 3 reflow events.
+let expectedEvents = [
+  {
+    interruptible: false,
+    sourceURL: "chrome://mochitests/content/chrome/toolkit/devtools/webconsole/test/test_reflow.html",
+    functionName: "generateReflow"
+  },
+  {
+    interruptible: true,
+    sourceURL: null,
+    functionName: null
+  },
+  {
+    interruptible: true,
+    sourceURL: null,
+    functionName: null
+  },
+];
+
+let receivedEvents = [];
+
+
+function onReflowActivity(aState, aType, aPacket)
+{
+  info("packet: " + aPacket.message);
+  receivedEvents.push(aPacket);
+  if (receivedEvents.length == expectedEvents.length) {
+    checkEvents();
+    finish(aState);
+  }
+}
+
+function checkEvents() {
+  for (let i = 0; i < expectedEvents.length; i++) {
+    let a = expectedEvents[i];
+    let b = receivedEvents[i];
+    for (let key in a) {
+      is(a[key], b[key], "field " + key + " is valid");
+    }
+  }
+}
+
+function finish(aState) {
+  client.removeListener("reflowActivity", onReflowActivity);
+  closeDebugger(aState, function() {
+    SimpleTest.finish();
+  });
+}
+
+addEventListener("load", startTest);
+
+</script>
+</body>
+</html>
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -1,17 +1,17 @@
 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
+const {Cc, Ci, Cu, components} = require("chrome");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "NetworkHelper", () => require("devtools/toolkit/webconsole/network-helper"));
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "ConsoleAPIStorage", "resource://gre/modules/ConsoleAPIStorage.jsm");
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 loader.lazyImporter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -2584,13 +2584,105 @@ ConsoleProgressListener.prototype = {
     }
 
     this._webProgress = null;
     this.window = null;
     this.owner = null;
   },
 };
 
+
+/**
+ * A ReflowObserver that listens for reflow events from the page.
+ * Implements nsIReflowObserver.
+ *
+ * @constructor
+ * @param object aWindow
+ *        The window for which we need to track reflow.
+ * @param object aOwner
+ *        The listener owner which needs to implement:
+ *        - onReflowActivity(aReflowInfo)
+ */
+
+function ConsoleReflowListener(aWindow, aListener)
+{
+  this.docshell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                         .getInterface(Ci.nsIWebNavigation)
+                         .QueryInterface(Ci.nsIDocShell);
+  this.listener = aListener;
+  this.docshell.addWeakReflowObserver(this);
+}
+
+exports.ConsoleReflowListener = ConsoleReflowListener;
+
+ConsoleReflowListener.prototype =
+{
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIReflowObserver,
+                                         Ci.nsISupportsWeakReference]),
+  docshell: null,
+  listener: null,
+
+  /**
+   * Forward reflow event to listener.
+   *
+   * @param DOMHighResTimeStamp aStart
+   * @param DOMHighResTimeStamp aEnd
+   * @param boolean aInterruptible
+   */
+  sendReflow: function CRL_sendReflow(aStart, aEnd, aInterruptible)
+  {
+    let frame = components.stack.caller.caller;
+
+    let filename = frame.filename;
+
+    if (filename) {
+      // Because filename could be of the form "xxx.js -> xxx.js -> xxx.js",
+      // we only take the last part.
+      filename = filename.split(" ").pop();
+    }
+
+    this.listener.onReflowActivity({
+      interruptible: aInterruptible,
+      start: aStart,
+      end: aEnd,
+      sourceURL: filename,
+      sourceLine: frame.lineNumber,
+      functionName: frame.name
+    });
+  },
+
+  /**
+   * On uninterruptible reflow
+   *
+   * @param DOMHighResTimeStamp aStart
+   * @param DOMHighResTimeStamp aEnd
+   */
+  reflow: function CRL_reflow(aStart, aEnd)
+  {
+    this.sendReflow(aStart, aEnd, false);
+  },
+
+  /**
+   * On interruptible reflow
+   *
+   * @param DOMHighResTimeStamp aStart
+   * @param DOMHighResTimeStamp aEnd
+   */
+  reflowInterruptible: function CRL_reflowInterruptible(aStart, aEnd)
+  {
+    this.sendReflow(aStart, aEnd, true);
+  },
+
+  /**
+   * Unregister listener.
+   */
+  destroy: function CRL_destroy()
+  {
+    this.docshell.removeWeakReflowObserver(this);
+    this.listener = this.docshell = null;
+  },
+};
+
 function gSequenceId()
 {
   return gSequenceId.n++;
 }
 gSequenceId.n = 0;
--- a/toolkit/locales/en-US/chrome/global/console.properties
+++ b/toolkit/locales/en-US/chrome/global/console.properties
@@ -4,8 +4,13 @@
 
 typeError=Error:
 typeWarning=Warning:
 errFile=Source File: %S
 errLine=Line: %S
 errLineCol=Line: %S, Column: %S
 errCode=Source Code:
 errTime=Timestamp: %S
+
+# LOCALIZATION NOTE (evaluationContextChanged): The message displayed when the
+# browser console's evaluation context (window against which input is evaluated)
+# changes.
+evaluationContextChanged=The console's evaluation context changed, probably because the target window was closed or because you opened a main window from the browser console's window.
--- a/toolkit/mozapps/downloads/nsHelperAppDlg.js
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js
@@ -98,16 +98,17 @@ const PREF_BD_USEDOWNLOADDIR = "browser.
 const nsITimer = Components.interfaces.nsITimer;
 
 let downloadModule = {};
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule);
 Components.utils.import("resource://gre/modules/DownloadPaths.jsm");
 Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
 Components.utils.import("resource://gre/modules/Downloads.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/Task.jsm");
 
 /* ctor
  */
 function nsUnknownContentTypeDialog() {
   // Initialize data properties.
   this.mLauncher = null;
   this.mContext  = null;
@@ -211,17 +212,18 @@ nsUnknownContentTypeDialog.prototype = {
         // folder without prompting. Note that preference might not be set.
         let autodownload = false;
         try {
           autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
         } catch (e) { }
 
         if (autodownload) {
           // Retrieve the user's default download directory
-          let defaultFolder = yield Downloads.getPreferredDownloadsDirectory();
+          let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+          let defaultFolder = new FileUtils.File(preferredDir);
 
           try {
             result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
           }
           catch (ex) {
             if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
               let prompter = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
                                         getService(Components.interfaces.nsIPromptService);
@@ -269,19 +271,20 @@ nsUnknownContentTypeDialog.prototype = {
       if (aSuggestedFileExtension) {
         wildCardExtension += aSuggestedFileExtension;
         picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
       }
 
       picker.appendFilters( nsIFilePicker.filterAll );
 
       // Default to lastDir if it is valid, otherwise use the user's default
-      // downloads directory.  userDownloadsDirectory should always return a
-      // valid directory, so we can safely default to it.
-      picker.displayDirectory = yield Downloads.getPreferredDownloadsDirectory();
+      // downloads directory.  getPreferredDownloadsDirectory should always 
+      // return a valid directory path, so we can safely default to it.
+      let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+      picker.displayDirectory = new FileUtils.File(preferredDir);
 
       gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) {
         if (lastDir && isUsableDirectory(lastDir))
           picker.displayDirectory = lastDir;
 
         if (picker.show() == nsIFilePicker.returnCancel) {
           // null result means user cancelled.
           aLauncher.saveDestinationAvailable(null);
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -22,16 +22,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                   "resource://gre/modules/PermissionsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ChromeRegistry",
                                    "@mozilla.org/chrome/chrome-registry;1",
                                    "nsIChromeRegistry");
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ResProtocolHandler",
                                    "@mozilla.org/network/protocol;1?name=resource",
@@ -98,16 +104,19 @@ const KEY_APP_SYSTEM_USER             = 
 
 const XPI_PERMISSION                  = "install";
 
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
+// The maximum amount of file data to buffer at a time during file extraction
+const EXTRACTION_BUFFER               = 1024 * 512;
+
 // The value for this is in Makefile.in
 #expand const DB_SCHEMA                       = __MOZ_EXTENSIONS_DB_SCHEMA__;
 
 // Properties that exist in the install manifest
 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
                             "updateKey", "optionsURL", "optionsType", "aboutURL",
                             "iconURL", "icon64URL"];
 const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"];
@@ -1106,16 +1115,150 @@ function getTemporaryFile() {
   let random = Math.random().toString(36).replace(/0./, '').substr(-3);
   file.append("tmp-" + random + ".xpi");
   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
 
   return file;
 }
 
 /**
+ * Asynchronously writes data from an nsIInputStream to an OS.File instance.
+ * The source stream and OS.File are closed regardless of whether the operation
+ * succeeds or fails.
+ * Returns a promise that will be resolved when complete.
+ *
+ * @param  aPath
+ *         The name of the file being extracted for logging purposes.
+ * @param  aStream
+ *         The source nsIInputStream.
+ * @param  aFile
+ *         The open OS.File instance to write to.
+ */
+function saveStreamAsync(aPath, aStream, aFile) {
+  let deferred = Promise.defer();
+
+  // Read the input stream on a background thread
+  let sts = Cc["@mozilla.org/network/stream-transport-service;1"].
+            getService(Ci.nsIStreamTransportService);
+  let transport = sts.createInputTransport(aStream, -1, -1, true);
+  let input = transport.openInputStream(0, 0, 0)
+                       .QueryInterface(Ci.nsIAsyncInputStream);
+  let source = Cc["@mozilla.org/binaryinputstream;1"].
+               createInstance(Ci.nsIBinaryInputStream);
+  source.setInputStream(input);
+
+  let data = Uint8Array(EXTRACTION_BUFFER);
+
+  function readFailed(error) {
+    try {
+      aStream.close();
+    }
+    catch (e) {
+      ERROR("Failed to close JAR stream for " + aPath);
+    }
+
+    aFile.close().then(function() {
+      deferred.reject(error);
+    }, function(e) {
+      ERROR("Failed to close file for " + aPath);
+      deferred.reject(error);
+    });
+  }
+
+  function readData() {
+    try {
+      let count = Math.min(source.available(), data.byteLength);
+      source.readArrayBuffer(count, data.buffer);
+
+      aFile.write(data, { bytes: count }).then(function() {
+        input.asyncWait(readData, 0, 0, Services.tm.currentThread);
+      }, readFailed);
+    }
+    catch (e if e.result == Cr.NS_BASE_STREAM_CLOSED) {
+      deferred.resolve(aFile.close());
+    }
+    catch (e) {
+      readFailed(e);
+    }
+  }
+
+  input.asyncWait(readData, 0, 0, Services.tm.currentThread);
+
+  return deferred.promise;
+}
+
+/**
+ * Asynchronously extracts files from a ZIP file into a directory.
+ * Returns a promise that will be resolved when the extraction is complete.
+ *
+ * @param  aZipFile
+ *         The source ZIP file that contains the add-on.
+ * @param  aDir
+ *         The nsIFile to extract to.
+ */
+function extractFilesAsync(aZipFile, aDir) {
+  let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+                  createInstance(Ci.nsIZipReader);
+  zipReader.open(aZipFile);
+
+  let promises = [];
+
+  // Get all of the entries in the zip and sort them so we create directories
+  // before files
+  let entries = zipReader.findEntries(null);
+  let names = [];
+  while (entries.hasMore())
+    names.push(entries.getNext());
+  names.sort();
+
+  for (let name of names) {
+    let entryName = name;
+    let zipentry = zipReader.getEntry(name);
+    let path = OS.Path.join(aDir.path, ...name.split("/"));
+
+    if (zipentry.isDirectory) {
+      promises.push(OS.File.makeDir(path).then(null, function(e) {
+        ERROR("extractFilesAsync: failed to create directory " + path, e);
+        throw e;
+      }));
+    }
+    else {
+      let options = { unixMode: zipentry.permissions | FileUtils.PERMS_FILE };
+      let promise = OS.File.open(path, { truncate: true }, options).then(function(file) {
+        if (zipentry.realSize == 0)
+          return file.close();
+
+        return saveStreamAsync(path, zipReader.getInputStream(entryName), file);
+      });
+
+      promises.push(promise.then(null, function(e) {
+        ERROR("extractFilesAsync: failed to extract file " + path, e);
+        throw e;
+      }));
+    }
+  }
+
+  // Will be rejected if any of the promises are rejected and resolved otherwise
+  let result = Promise.defer();
+
+  // If any promise is rejected then result is rejected, the resulting array of
+  // promises are all resolved though
+  promises = promises.map(p => p.then(null, result.reject));
+
+  // Wait for all of the promises to be resolved
+  return Promise.all(promises).then(function() {
+    // Resolve the result if it hasn't already been rejected
+    result.resolve();
+
+    zipReader.close();
+    return result.promise;
+  });
+}
+
+/**
  * Extracts files from a ZIP file into a directory.
  *
  * @param  aZipFile
  *         The source ZIP file that contains the add-on.
  * @param  aDir
  *         The nsIFile to extract to.
  */
 function extractFiles(aZipFile, aDir) {
@@ -1239,52 +1382,53 @@ function escapeAddonURI(aAddon, aUri, aU
     compatMode = "ignore";
   else if (AddonManager.strictCompatibility)
     compatMode = "strict";
   uri = uri.replace(/%COMPATIBILITY_MODE%/g, compatMode);
 
   return uri;
 }
 
-
-/**
- * Removes the specified files or directories in a staging directory and then if
- * the staging directory is empty attempts to remove it.
- *
- * @param  aDir
- *         nsIFile for the staging directory to clean up
- * @param  aLeafNames
- *         An array of file or directory to remove from the directory, the
- *         array may be empty
- */
-function cleanStagingDir(aDir, aLeafNames) {
-  aLeafNames.forEach(function(aName) {
-    let file = aDir.clone();
-    file.append(aName);
-    if (file.exists())
-      recursiveRemove(file);
+function recursiveRemoveAsync(aFile) {
+  return Task.spawn(function () {
+    let info = null;
+    try {
+      info = yield OS.File.stat(aFile.path);
+    }
+    catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+      // The file has already gone away
+      return;
+    }
+
+    setFilePermissions(aFile, info.isDir ? FileUtils.PERMS_DIRECTORY
+                                         : FileUtils.PERMS_FILE);
+
+    // OS.File means we have to recurse into directories
+    if (info.isDir) {
+      let iterator = new OS.File.DirectoryIterator(aFile.path);
+      yield iterator.forEach(function(entry) {
+        let nextFile = aFile.clone();
+        nextFile.append(entry.name);
+        return recursiveRemoveAsync(nextFile);
+      });
+      yield iterator.close();
+    }
+
+    try {
+      yield info.isDir ? OS.File.removeEmptyDir(aFile.path)
+                       : OS.File.remove(aFile.path);
+    }
+    catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+      // The file has already gone away
+    }
+    catch (e) {
+      ERROR("Failed to remove file " + aFile.path, e);
+      throw e;
+    }
   });
-
-  let dirEntries = aDir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
-  try {
-    if (dirEntries.nextFile)
-      return;
-  }
-  finally {
-    dirEntries.close();
-  }
-
-  try {
-    setFilePermissions(aDir, FileUtils.PERMS_DIRECTORY);
-    aDir.remove(false);
-  }
-  catch (e) {
-    WARN("Failed to remove staging dir", e);
-    // Failing to remove the staging directory is ignorable
-  }
 }
 
 /**
  * Recursively removes a directory or file fixing permissions when necessary.
  *
  * @param  aFile
  *         The nsIFile to remove
  */
@@ -1295,16 +1439,18 @@ function recursiveRemove(aFile) {
     isDir = aFile.isDirectory();
   }
   catch (e) {
     // If the file has already gone away then don't worry about it, this can
     // happen on OSX where the resource fork is automatically moved with the
     // data fork for the file. See bug 733436.
     if (e.result == Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
       return;
+    if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND)
+      return;
 
     throw e;
   }
 
   setFilePermissions(aFile, isDir ? FileUtils.PERMS_DIRECTORY
                                   : FileUtils.PERMS_FILE);
 
   try {
@@ -2371,17 +2517,17 @@ var XPIProvider = {
                                      oldBootstrap.type, existingAddon, "install",
                                      BOOTSTRAP_REASONS.ADDON_INSTALL);
           }
           continue;
         }
       }
 
       try {
-        cleanStagingDir(stagingDir, seenFiles);
+        aLocation.cleanStagingDir(seenFiles);
       }
       catch (e) {
         // Non-critical, just saves some perf on startup if we clean this up.
         LOG("Error cleaning staging dir " + stagingDir.path, e);
       }
     }, this);
     return changed;
   },
@@ -4281,17 +4427,17 @@ var XPIProvider = {
    *
    * @param  aAddon
    *         The DBAddonInternal to cancel uninstall for
    */
   cancelUninstallAddon: function XPI_cancelUninstallAddon(aAddon) {
     if (!(aAddon.inDatabase))
       throw new Error("Can only cancel uninstall for installed addons.");
 
-    cleanStagingDir(aAddon._installLocation.getStagingDir(), [aAddon.id]);
+    aAddon._installLocation.cleanStagingDir([aAddon.id]);
 
     XPIDatabase.setAddonProperties(aAddon, {
       pendingUninstall: false
     });
 
     if (!aAddon.visible)
       return;
 
@@ -4602,19 +4748,18 @@ AddonInstall.prototype = {
                                                this.listeners, this.wrapper);
       this.removeTemporaryFile();
       break;
     case AddonManager.STATE_INSTALLED:
       LOG("Cancelling install of " + this.addon.id);
       let xpi = this.installLocation.getStagingDir();
       xpi.append(this.addon.id + ".xpi");
       flushJarCache(xpi);
-      cleanStagingDir(this.installLocation.getStagingDir(),
-                      [this.addon.id, this.addon.id + ".xpi",
-                       this.addon.id + ".json"]);
+      this.installLocation.cleanStagingDir([this.addon.id, this.addon.id + ".xpi",
+                                            this.addon.id + ".json"]);
       this.state = AddonManager.STATE_CANCELLED;
       XPIProvider.removeActiveInstall(this);
 
       if (this.existingAddon) {
         delete this.existingAddon.pendingUpgrade;
         this.existingAddon.pendingUpgrade = null;
       }
 
@@ -5239,37 +5384,38 @@ AddonInstall.prototype = {
     let isUpgrade = this.existingAddon &&
                     this.existingAddon._installLocation == this.installLocation;
     let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
 
     LOG("Starting install of " + this.sourceURI.spec);
     AddonManagerPrivate.callAddonListeners("onInstalling",
                                            createWrapper(this.addon),
                                            requiresRestart);
-    let stagedAddon = this.installLocation.getStagingDir();
-
-    try {
+
+    let stagingDir = this.installLocation.getStagingDir();
+    let stagedAddon = stagingDir.clone();
+
+    Task.spawn((function() {
+      yield this.installLocation.requestStagingDir();
+
       // First stage the file regardless of whether restarting is necessary
       if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) {
         LOG("Addon " + this.addon.id + " will be installed as " +
             "an unpacked directory");
         stagedAddon.append(this.addon.id);
-        if (stagedAddon.exists())
-          recursiveRemove(stagedAddon);
-        stagedAddon.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
-        extractFiles(this.file, stagedAddon);
+        yield recursiveRemoveAsync(stagedAddon);
+        yield OS.File.makeDir(stagedAddon.path);
+        yield extractFilesAsync(this.file, stagedAddon);
       }
       else {
         LOG("Addon " + this.addon.id + " will be installed as " +
             "a packed xpi");
         stagedAddon.append(this.addon.id + ".xpi");
-        if (stagedAddon.exists())
-          stagedAddon.remove(true);
-        this.file.copyTo(this.installLocation.getStagingDir(),
-                         this.addon.id + ".xpi");
+        yield recursiveRemoveAsync(stagedAddon);
+        yield OS.File.copy(this.file.path, stagedAddon.path);
       }
 
       if (requiresRestart) {
         // Point the add-on to its extracted files as the xpi may get deleted
         this.addon._sourceBundle = stagedAddon;
 
         // Cache the AddonInternal as it may have updated compatibility info
         let stagedJSON = stagedAddon.clone();
@@ -5343,17 +5489,16 @@ AddonInstall.prototype = {
             XPIDatabase.updateAddonActive(this.existingAddon, false);
           }
         }
 
         // Install the new add-on into its final location
         let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
         let file = this.installLocation.installAddon(this.addon.id, stagedAddon,
                                                      existingAddonID);
-        cleanStagingDir(stagedAddon.parent, []);
 
         // Update the metadata in the database
         this.addon._sourceBundle = file;
         this.addon._installLocation = this.installLocation;
         let [mFile, mTime] = recursiveLastModifiedTime(file);
         this.addon.updateDate = mTime;
         this.addon.visible = true;
         if (isUpgrade) {
@@ -5394,31 +5539,30 @@ AddonInstall.prototype = {
           }
           else {
             // XXX this makes it dangerous to do many things in onInstallEnded
             // listeners because important cleanup hasn't been done yet
             XPIProvider.unloadBootstrapScope(this.addon.id);
           }
         }
       }
-    }
-    catch (e) {
-      WARN("Failed to install", e);
+    }).bind(this)).then(null, (e) => {
+      WARN("Failed to install " + this.file.path + " from " + this.sourceURI.spec, e);
       if (stagedAddon.exists())
         recursiveRemove(stagedAddon);
       this.state = AddonManager.STATE_INSTALL_FAILED;
       this.error = AddonManager.ERROR_FILE_ACCESS;
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callInstallListeners("onInstallFailed",
                                                this.listeners,
                                                this.wrapper);
-    }
-    finally {
+    }).then(() => {
       this.removeTemporaryFile();
-    }
+      return this.installLocation.releaseStagingDir();
+    });
   },
 
   getInterface: function AI_getInterface(iid) {
     if (iid.equals(Ci.nsIAuthPrompt2)) {
       var factory = Cc["@mozilla.org/prompter;1"].
                     getService(Ci.nsIPromptFactory);
       return factory.getPrompt(this.window, Ci.nsIAuthPrompt);
     }
@@ -6511,16 +6655,17 @@ function AddonWrapper(aAddon) {
 function DirectoryInstallLocation(aName, aDirectory, aScope, aLocked) {
   this._name = aName;
   this.locked = aLocked;
   this._directory = aDirectory;
   this._scope = aScope
   this._IDToFileMap = {};
   this._FileToIDMap = {};
   this._linkedAddons = [];
+  this._stagingDirLock = 0;
 
   if (!aDirectory.exists())
     return;
   if (!aDirectory.isDirectory())
     throw new Error("Location must be a directory.");
 
   this._readAddons();
 }
@@ -6658,16 +6803,82 @@ DirectoryInstallLocation.prototype = {
    * @return an nsIFile
    */
   getStagingDir: function DirInstallLocation_getStagingDir() {
     let dir = this._directory.clone();
     dir.append(DIR_STAGE);
     return dir;
   },
 
+  requestStagingDir: function() {
+    this._stagingDirLock++;
+
+    if (this._stagingDirPromise)
+      return this._stagingDirPromise;
+
+    OS.File.makeDir(this._directory.path);
+    let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+    return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
+      if (e instanceof OS.File.Error && e.becauseExists)
+        return;
+      ERROR("Failed to create staging directory", e);
+      throw e;
+    });
+  },
+
+  releaseStagingDir: function() {
+    this._stagingDirLock--;
+
+    if (this._stagingDirLock == 0) {
+      this._stagingDirPromise = null;
+      this.cleanStagingDir();
+    }
+
+    return Promise.resolve();
+  },
+
+  /**
+   * Removes the specified files or directories in the staging directory and
+   * then if the staging directory is empty attempts to remove it.
+   *
+   * @param  aLeafNames
+   *         An array of file or directory to remove from the directory, the
+   *         array may be empty
+   */
+  cleanStagingDir: function(aLeafNames = []) {
+    let dir = this.getStagingDir();
+
+    for (let name of aLeafNames) {
+      let file = dir.clone();
+      file.append(name);
+      recursiveRemove(file);
+    }
+
+    if (this.stagingDirLock > 0)
+      return;
+
+    let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+    try {
+      if (dirEntries.nextFile)
+        return;
+    }
+    finally {
+      dirEntries.close();
+    }
+
+    try {
+      setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
+      dir.remove(false);
+    }
+    catch (e) {
+      WARN("Failed to remove staging dir", e);
+      // Failing to remove the staging directory is ignorable
+    }
+  },
+
   /**
    * Gets the directory used by old versions for staging XPI and JAR files ready
    * to be installed.
    *
    * @return an nsIFile
    */
   getXPIStagingDir: function DirInstallLocation_getXPIStagingDir() {
     let dir = this._directory.clone();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
@@ -8,16 +8,17 @@ const ADDON_ENABLE                    = 
 const ADDON_DISABLE                   = 4;
 const ADDON_INSTALL                   = 5;
 const ADDON_UNINSTALL                 = 6;
 const ADDON_UPGRADE                   = 7;
 const ADDON_DOWNGRADE                 = 8;
 
 // This verifies that bootstrappable add-ons can be used without restarts.
 Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
 
 // Enable loading extensions from the user scopes
 Services.prefs.setIntPref("extensions.enabledScopes",
                           AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
 const profileDir = gProfD.clone();
@@ -53,16 +54,34 @@ function waitForPref(aPref, aCallback) {
   function prefChanged() {
     Services.prefs.removeObserver(aPref, prefChanged);
     // Always let whoever set the preference keep running
     do_execute_soon(aCallback);
   }
   Services.prefs.addObserver(aPref, prefChanged, false);
 }
 
+function promisePref(aPref) {
+  let deferred = Promise.defer();
+
+  waitForPref(aPref, deferred.resolve.bind(deferred));
+
+  return deferred.promise;
+}
+
+function promiseInstall(aFiles) {
+  let deferred = Promise.defer();
+
+  installAllFiles(aFiles, function() {
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
+
 function getActiveVersion() {
   return Services.prefs.getIntPref("bootstraptest.active_version");
 }
 
 function getInstalledVersion() {
   return Services.prefs.getIntPref("bootstraptest.installed_version");
 }
 
@@ -1219,17 +1238,20 @@ function check_test_23() {
     });
   });
 }
 
 // Tests that we recover from a broken preference
 function run_test_24() {
   resetPrefs();
   do_print("starting 24");
-  waitForPref("bootstraptest2.active_version", function test_24_pref() {
+
+  Promise.all([promisePref("bootstraptest2.active_version"),
+              promiseInstall([do_get_addon("test_bootstrap1_1"), do_get_addon("test_bootstrap2_1")])])
+         .then(function test_24_pref() {
     do_print("test 24 got prefs");
     do_check_eq(getInstalledVersion(), 1);
     do_check_eq(getActiveVersion(), 1);
     do_check_eq(getInstalledVersion2(), 1);
     do_check_eq(getActiveVersion2(), 1);
 
     resetPrefs();
 
@@ -1256,21 +1278,16 @@ function run_test_24() {
 
     do_check_eq(getInstalledVersion(), -1);
     do_check_eq(getActiveVersion(), 1);
     do_check_eq(getInstalledVersion2(), -1);
     do_check_eq(getActiveVersion2(), 1);
 
     run_test_25();
   });
-
-  installAllFiles([do_get_addon("test_bootstrap1_1"), do_get_addon("test_bootstrap2_1")],
-                  function test_24_installed() {
-    do_print("test 24 installed");
-  });
 }
 
 // Tests that updating from a bootstrappable add-on to a normal add-on calls
 // the uninstall method
 function run_test_25() {
   waitForPref("bootstraptest.startup_reason", function test_25_after_pref() {
       do_print("test 25 pref change detected");
       do_check_eq(getInstalledVersion(), 1);
--- a/widget/MiscEvents.h
+++ b/widget/MiscEvents.h
@@ -58,16 +58,27 @@ public:
     uint8_t mUnit;      // [in]
     bool mIsHorizontal; // [in]
   } mScroll;
 
   bool mOnlyEnabledCheck; // [in]
 
   bool mSucceeded; // [out]
   bool mIsEnabled; // [out]
+
+  void AssignContentCommandEventData(const WidgetContentCommandEvent& aEvent,
+                                     bool aCopyTargets)
+  {
+    AssignGUIEventData(aEvent, aCopyTargets);
+
+    mScroll = aEvent.mScroll;
+    mOnlyEnabledCheck = aEvent.mOnlyEnabledCheck;
+    mSucceeded = aEvent.mSucceeded;
+    mIsEnabled = aEvent.mIsEnabled;
+  }
 };
 
 /******************************************************************************
  * mozilla::WidgetCommandEvent
  *
  * This sends a command to chrome.  If you want to request what is performed
  * in focused content, you should use WidgetContentCommandEvent instead.
  *
--- a/widget/windows/WinMouseScrollHandler.cpp
+++ b/widget/windows/WinMouseScrollHandler.cpp
@@ -323,17 +323,18 @@ MouseScrollHandler::SynthesizeNativeMous
            Synthesize(pts, target, aNativeMessage, wParam, lParam, kbdState);
 }
 
 /* static */
 bool
 MouseScrollHandler::DispatchEvent(nsWindowBase* aWidget,
                                   WidgetGUIEvent& aEvent)
 {
-  return aWidget->DispatchWindowEvent(&aEvent);
+  // note, in metrofx, this will always return false for now
+  return aWidget->DispatchScrollEvent(&aEvent);
 }
 
 /* static */
 void
 MouseScrollHandler::InitEvent(nsWindowBase* aWidget,
                               WidgetGUIEvent& aEvent,
                               nsIntPoint* aPoint)
 {
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -3676,16 +3676,23 @@ bool nsWindow::DispatchStandardEvent(uin
 
 bool nsWindow::DispatchKeyboardEvent(WidgetGUIEvent* event)
 {
   nsEventStatus status;
   DispatchEvent(event, status);
   return ConvertStatus(status);
 }
 
+bool nsWindow::DispatchScrollEvent(WidgetGUIEvent* event)
+{
+  nsEventStatus status;
+  DispatchEvent(event, status);
+  return ConvertStatus(status);
+}
+
 bool nsWindow::DispatchWindowEvent(WidgetGUIEvent* event)
 {
   nsEventStatus status;
   DispatchEvent(event, status);
   return ConvertStatus(status);
 }
 
 bool nsWindow::DispatchWindowEvent(WidgetGUIEvent* event,
--- a/widget/windows/nsWindow.h
+++ b/widget/windows/nsWindow.h
@@ -86,16 +86,17 @@ public:
 
   friend class nsWindowGfx;
 
   // nsWindowBase
   virtual void InitEvent(mozilla::WidgetGUIEvent& aEvent,
                          nsIntPoint* aPoint = nullptr) MOZ_OVERRIDE;
   virtual bool DispatchWindowEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
   virtual bool DispatchKeyboardEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
+  virtual bool DispatchScrollEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
   virtual nsWindowBase* GetParentWindowBase(bool aIncludeOwner) MOZ_OVERRIDE;
   virtual bool IsTopLevelWidget() MOZ_OVERRIDE { return mIsTopWidgetWindow; }
 
   // nsIWidget interface
   NS_IMETHOD              Create(nsIWidget *aParent,
                                  nsNativeWidget aNativeParent,
                                  const nsIntRect &aRect,
                                  nsDeviceContext *aContext,
--- a/widget/windows/nsWindowBase.h
+++ b/widget/windows/nsWindowBase.h
@@ -51,16 +51,23 @@ public:
   /*
    * Dispatch a gecko keyboard event for this widget. This
    * is called by KeyboardLayout to dispatch gecko events.
    * Returns true if it's consumed.  Otherwise, false.
    */
   virtual bool DispatchKeyboardEvent(mozilla::WidgetGUIEvent* aEvent) = 0;
 
   /*
+   * Dispatch a gecko scroll event for this widget. This
+   * is called by ScrollHandler to dispatch gecko events.
+   * Returns true if it's consumed.  Otherwise, false.
+   */
+  virtual bool DispatchScrollEvent(mozilla::WidgetGUIEvent* aEvent) = 0;
+
+  /*
    * Default dispatch of a plugin event.
    */
   virtual bool DispatchPluginEvent(const MSG& aMsg);
 
   /*
    * Returns true if a plugin has focus on this widget.  Otherwise, false.
    */
   virtual bool PluginHasFocus() const MOZ_FINAL
--- a/widget/windows/winrt/MetroWidget.cpp
+++ b/widget/windows/winrt/MetroWidget.cpp
@@ -29,16 +29,17 @@
 #ifdef MOZ_CRASHREPORTER
 #include "nsExceptionHandler.h"
 #endif
 #include "UIABridgePrivate.h"
 #include "WinMouseScrollHandler.h"
 #include "InputData.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
+#include "mozilla/MiscEvents.h"
 
 using namespace Microsoft::WRL;
 using namespace Microsoft::WRL::Wrappers;
 
 using namespace mozilla;
 using namespace mozilla::widget;
 using namespace mozilla::layers;
 using namespace mozilla::widget::winrt;
@@ -562,78 +563,61 @@ CloseGesture()
     do_GetService(NS_APPSTARTUP_CONTRACTID);
   if (appStartup) {
     appStartup->Quit(nsIAppStartup::eForceQuit);
   }
 }
 
 // Async event sending for mouse and keyboard input.
 
-// Simple Windows message wrapper for dispatching async events. 
-class DispatchMsg
-{
-public:
-  DispatchMsg(UINT aMsg, WPARAM aWParam, LPARAM aLParam) :
-    mMsg(aMsg),
-    mWParam(aWParam),
-    mLParam(aLParam)
-  {
-  }
-  ~DispatchMsg()
-  {
-  }
-
-  UINT mMsg;
-  WPARAM mWParam;
-  LPARAM mLParam;
-};
-
-DispatchMsg*
-MetroWidget::CreateDispatchMsg(UINT aMsg, WPARAM aWParam, LPARAM aLParam)
+// defined in nsWindowBase, called from shared module WinMouseScrollHandler.
+bool
+MetroWidget::DispatchScrollEvent(mozilla::WidgetGUIEvent* aEvent)
 {
-  switch (aMsg) {
-    case WM_SETTINGCHANGE:
-    case WM_MOUSEWHEEL:
-    case WM_MOUSEHWHEEL:
-    case WM_HSCROLL:
-    case WM_VSCROLL:
-    case MOZ_WM_HSCROLL:
-    case MOZ_WM_VSCROLL:
-    case WM_KEYDOWN:
-    case WM_KEYUP:
-    // MOZ_WM events are plugin specific, we keep them for completness
-    case MOZ_WM_MOUSEVWHEEL:
-    case MOZ_WM_MOUSEHWHEEL:
-      return new DispatchMsg(aMsg, aWParam, aLParam);
+  WidgetGUIEvent* newEvent = nullptr;
+  switch(aEvent->eventStructType) {
+    case NS_WHEEL_EVENT:
+    {
+      WidgetWheelEvent* oldEvent = aEvent->AsWheelEvent();
+      WidgetWheelEvent* wheelEvent =
+        new WidgetWheelEvent(oldEvent->mFlags.mIsTrusted, oldEvent->message, oldEvent->widget);
+      wheelEvent->AssignWheelEventData(*oldEvent, true);
+      newEvent = static_cast<WidgetGUIEvent*>(wheelEvent);
+    }
+    break;
+    case NS_CONTENT_COMMAND_EVENT:
+    {
+      WidgetContentCommandEvent* oldEvent = aEvent->AsContentCommandEvent();
+      WidgetContentCommandEvent* cmdEvent =
+        new WidgetContentCommandEvent(oldEvent->mFlags.mIsTrusted, oldEvent->message, oldEvent->widget);
+      cmdEvent->AssignContentCommandEventData(*oldEvent, true);
+      newEvent = static_cast<WidgetGUIEvent*>(cmdEvent);
+    }
+    break;
     default:
-      MOZ_CRASH("Unknown event being passed to CreateDispatchMsg.");
-      return nullptr;
+      MOZ_CRASH("unknown event in DispatchScrollEvent");
+    break;
   }
-}
-
-void
-MetroWidget::DispatchAsyncScrollEvent(DispatchMsg* aEvent)
-{
-  mMsgEventQueue.Push(aEvent);
+  mEventQueue.Push(newEvent);
   nsCOMPtr<nsIRunnable> runnable =
     NS_NewRunnableMethod(this, &MetroWidget::DeliverNextScrollEvent);
   NS_DispatchToCurrentThread(runnable);
+  return false;
 }
 
 void
 MetroWidget::DeliverNextScrollEvent()
 {
-  DispatchMsg* msg = static_cast<DispatchMsg*>(mMsgEventQueue.PopFront());
-  MOZ_ASSERT(msg);
-  MSGResult msgResult;
-  MouseScrollHandler::ProcessMessage(this, msg->mMsg, msg->mWParam, msg->mLParam, msgResult);
-  delete msg;
+  WidgetGUIEvent* event =
+    static_cast<WidgetInputEvent*>(mEventQueue.PopFront());
+  DispatchWindowEvent(event);
+  delete event;
 }
 
-// defined in nsWiondowBase, called from shared module KeyboardLayout.
+// defined in nsWindowBase, called from shared module KeyboardLayout.
 bool
 MetroWidget::DispatchKeyboardEvent(WidgetGUIEvent* aEvent)
 {
   MOZ_ASSERT(aEvent);
   WidgetKeyboardEvent* oldKeyEvent = aEvent->AsKeyboardEvent();
   WidgetKeyboardEvent* keyEvent =
     new WidgetKeyboardEvent(oldKeyEvent->mFlags.mIsTrusted,
                             oldKeyEvent->message, oldKeyEvent->widget);
@@ -707,24 +691,20 @@ MetroWidget::WindowProcedure(HWND aWnd, 
 
   // Indicates if we should hand messages to the default windows
   // procedure for processing.
   bool processDefault = true;
 
   // The result returned if we do not do default processing.
   LRESULT processResult = 0;
 
-  // We ignore return results from the scroll module and pass everything
-  // to mMetroWndProc. These fall through to winrt handlers that generate
-  // input events in MetroInput. Since we have no listeners for scroll
-  // events no processing should occur. For now processDefault must be left
-  // true since the mouse module consumes non-mouse wheel related events.
-  if (MouseScrollHandler::NeedsMessage(aMsg)) {
-    DispatchMsg* msg = CreateDispatchMsg(aMsg, aWParam, aLParam);
-    DispatchAsyncScrollEvent(msg);
+  MSGResult msgResult(&processResult);
+  MouseScrollHandler::ProcessMessage(this, aMsg, aWParam, aLParam, msgResult);
+  if (msgResult.mConsumed) {
+    return processResult;
   }
 
   switch (aMsg) {
     case WM_PAINT:
     {
       HRGN rgn = CreateRectRgn(0, 0, 0, 0);
       GetUpdateRgn(mWnd, rgn, false);
       nsIntRegion region = WinUtils::ConvertHRGNToRegion(rgn);
--- a/widget/windows/winrt/MetroWidget.h
+++ b/widget/windows/winrt/MetroWidget.h
@@ -69,16 +69,17 @@ public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIOBSERVER
 
   static HWND GetICoreWindowHWND() { return sICoreHwnd; }
 
   // nsWindowBase
   virtual bool DispatchWindowEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
   virtual bool DispatchKeyboardEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
+  virtual bool DispatchScrollEvent(mozilla::WidgetGUIEvent* aEvent) MOZ_OVERRIDE;
   virtual bool DispatchPluginEvent(const MSG &aMsg) MOZ_OVERRIDE { return false; }
   virtual bool IsTopLevelWidget() MOZ_OVERRIDE { return true; }
   virtual nsWindowBase* GetParentWindowBase(bool aIncludeOwner) MOZ_OVERRIDE { return nullptr; }
   // InitEvent assumes physical coordinates and is used by shared win32 code. Do
   // not hand winrt event coordinates to this routine.
   virtual void InitEvent(mozilla::WidgetGUIEvent& aEvent,
                          nsIntPoint* aPoint = nullptr) MOZ_OVERRIDE;
 
@@ -246,12 +247,12 @@ protected:
   nsTransparencyMode mTransparencyMode;
   nsIntRegion mInvalidatedRegion;
   nsCOMPtr<nsIdleService> mIdleService;
   HWND mWnd;
   static HWND sICoreHwnd;
   WNDPROC mMetroWndProc;
   bool mTempBasicLayerInUse;
   uint64_t mRootLayerTreeId;
-  nsDeque mMsgEventQueue;
+  nsDeque mEventQueue;
   nsDeque mKeyEventQueue;
   nsRefPtr<APZController> mController;
 };