Merge m-c to fx-team
authorPanos Astithas <past@mozilla.com>
Thu, 04 Oct 2012 09:54:25 +0300
changeset 109283 4cb8f88213f5e8c108103f7f167463bedd798542
parent 109176 0a095af171f4fa1fa6509727e0f9daaf15d5ba88 (current diff)
parent 109282 7129a1d713f43cf7834e07b937cbd31c3a38562d (diff)
child 109284 0d90d5f3da5a9539fa4cf9b2aec0a218fea43e05
child 109656 b34419698e9690c61da628e316a87bbcfb9c9392
child 111067 7e9d2d39d6ede5f14c7f9773e5df39fbc672fa2c
push id15951
push userryanvm@gmail.com
push dateThu, 04 Oct 2012 23:17:21 +0000
treeherdermozilla-inbound@e1c62d51d0ba [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone18.0a1
first release with
nightly linux32
4cb8f88213f5 / 18.0a1 / 20121004030525 / files
nightly linux64
4cb8f88213f5 / 18.0a1 / 20121004030525 / files
nightly mac
4cb8f88213f5 / 18.0a1 / 20121004030525 / files
nightly win32
4cb8f88213f5 / 18.0a1 / 20121004030525 / files
nightly win64
4cb8f88213f5 / 18.0a1 / 20121004030525 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to fx-team
browser/devtools/shared/Promise.jsm
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -517,17 +517,17 @@
               <menu id="webDeveloperMenu"
                     label="&webDeveloperMenu.label;"
                     accesskey="&webDeveloperMenu.accesskey;">
                 <menupopup id="menuWebDeveloperPopup">
                   <menuitem id="menu_devToolbar" observes="devtoolsMenuBroadcaster_DevToolbar" accesskey="&devToolbarMenu.accesskey;"/>
                   <menuitem id="webConsole" observes="devtoolsMenuBroadcaster_WebConsole" accesskey="&webConsoleCmd.accesskey;"/>
                   <menuitem id="menu_pageinspect" observes="devtoolsMenuBroadcaster_Inspect" accesskey="&inspectMenu.accesskey;"/>
                   <menuitem id="menu_responsiveUI" observes="devtoolsMenuBroadcaster_ResponsiveUI" accesskey="&responsiveDesignTool.accesskey;"/>
-                  <menuitem id="menu_debugger" observes="devtoolsMenuBroadcaster_Debugger"/>
+                  <menuitem id="menu_debugger" observes="devtoolsMenuBroadcaster_Debugger" accesskey="&debuggerMenu.accesskey;"/>
                   <menuitem id="menu_remoteDebugger" observes="devtoolsMenuBroadcaster_RemoteDebugger"/>
                   <menuitem id="menu_chromeDebugger" observes="devtoolsMenuBroadcaster_ChromeDebugger"/>
                   <menuitem id="menu_scratchpad" observes="devtoolsMenuBroadcaster_Scratchpad" accesskey="&scratchpad.accesskey;"/>
                   <menuitem id="menu_styleeditor" observes="devtoolsMenuBroadcaster_StyleEditor" accesskey="&styleeditor.accesskey;"/>
                   <menuitem id="menu_pageSource" observes="devtoolsMenuBroadcaster_PageSource" accesskey="&pageSourceCmd.accesskey;"/>
                   <menuitem id="javascriptConsole" observes="devtoolsMenuBroadcaster_ErrorConsole" accesskey="&errorConsoleCmd.accesskey;"/>
                   <menuseparator id="devToolsEndSeparator"/>
                   <menuitem id="getMoreDevtools" observes="devtoolsMenuBroadcaster_GetMoreTools" accesskey="&getMoreDevtoolsCmd.accesskey;"/>
--- a/browser/devtools/commandline/CmdDbg.jsm
+++ b/browser/devtools/commandline/CmdDbg.jsm
@@ -13,16 +13,57 @@ Cu.import("resource://gre/modules/XPCOMU
  * 'dbg' command
  */
 gcli.addCommand({
   name: "dbg",
   description: gcli.lookup("dbgDesc"),
   manual: gcli.lookup("dbgManual")
 });
 
+/**
+ * 'dbg open' command
+ */
+gcli.addCommand({
+  name: "dbg open",
+  description: gcli.lookup("dbgOpen"),
+  params: [],
+  exec: function (args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let tab = win.gBrowser.selectedTab;
+    let dbg = win.DebuggerUI.findDebugger();
+
+    if (dbg) {
+      if (dbg.ownerTab !== tab) {
+        win.DebuggerUI.toggleDebugger();
+      }
+
+      return;
+    }
+
+    win.DebuggerUI.toggleDebugger();
+  }
+});
+
+/**
+ * 'dbg close' command
+ */
+gcli.addCommand({
+  name: "dbg close",
+  description: gcli.lookup("dbgClose"),
+  params: [],
+  exec: function (args, context) {
+    let win = context.environment.chromeDocument.defaultView;
+    let tab = win.gBrowser.selectedTab;
+    let dbg = win.DebuggerUI.findDebugger();
+
+    if (dbg) {
+      dbg.close();
+    }
+  }
+});
 
 /**
  * 'dbg interrupt' command
  */
 gcli.addCommand({
   name: "dbg interrupt",
   description: gcli.lookup("dbgInterrupt"),
   params: [],
--- a/browser/devtools/commandline/test/browser_dbg_cmd.js
+++ b/browser/devtools/commandline/test/browser_dbg_cmd.js
@@ -3,18 +3,23 @@ function test() {
                    "test/browser_dbg_cmd.html";
 
   DeveloperToolbarTest.test(TEST_URI, function() {
     testDbgCmd();
   });
 }
 
 function testDbgCmd() {
-  let pane = DebuggerUI.toggleDebugger();
-  ok(pane, "toggleDebugger() should return a pane.");
+  DeveloperToolbarTest.exec({
+    typed: "dbg open",
+    blankOutput: true
+  });
+
+  let pane = DebuggerUI.findDebugger();
+  ok(pane, "Debugger was opened.");
   let frame = pane._frame;
 
   frame.addEventListener("Debugger:Connecting", function dbgConnected(aEvent) {
     frame.removeEventListener("Debugger:Connecting", dbgConnected, true);
 
     // Wait for the initial resume...
     aEvent.target.ownerDocument.defaultView.gClient
         .addOneTimeListener("resumed", function() {
@@ -36,19 +41,24 @@ function testDbgCmd() {
                   is(output.value, "step in", "debugger stepped in");
                   cmd("dbg step over", function() {
                     is(output.value, "step over", "debugger stepped over");
                     cmd("dbg step out", function() {
                       is(output.value, "step out", "debugger stepped out");
                       cmd("dbg continue", function() {
                         cmd("dbg continue", function() {
                           is(output.value, "dbg continue", "debugger continued");
-                          pane.contentWindow.gClient.close(function() {
-                            finish();
+                          DeveloperToolbarTest.exec({
+                            typed: "dbg close",
+                            blankOutput: true
                           });
+
+                          let dbg = DebuggerUI.findDebugger();
+                          ok(!dbg, "Debugger was closed.");
+                          finish();
                         });
                       });
                     });
                   });
                 });
               });
             });
           });
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -4,16 +4,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/. */
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const NEW_SCRIPT_DISPLAY_DELAY = 100; // ms
 const FRAME_STEP_CACHE_DURATION = 100; // ms
 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
 const SCRIPTS_URL_MAX_LENGTH = 64; // chars
 const SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
 
 Cu.import("resource:///modules/source-editor.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
@@ -924,33 +925,43 @@ SourceScripts.prototype = {
    * Handler for the debugger client's unsolicited newScript notification.
    */
   _onNewScript: function SS__onNewScript(aNotification, aPacket) {
     // Ignore scripts generated from 'clientEvaluate' packets.
     if (aPacket.url == "debugger eval code") {
       return;
     }
 
-    this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
+    this._addScript({
+      url: aPacket.url,
+      startLine: aPacket.startLine,
+      source: aPacket.source
+    }, true);
 
     let preferredScriptUrl = DebuggerView.Scripts.preferredScriptUrl;
 
     // Select this script if it's the preferred one.
     if (aPacket.url === DebuggerView.Scripts.preferredScriptUrl) {
       DebuggerView.Scripts.selectScript(aPacket.url);
     }
-    // ..or the first entry if there's not one selected yet.
-    else if (!DebuggerView.Scripts.selected) {
-      DebuggerView.Scripts.selectIndex(0);
-      // Selecting a script would make it "preferred", which is a lie here,
-      // because we're only displaying a script to make sure there's always
-      // something available in the SourceEditor and the scripts menulist.
-      // Hence the need revert back to the initial preferred script, just
-      // in case it will be available soon.
-      DebuggerView.Scripts.preferredScriptUrl = preferredScriptUrl;
+    // ..or the first entry if there's none selected yet after a while
+    else {
+      window.setTimeout(function() {
+        // If after a certain delay the preferred script still wasn't received,
+        // just give up on waiting and display the first entry.
+        if (!DebuggerView.Scripts.selected) {
+          DebuggerView.Scripts.selectIndex(0);
+          // Selecting a script would make it "preferred", which is a lie here,
+          // because we're only displaying a script to make sure there's always
+          // something available in the SourceEditor and the scripts menulist.
+          // Hence the need revert back to the initial preferred script, just
+          // in case it will be available soon.
+          DebuggerView.Scripts.preferredScriptUrl = preferredScriptUrl;
+        }
+      }, NEW_SCRIPT_DISPLAY_DELAY);
     }
 
     // If there are any stored breakpoints for this script, display them again,
     // both in the editor and the pane.
     for each (let breakpoint in DebuggerController.Breakpoints.store) {
       if (breakpoint.location.url == aPacket.url) {
         DebuggerController.Breakpoints.displayBreakpoint(breakpoint);
       }
@@ -1201,28 +1212,32 @@ SourceScripts.prototype = {
    * @param object aScript
    *        The script object coming from the active thread.
    * @param object aOptions [optional]
    *        Additional options for showing the script. Supported options:
    *        - targetLine: place the editor at the given line number.
    */
   showScript: function SS_showScript(aScript, aOptions = {}) {
     if (aScript.loaded) {
-      this._onShowScript(aScript, aOptions);
+      // Scripts may take a longer time to load than expected, therefore the
+      // required one may change at any time after a previous request was made.
+      if (aScript.url === DebuggerView.Scripts.selected) {
+        this._onShowScript(aScript, aOptions);
+      }
       return;
     }
 
     let editor = DebuggerView.editor;
     editor.setMode(SourceEditor.MODES.TEXT);
     editor.setText(L10N.getStr("loadingText"));
     editor.resetUndo();
 
     // Notify that we need to load a script file.
     DebuggerController.dispatchEvent("Debugger:LoadSource", {
-      url: aScript.url,
+      script: aScript,
       options: aOptions
     });
   },
 
   /**
    * Display the script source once it loads.
    *
    * @private
@@ -1251,128 +1266,63 @@ SourceScripts.prototype = {
 
     // Notify that we shown script file.
     DebuggerController.dispatchEvent("Debugger:ScriptShown", {
       url: aScript.url
     });
   },
 
   /**
-   * Handles notifications to load a source script from the cache or from a
-   * local file.
-   *
-   * XXX: It may be better to use nsITraceableChannel to get to the sources
-   * without relying on caching when we can (not for eval, etc.):
-   * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
+   * Handles notifications to load a source script.
    */
   _onLoadSource: function SS__onLoadSource(aEvent) {
-    let url = aEvent.detail.url;
+    let script = aEvent.detail.script;
     let options = aEvent.detail.options;
-    let self = this;
-
-    switch (Services.io.extractScheme(url)) {
-      case "file":
-      case "chrome":
-      case "resource":
-        try {
-          NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
-            if (!Components.isSuccessCode(aStatus)) {
-              return self._logError(url, aStatus);
-            }
-            let source = NetUtil.readInputStreamToString(aStream, aStream.available());
-            source = self._convertToUnicode(source);
-            self._onLoadSourceFinished(url, source, null, options);
-            aStream.close();
-          });
-        } catch (ex) {
-          return self._logError(url, ex.name);
-        }
-        break;
 
-      default:
-        let channel = Services.io.newChannel(url, null, null);
-        let chunks = [];
-        let streamListener = {
-          onStartRequest: function(aRequest, aContext, aStatusCode) {
-            if (!Components.isSuccessCode(aStatusCode)) {
-              return self._logError(url, aStatusCode);
-            }
-          },
-          onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
-            chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
-          },
-          onStopRequest: function(aRequest, aContext, aStatusCode) {
-            if (!Components.isSuccessCode(aStatusCode)) {
-              return self._logError(url, aStatusCode);
-            }
-            let source = self._convertToUnicode(chunks.join(""), channel.contentCharset);
-            self._onLoadSourceFinished(url, source, channel.contentType, options);
-          }
-        };
+    let sourceClient = this.activeThread.source(script.source);
+    sourceClient.source(function (aResponse) {
+      if (aResponse.error) {
+        return this._logError(script.url, -1);
+      }
 
-        channel.loadFlags = channel.LOAD_FROM_CACHE;
-        channel.asyncOpen(streamListener, null);
-        break;
-    }
-  },
-
-  /**
-   * Convert a given string, encoded in a given character set, to unicode.
-   * @param string aString
-   *        A string.
-   * @param string aCharset
-   *        A character set.
-   * @return string
-   *         A unicode string.
-   */
-  _convertToUnicode: function SS__convertToUnicode(aString, aCharset) {
-    // Decoding primitives.
-    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
-        .createInstance(Ci.nsIScriptableUnicodeConverter);
-
-    try {
-      converter.charset = aCharset || "UTF-8";
-      return converter.ConvertToUnicode(aString);
-    } catch(e) {
-      return aString;
-    }
+      this._onLoadSourceFinished(script.url,
+                                 aResponse.source,
+                                 options);
+    }.bind(this));
   },
 
   /**
    * Called when a script's source has been loaded.
    *
    * @private
    * @param string aScriptUrl
    *        The URL of the source script.
    * @param string aSourceText
    *        The text of the source script.
-   * @param string aContentType
-   *        The content type of the source script.
    * @param object aOptions [optional]
    *        Additional options for showing the script. Supported options:
    *        - targetLine: place the editor at the given line number.
    */
   _onLoadSourceFinished:
-  function SS__onLoadSourceFinished(aScriptUrl, aSourceText, aContentType, aOptions) {
+  function SS__onLoadSourceFinished(aScriptUrl, aSourceText, aOptions) {
     let element = DebuggerView.Scripts.getScriptByLocation(aScriptUrl);
 
     // Tab navigated before we got a chance to finish loading and displaying
     // the source. The outcome is that the expected url is not present anymore
     // in the scripts container, hence the original script object coming from
     // the active thread no longer exists. There's really nothing that needs
     // to be done in this case, nor something that can be currently avoided.
     if (!element) {
       return;
     }
 
     let script = element.getUserData("sourceScript");
 
     script.loaded = true;
     script.text = aSourceText;
-    script.contentType = aContentType;
     element.setUserData("sourceScript", script, null);
 
     if (aOptions.silent) {
       aOptions.callback && aOptions.callback(aScriptUrl, aSourceText);
       return;
     }
 
     this.showScript(script, aOptions);
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -463,17 +463,17 @@ GlobalSearchView.prototype = {
     }
 
     // Fetch each new script's source.
     for (let url of aUrls) {
       if (this._scriptSources.has(url)) {
         continue;
       }
       DebuggerController.dispatchEvent("Debugger:LoadSource", {
-        url: url,
+        script: DebuggerView.Scripts.getScriptByLocation(url).getUserData("sourceScript"),
         options: {
           silent: true,
           callback: aFetchCallback
         }
       });
     }
   },
 
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
@@ -54,17 +54,17 @@ function testSimpleCall() {
 
 function testLocationChange()
 {
   gDebugger.DebuggerController.activeThread.resume(function() {
     gDebugger.DebuggerController.client.addOneTimeListener("tabNavigated", function(aEvent, aPacket) {
       ok(true, "tabNavigated event was fired.");
       info("Still attached to the tab.");
 
-      gDebugger.addEventListener("Debugger:AfterNewScript", function _onEvent(aEvent) {
+      gDebugger.addEventListener("Debugger:ScriptShown", function _onEvent(aEvent) {
         gDebugger.removeEventListener(aEvent.type, _onEvent);
 
         isnot(gDebugger.DebuggerView.Scripts.selected, null,
           "There should be a selected script.");
         isnot(gDebugger.editor.getText().length, 0,
           "The source editor should have some text displayed.");
 
         let menulist = gDebugger.DebuggerView.Scripts._scripts;
--- a/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
+++ b/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
@@ -165,28 +165,34 @@ function test()
     is(gView.Scripts.selected, scriptShownUrl,
       "The shown script is not the the correct one. (" + step + ")");
   }
 
   function switchScript(index)
   {
     let scriptsView = gView.Scripts;
     let scriptLocations = scriptsView.scriptLocations;
-    info("Available scripts: " + scriptLocations);
 
-    if (scriptLocations.length === 2) {
+    // Poll every few milliseconds until the scripts are retrieved.
+    let count = 0;
+    let intervalID = window.setInterval(function() {
+      dump("count: " + count + " ");
+      if (++count > 50) {
+        ok(false, "Timed out while polling for the scripts.");
+        closeDebuggerAndFinish();
+      }
+      if (scriptLocations.length !== 2) {
+        return;
+      }
+      info("Available scripts: " + scriptLocations);
+
       // We got all the scripts, it's safe to switch.
+      window.clearInterval(intervalID);
       scriptsView.selectScript(scriptLocations[index]);
-      return;
-    }
-
-    window.addEventListener("Debugger:AfterNewScript", function _onEvent(aEvent) {
-      window.removeEventListener(aEvent.type, _onEvent);
-      switchScript(index);
-    });
+    }, 100);
   }
 
   function reloadPage()
   {
     gDebuggee.location.reload();
   }
 
   registerCleanupFunction(function() {
--- 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"/>
 </head>
-<body role="application">
+<body class="devtools-theme-background" role="application">
   <div id="root"></div>
   <div id="templates" style="display:none">
     <ul>
       <li id="template-container" save="${elt}" class="container"><span save="${expander}" class="expander"></span><span save="${codeBox}" class="codebox"><ul save="${children}" class="children"></ul></span></li>
     </ul>
 
     <span id="template-element" save="${elt}" class="editor"><span>&lt;</span><span save="${tag}" class="tagname devtools-theme-tagname"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span>&gt;</span>
 
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -4,16 +4,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/. */
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/FloatingScrollbars.jsm");
 
 var EXPORTED_SYMBOLS = ["ResponsiveUIManager"];
 
 const MIN_WIDTH = 50;
 const MIN_HEIGHT = 50;
 
 const MAX_WIDTH = 10000;
 const MAX_HEIGHT = 10000;
@@ -154,16 +155,18 @@ function ResponsiveUI(aWindow, aTab)
 
   this.inspectorWasOpen = this.mainWindow.InspectorUI.isInspectorOpen;
 
   try {
     if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) {
       this.rotate();
     }
   } catch(e) {}
+
+  switchToFloatingScrollbars(this.tab);
 }
 
 ResponsiveUI.prototype = {
   _transitionsEnabled: true,
   get transitionsEnabled() this._transitionsEnabled,
   set transitionsEnabled(aValue) {
     this._transitionsEnabled = aValue;
     if (aValue && !this._resizing && this.stack.hasAttribute("responsivemode")) {
@@ -174,16 +177,17 @@ ResponsiveUI.prototype = {
   },
 
   /**
    * Destroy the nodes. Remove listeners. Reset the style.
    */
   close: function RUI_unload() {
     if (this.closing)
       return;
+    switchToNativeScrollbars(this.tab);
     this.closing = true;
 
     this.unCheckMenus();
     // Reset style of the stack.
     let style = "max-width: none;" +
                 "min-width: 0;" +
                 "max-height: none;" +
                 "min-height: 0;";
--- a/browser/devtools/responsivedesign/test/browser_responsiveui.js
+++ b/browser/devtools/responsivedesign/test/browser_responsiveui.js
@@ -26,21 +26,37 @@ function test() {
     is(container.getAttribute("responsivemode"), "true", "In responsive mode.");
 
     // Menus are correctly updated?
     is(document.getElementById("Tools:ResponsiveUI").getAttribute("checked"), "true", "menus checked");
 
     instance = gBrowser.selectedTab.__responsiveUI;
     ok(instance, "instance of the module is attached to the tab.");
 
+    ensureScrollbarsAreFloating();
+
     instance.transitionsEnabled = false;
 
     testPresets();
   }
 
+  function ensureScrollbarsAreFloating() {
+    let body = gBrowser.contentDocument.body;
+    let html = gBrowser.contentDocument.documentElement;
+
+    let originalWidth = body.getBoundingClientRect().width;
+
+    html.style.overflowY = "scroll"; // Force scrollbars
+    // Flush. Should not be needed as getBoundingClientRect() should flush,
+    // but just in case.
+    gBrowser.contentWindow.getComputedStyle(html).overflowY;
+    let newWidth = body.getBoundingClientRect().width;
+    is(originalWidth, newWidth, "Floating scrollbars are presents");
+  }
+
   function testPresets() {
     function testOnePreset(c) {
       if (c == 0) {
         executeSoon(testCustom);
         return;
       }
       instance.menulist.selectedIndex = c;
       let item = instance.menulist.firstChild.childNodes[c];
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -760,17 +760,17 @@ OutputPanel.prototype.update = function 
  * Detach listeners from the currently displayed Output.
  */
 OutputPanel.prototype.remove = function OP_remove()
 {
   if (isLinux) {
     this.canHide = true;
   }
 
-  if (this._panel) {
+  if (this._panel && this._panel.hidePopup) {
     this._panel.hidePopup();
   }
 
   if (this.displayedOutput) {
     this.displayedOutput.onChange.remove(this.update, this);
     this.displayedOutput.onClose.remove(this.remove, this);
     delete this.displayedOutput;
   }
@@ -987,17 +987,19 @@ TooltipPanel.prototype._resize = functio
 /**
  * Hide the TooltipPanel.
  */
 TooltipPanel.prototype.remove = function TP_remove()
 {
   if (isLinux) {
     this.canHide = true;
   }
-  this._panel.hidePopup();
+  if (this._panel && this._panel.hidePopup) {
+    this._panel.hidePopup();
+  }
 };
 
 /**
  * Hide the TooltipPanel.
  */
 TooltipPanel.prototype.destroy = function TP_destroy()
 {
   this.remove();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/FloatingScrollbars.jsm
@@ -0,0 +1,126 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const EXPORTED_SYMBOLS = [ "switchToFloatingScrollbars", "switchToNativeScrollbars" ];
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let URL = Services.io.newURI("chrome://browser/skin/devtools/floating-scrollbars.css", null, null);
+
+let trackedTabs = new WeakMap();
+
+/**
+ * Switch to floating scrollbars, à la mobile.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+function switchToFloatingScrollbars(aTab) {
+  let mgr = trackedTabs.get(aTab);
+  if (!mgr) {
+    mgr = new ScrollbarManager(aTab);
+  }
+  mgr.switchToFloating();
+}
+
+/**
+ * Switch to original native scrollbars.
+ *
+ * @param aTab the targeted tab.
+ *
+ */
+function switchToNativeScrollbars(aTab) {
+  let mgr = trackedTabs.get(aTab);
+  if (mgr) {
+    mgr.reset();
+  }
+}
+
+function ScrollbarManager(aTab) {
+  trackedTabs.set(aTab, this);
+
+  this.attachedTab = aTab;
+  this.attachedBrowser = aTab.linkedBrowser;
+
+  this.reset = this.reset.bind(this);
+  this.switchToFloating = this.switchToFloating.bind(this);
+
+  this.attachedTab.addEventListener("TabClose", this.reset, true);
+  this.attachedBrowser.addEventListener("DOMContentLoaded", this.switchToFloating, true);
+}
+
+ScrollbarManager.prototype = {
+  get win() {
+    return this.attachedBrowser.contentWindow;
+  },
+
+  /*
+   * Change the look of the scrollbars.
+   */
+  switchToFloating: function() {
+    let windows = this.getInnerWindows(this.win);
+    windows.forEach(this.injectStyleSheet);
+    this.forceStyle();
+  },
+
+
+  /*
+   * Reset the look of the scrollbars.
+   */
+  reset: function() {
+    let windows = this.getInnerWindows(this.win);
+    windows.forEach(this.removeStyleSheet);
+    this.forceStyle(this.attachedBrowser);
+    this.attachedBrowser.removeEventListener("DOMContentLoaded", this.switchToFloating, true);
+    this.attachedTab.removeEventListener("TabClose", this.reset, true);
+    trackedTabs.delete(this.attachedTab);
+  },
+
+  /*
+   * Toggle the display property of the window to force the style to be applied.
+   */
+  forceStyle: function() {
+    let parentWindow = this.attachedBrowser.ownerDocument.defaultView;
+    let display = parentWindow.getComputedStyle(this.attachedBrowser).display; // Save display value
+    this.attachedBrowser.style.display = "none";
+    parentWindow.getComputedStyle(this.attachedBrowser).display; // Flush
+    this.attachedBrowser.style.display = display; // Restore
+  },
+
+  /*
+   * return all the window objects present in the hiearchy of a window.
+   */
+  getInnerWindows: function(win) {
+    let iframes = win.document.querySelectorAll("iframe");
+    let innerWindows = [];
+    for (let iframe of iframes) {
+      innerWindows = innerWindows.concat(this.getInnerWindows(iframe.contentWindow));
+    }
+    return [win].concat(innerWindows);
+  },
+
+  /*
+   * Append the new scrollbar style.
+   */
+  injectStyleSheet: function(win) {
+    let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    try {
+      winUtils.loadSheet(URL, win.AGENT_SHEET);
+    }catch(e) {}
+  },
+
+  /*
+   * Remove the injected stylesheet.
+   */
+  removeStyleSheet: function(win) {
+    let winUtils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+    try {
+      winUtils.removeSheet(URL, win.AGENT_SHEET);
+    }catch(e) {}
+  },
+}
deleted file mode 100644
--- a/browser/devtools/shared/Promise.jsm
+++ /dev/null
@@ -1,409 +0,0 @@
-/*
- * Copyright 2009-2011 Mozilla Foundation and contributors
- * Licensed under the New BSD license. See LICENSE.txt or:
- * http://opensource.org/licenses/BSD-3-Clause
- */
-
-
-var EXPORTED_SYMBOLS = [ "Promise" ];
-
-/**
- * Create an unfulfilled promise
- *
- * @param {*=} aTrace A debugging value
- *
- * @constructor
- */
-function Promise(aTrace) {
-  this._status = Promise.PENDING;
-  this._value = undefined;
-  this._onSuccessHandlers = [];
-  this._onErrorHandlers = [];
-  this._trace = aTrace;
-
-  // Debugging help
-  if (Promise.Debug._debug) {
-    this._id = Promise.Debug._nextId++;
-    Promise.Debug._outstanding[this._id] = this;
-  }
-}
-
-/**
- * Debugging options and tools.
- */
-Promise.Debug = {
-  /**
-   * Set current debugging mode.
-   *
-   * @param {boolean} value If |true|, maintain _nextId, _outstanding, _recent.
-   * Otherwise, cleanup debugging data.
-   */
-  setDebug: function(value) {
-    Promise.Debug._debug = value;
-    if (!value) {
-      Promise.Debug._outstanding = [];
-      Promise.Debug._recent = [];
-    }
-  },
-
-  _debug: false,
-
-  /**
-   * We give promises and ID so we can track which are outstanding.
-   */
-  _nextId: 0,
-
-  /**
-   * Outstanding promises. Handy for debugging (only).
-   */
-  _outstanding: [],
-
-  /**
-   * Recently resolved promises. Also for debugging only.
-   */
-  _recent: []
-};
-
-
-/**
- * A promise can be in one of 2 states.
- * The ERROR and SUCCESS states are terminal, the PENDING state is the only
- * start state.
- */
-Promise.ERROR = -1;
-Promise.PENDING = 0;
-Promise.SUCCESS = 1;
-
-/**
- * Yeay for RTTI
- */
-Promise.prototype.isPromise = true;
-
-/**
- * Have we either been resolve()ed or reject()ed?
- */
-Promise.prototype.isComplete = function() {
-  return this._status != Promise.PENDING;
-};
-
-/**
- * Have we resolve()ed?
- */
-Promise.prototype.isResolved = function() {
-  return this._status == Promise.SUCCESS;
-};
-
-/**
- * Have we reject()ed?
- */
-Promise.prototype.isRejected = function() {
-  return this._status == Promise.ERROR;
-};
-
-/**
- * Take the specified action of fulfillment of a promise, and (optionally)
- * a different action on promise rejection
- */
-Promise.prototype.then = function(onSuccess, onError) {
-  if (typeof onSuccess === 'function') {
-    if (this._status === Promise.SUCCESS) {
-      onSuccess.call(null, this._value);
-    }
-    else if (this._status === Promise.PENDING) {
-      this._onSuccessHandlers.push(onSuccess);
-    }
-  }
-
-  if (typeof onError === 'function') {
-    if (this._status === Promise.ERROR) {
-      onError.call(null, this._value);
-    }
-    else if (this._status === Promise.PENDING) {
-      this._onErrorHandlers.push(onError);
-    }
-  }
-
-  return this;
-};
-
-/**
- * Like then() except that rather than returning <tt>this</tt> we return
- * a promise which resolves when the original promise resolves
- */
-Promise.prototype.chainPromise = function(onSuccess) {
-  var chain = new Promise();
-  chain._chainedFrom = this;
-  this.then(function(data) {
-    try {
-      chain.resolve(onSuccess(data));
-    }
-    catch (ex) {
-      chain.reject(ex);
-    }
-  }, function(ex) {
-    chain.reject(ex);
-  });
-  return chain;
-};
-
-/**
- * Supply the fulfillment of a promise
- */
-Promise.prototype.resolve = function(data) {
-  return this._complete(this._onSuccessHandlers,
-                        Promise.SUCCESS, data, 'resolve');
-};
-
-/**
- * Renege on a promise
- */
-Promise.prototype.reject = function(data) {
-  return this._complete(this._onErrorHandlers, Promise.ERROR, data, 'reject');
-};
-
-/**
- * Internal method to be called on resolve() or reject()
- * @private
- */
-Promise.prototype._complete = function(list, status, data, name) {
-  // Complain if we've already been completed
-  if (this._status != Promise.PENDING) {
-    Promise._error("Promise complete.", "Attempted ", name, "() with ", data);
-    Promise._error("Previous status: ", this._status, ", value =", this._value);
-    throw new Error('Promise already complete');
-  }
-
-  if (list.length == 0 && status == Promise.ERROR) {
-    var frame;
-    var text;
-
-    //Complain if a rejection is ignored
-    //(this is the equivalent of an empty catch-all clause)
-    Promise._error("Promise rejection ignored and silently dropped", data);
-    if (data.stack) {// This looks like an exception. Try harder to display it
-      if (data.fileName && data.lineNumber) {
-        Promise._error("Error originating at", data.fileName,
-                       ", line", data.lineNumber );
-      }
-      try {
-        for (frame = data.stack; frame; frame = frame.caller) {
-          text += frame + "\n";
-        }
-        Promise._error("Attempting to extract exception stack", text);
-      } catch (x) {
-        Promise._error("Could not extract exception stack.");
-      }
-    } else {
-      Promise._error("Exception stack not available.");
-    }
-    if (Components && Components.stack) {
-      try {
-        text = "";
-        for (frame = Components.stack; frame; frame = frame.caller) {
-          text += frame + "\n";
-        }
-        Promise._error("Attempting to extract current stack", text);
-      } catch (x) {
-        Promise._error("Could not extract current stack.");
-      }
-    } else {
-      Promise._error("Current stack not available.");
-    }
-  }
-
-
-  this._status = status;
-  this._value = data;
-
-  // Call all the handlers, and then delete them
-  list.forEach(function(handler) {
-    handler.call(null, this._value);
-  }, this);
-  delete this._onSuccessHandlers;
-  delete this._onErrorHandlers;
-
-  // Remove the given {promise} from the _outstanding list, and add it to the
-  // _recent list, pruning more than 20 recent promises from that list
-  delete Promise.Debug._outstanding[this._id];
-  // The original code includes this very useful debugging aid, however there
-  // is concern that it will create a memory leak, so we leave it out here.
-  /*
-  Promise._recent.push(this);
-  while (Promise._recent.length > 20) {
-    Promise._recent.shift();
-  }
-  */
-
-  return this;
-};
-
-/**
- * Log an error on the most appropriate channel.
- *
- * If the console is available, this method uses |console.warn|. Otherwise,
- * this method falls back to |dump|.
- *
- * @param {...*} items Items to log.
- */
-Promise._error = null;
-if (typeof console != "undefined" && console.warn) {
-  Promise._error = function() {
-    var args = Array.prototype.slice.call(arguments);
-    args.unshift("Promise");
-    console.warn.call(console, args);
-  };
-} else {
-  Promise._error = function() {
-    var i;
-    var len = arguments.length;
-    dump("Promise: ");
-    for (i = 0; i < len; ++i) {
-      dump(arguments[i]+" ");
-    }
-    dump("\n");
-  };
-}
-
-/**
- * Takes an array of promises and returns a promise that that is fulfilled once
- * all the promises in the array are fulfilled
- * @param promiseList The array of promises
- * @return the promise that is fulfilled when all the array is fulfilled
- */
-Promise.group = function(promiseList) {
-  if (!Array.isArray(promiseList)) {
-    promiseList = Array.prototype.slice.call(arguments);
-  }
-
-  // If the original array has nothing in it, return now to avoid waiting
-  if (promiseList.length === 0) {
-    return new Promise().resolve([]);
-  }
-
-  var groupPromise = new Promise();
-  var results = [];
-  var fulfilled = 0;
-
-  var onSuccessFactory = function(index) {
-    return function(data) {
-      results[index] = data;
-      fulfilled++;
-      // If the group has already failed, silently drop extra results
-      if (groupPromise._status !== Promise.ERROR) {
-        if (fulfilled === promiseList.length) {
-          groupPromise.resolve(results);
-        }
-      }
-    };
-  };
-
-  promiseList.forEach(function(promise, index) {
-    var onSuccess = onSuccessFactory(index);
-    var onError = groupPromise.reject.bind(groupPromise);
-    promise.then(onSuccess, onError);
-  });
-
-  return groupPromise;
-};
-
-/**
- * Trap errors.
- *
- * This function serves as an asynchronous counterpart to |catch|.
- *
- * Example:
- *  myPromise.chainPromise(a) //May reject
- *           .chainPromise(b) //May reject
- *           .chainPromise(c) //May reject
- *           .trap(d)       //Catch any rejection from a, b or c
- *           .chainPromise(e) //If either a, b and c or
- *                            //d has resolved, execute
- *
- * Scenario 1:
- *   If a, b, c resolve, e is executed as if d had not been added.
- *
- * Scenario 2:
- *   If a, b or c rejects, d is executed. If d resolves, we proceed
- *   with e as if nothing had happened. Otherwise, we proceed with
- *   the rejection of d.
- *
- * @param {Function} aTrap Called if |this| promise is rejected,
- *   with one argument: the rejection.
- * @return {Promise} A new promise. This promise resolves if all
- *   previous promises have resolved or if |aTrap| succeeds.
- */
-Promise.prototype.trap = function(aTrap) {
-  var promise = new Promise();
-  var resolve = Promise.prototype.resolve.bind(promise);
-  var reject = function(aRejection) {
-    try {
-      //Attempt to handle issue
-      var result = aTrap.call(aTrap, aRejection);
-      promise.resolve(result);
-    } catch (x) {
-      promise.reject(x);
-    }
-  };
-  this.then(resolve, reject);
-  return promise;
-};
-
-/**
- * Execute regardless of errors.
- *
- * This function serves as an asynchronous counterpart to |finally|.
- *
- * Example:
- *  myPromise.chainPromise(a) //May reject
- *           .chainPromise(b) //May reject
- *           .chainPromise(c) //May reject
- *           .always(d)       //Executed regardless
- *           .chainPromise(e)
- *
- * Whether |a|, |b| or |c| resolve or reject, |d| is executed.
- *
- * @param {Function} aTrap Called regardless of whether |this|
- *   succeeds or fails.
- * @return {Promise} A new promise. This promise holds the same
- *   resolution/rejection as |this|.
- */
-Promise.prototype.always = function(aTrap) {
-  var promise = new Promise();
-  var resolve = function(result) {
-    try {
-      aTrap.call(aTrap);
-      promise.resolve(result);
-    } catch (x) {
-      promise.reject(x);
-    }
-  };
-  var reject = function(result) {
-    try {
-      aTrap.call(aTrap);
-      promise.reject(result);
-    } catch (x) {
-      promise.reject(result);
-    }
-  };
-  this.then(resolve, reject);
-  return promise;
-};
-
-
-Promise.prototype.toString = function() {
-  var status;
-  switch (this._status) {
-  case Promise.PENDING:
-    status = "pending";
-    break;
-  case Promise.SUCCESS:
-    status = "resolved";
-    break;
-  case Promise.ERROR:
-    status = "rejected";
-    break;
-  default:
-    status = "invalid status: "+this._status;
-  }
-  return "[Promise " + this._id + " (" + status + ")]";
-};
--- a/browser/devtools/shared/test/browser_promise_basic.js
+++ b/browser/devtools/shared/test/browser_promise_basic.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that our Promise implementation works properly
 
 let tempScope = {};
-Cu.import("resource:///modules/devtools/Promise.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/_Promise.jsm", tempScope);
 let Promise = tempScope.Promise;
 
 function test() {
   addTab("about:blank", function() {
     info("Starting Promise Tests");
     testBasic();
   });
 }
--- a/browser/devtools/shared/test/browser_templater_basic.js
+++ b/browser/devtools/shared/test/browser_templater_basic.js
@@ -6,17 +6,17 @@
 /*
  * These tests run both in Mozilla/Mochitest and plain browsers (as does
  * domtemplate)
  * We should endevour to keep the source in sync.
  */
 
 var imports = {};
 Cu.import("resource:///modules/devtools/Templater.jsm", imports);
-Cu.import("resource:///modules/devtools/Promise.jsm", imports);
+Cu.import("resource://gre/modules/devtools/_Promise.jsm", imports);
 
 function test() {
   addTab("http://example.com/browser/browser/devtools/shared/test/browser_templater_basic.html", function() {
     info("Starting DOM Templater Tests");
     runTest(0);
   });
 }
 
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -1624,23 +1624,16 @@ TextPropertyEditor.prototype = {
     // Property value, editable when focused.  Changes to the
     // property value are applied as they are typed, and reverted
     // if the user presses escape.
     this.valueSpan = createChild(propertyContainer, "span", {
       class: "ruleview-propertyvalue",
       tabindex: "0",
     });
 
-    editableField({
-      start: this._onStartEditing,
-      element: this.valueSpan,
-      done: this._onValueDone,
-      advanceChars: ';'
-    });
-
     // Save the initial value as the last committed value,
     // for restoring after pressing escape.
     this.committed = { name: this.prop.name,
                        value: this.prop.value,
                        priority: this.prop.priority };
 
     appendText(propertyContainer, ";");
 
@@ -1650,16 +1643,25 @@ TextPropertyEditor.prototype = {
       title: CssLogic.l10n("rule.warning.title"),
     });
 
     // Holds the viewers for the computed properties.
     // will be populated in |_updateComputed|.
     this.computed = createChild(this.element, "ul", {
       class: "ruleview-computedlist",
     });
+
+    editableField({
+      start: this._onStartEditing,
+      element: this.valueSpan,
+      done: this._onValueDone,
+      validate: this._validate.bind(this),
+      warning: this.warning,
+      advanceChars: ';'
+    });
   },
 
   /**
    * Populate the span based on changes to the TextProperty.
    */
   update: function TextPropertyEditor_update()
   {
     if (this.prop.enabled) {
@@ -1836,27 +1838,40 @@ TextPropertyEditor.prototype = {
     } else {
       this.prop.setValue(this.committed.value, this.committed.priority);
     }
   },
 
   /**
    * Validate this property.
    *
+   * @param {String} [aValue]
+   *        Override the actual property value used for validation without
+   *        applying property values e.g. validate as you type.
+   *
    * @returns {Boolean}
    *          True if the property value is valid, false otherwise.
    */
-  _validate: function TextPropertyEditor_validate()
+  _validate: function TextPropertyEditor_validate(aValue)
   {
     let name = this.prop.name;
-    let value = this.prop.value;
+    let value = typeof aValue == "undefined" ? this.prop.value : aValue;
+    let val = this._parseValue(value);
     let style = this.doc.createElementNS(HTML_NS, "div").style;
+    let prefs = Services.prefs;
 
-    style.setProperty(name, value, null);
+    // We toggle output of errors whilst the user is typing a property value.
+    let prefVal = Services.prefs.getBoolPref("layout.css.report_errors");
+    prefs.setBoolPref("layout.css.report_errors", false);
 
+    try {
+      style.setProperty(name, val.value, val.priority);
+    } finally {
+      prefs.setBoolPref("layout.css.report_errors", prefVal);
+    }
     return !!style.getPropertyValue(name);
   },
 };
 
 /**
  * Mark a span editable.  |editableField| will listen for the span to
  * be focused and create an InlineEditor to handle text input.
  * Changes will be committed when the InlineEditor's input is blurred
@@ -1966,16 +1981,17 @@ function InplaceEditor(aOptions, aEvent)
   this.destroy = aOptions.destroy;
   this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
   this.multiline = aOptions.multiline || false;
   this.stopOnReturn = !!aOptions.stopOnReturn;
 
   this._onBlur = this._onBlur.bind(this);
   this._onKeyPress = this._onKeyPress.bind(this);
   this._onInput = this._onInput.bind(this);
+  this._onKeyup = this._onKeyup.bind(this);
 
   this._createInput();
   this._autosize();
 
   // Pull out character codes for advanceChars, listing the
   // characters that should trigger a blur.
   this._advanceCharCodes = {};
   let advanceChars = aOptions.advanceChars || '';
@@ -1993,16 +2009,23 @@ function InplaceEditor(aOptions, aEvent)
   }
   this.input.focus();
 
   this.input.addEventListener("blur", this._onBlur, false);
   this.input.addEventListener("keypress", this._onKeyPress, false);
   this.input.addEventListener("input", this._onInput, false);
   this.input.addEventListener("mousedown", function(aEvt) { aEvt.stopPropagation(); }, false);
 
+  this.warning = aOptions.warning;
+  this.validate = aOptions.validate;
+
+  if (this.warning && this.validate) {
+    this.input.addEventListener("keyup", this._onKeyup, false);
+  }
+
   if (aOptions.start) {
     aOptions.start(this, aEvent);
   }
 }
 
 InplaceEditor.prototype = {
   _createInput: function InplaceEditor_createEditor()
   {
@@ -2021,16 +2044,17 @@ InplaceEditor.prototype = {
   {
     if (!this.input) {
       // Already cleared.
       return;
     }
 
     this.input.removeEventListener("blur", this._onBlur, false);
     this.input.removeEventListener("keypress", this._onKeyPress, false);
+    this.input.removeEventListener("keyup", this._onKeyup, false);
     this.input.removeEventListener("oninput", this._onInput, false);
     this._stopAutosize();
 
     this.elt.style.display = this.originalDisplay;
     this.elt.focus();
 
     if (this.destroy) {
       this.destroy();
@@ -2123,20 +2147,22 @@ InplaceEditor.prototype = {
       return this.done(this.cancelled ? this.initial : val, !this.cancelled);
     }
     return null;
   },
 
   /**
    * Handle loss of focus by calling done if it hasn't been called yet.
    */
-  _onBlur: function InplaceEditor_onBlur(aEvent)
+  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
   {
     this._apply();
-    this._clear();
+    if (!aDoNotClear) {
+      this._clear();
+    }
   },
 
   _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
   {
     let prevent = false;
     if (this.multiline &&
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
         aEvent.shiftKey) {
@@ -2189,20 +2215,35 @@ InplaceEditor.prototype = {
     }
 
     if (prevent) {
       aEvent.preventDefault();
     }
   },
 
   /**
+   * Handle the input field's keyup event.
+   */
+  _onKeyup: function(aEvent) {
+    // Validate the entered value.
+    this.warning.hidden = this.validate(this.input.value);
+    this._applied = false;
+    this._onBlur(null, true);
+  },
+
+  /**
    * Handle changes the input text.
    */
   _onInput: function InplaceEditor_onInput(aEvent)
   {
+    // Validate the entered value.
+    if (this.warning && this.validate) {
+      this.warning.hidden = this.validate(this.input.value);
+    }
+
     // Update size if we're autosizing.
     if (this._measurement) {
       this._updateSize();
     }
 
     // Call the user's change handler if available.
     if (this.change) {
       this.change(this.input.value.trim());
--- a/browser/devtools/styleinspector/test/browser_ruleview_ui.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
@@ -165,16 +165,18 @@ function testEditProperty()
           is(props[i].hasAttribute("dirty"), i <= 1,
             "props[" + i + "] marked dirty as appropriate");
         }
         testDisableProperty();
       });
 
       for each (let ch in "red;") {
         EventUtils.sendChar(ch, ruleDialog);
+        is(propEditor.warning.hidden, ch == "d" || ch == ";",
+          "warning triangle is hidden or shown as appropriate");
       }
     });
     for each (let ch in "border-color:") {
       EventUtils.sendChar(ch, ruleDialog);
     }
   });
 
   EventUtils.synthesizeMouse(propEditor.nameSpan, 1, 1,
@@ -196,17 +198,16 @@ function testDisableProperty()
      "Border-color should have been reset.");
   expectChange();
 
   finishTest();
 }
 
 function finishTest()
 {
-  ruleView.element.removeEventListener("CssRuleViewChanged", ruleViewChanged, false);
   ruleView.clear();
   ruleDialog.close();
   ruleDialog = ruleView = null;
   doc = null;
   gBrowser.removeCurrentTab();
   finish();
 }
 
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -10,16 +10,20 @@
   - You want to make that choice consistent across the developer tools.
   - A good criteria is the language in which you'd find the best
   - documentation on web development on the web. -->
 
 <!-- LOCALIZATION NOTE (debuggerMenu.label): This is the label for the
   -  application menu item that opens the debugger UI. -->
 <!ENTITY debuggerMenu.label2            "Debugger">
 
+<!-- LOCALIZATION NOTE (debuggerMenu.accesskey): This is accesskey for the
+  -  Tools meny entry of Debugger that opens the debugger UI. -->
+<!ENTITY debuggerMenu.accesskey         "D">
+
 <!-- LOCALIZATION NOTE (remoteDebuggerMenu.label): This is the label for the
   -  application menu item that opens the remote debugger UI. -->
 <!ENTITY remoteDebuggerMenu.label       "Remote Debugger">
 
 <!-- LOCALIZATION NOTE (chromeDebuggerMenu.label): This is the label for the
   -  application menu item that opens the browser debugger UI. -->
 <!ENTITY chromeDebuggerMenu.label       "Browser Debugger">
 
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -375,16 +375,24 @@ breakNotFound=Breakpoint was not found
 # LOCALIZATION NOTE (dbgDesc) A very short string used to describe the
 # function of the dbg command.
 dbgDesc=Manage debugger
 
 # LOCALIZATION NOTE (dbgManual) A longer description describing the
 # set of commands that control the debugger.
 dbgManual=Commands to interrupt or resume the main thread, step in, out and over lines of code
 
+# LOCALIZATION NOTE (dbgOpen) A very short string used to describe the function
+# of the dbg open command.
+dbgOpen=Open the debugger
+
+# LOCALIZATION NOTE (dbgClose) A very short string used to describe the function
+# of the dbg close command.
+dbgClose=Close the debugger
+
 # LOCALIZATION NOTE (dbgInterrupt) A very short string used to describe the
 # function of the dbg interrupt command.
 dbgInterrupt=Pauses the main thread
 
 # LOCALIZATION NOTE (dbgContinue) A very short string used to describe the
 # function of the dbg continue command.
 dbgContinue=Resumes the main thread, and continues execution following a breakpoint, until the next breakpoint or the termination of the script.
 
new file mode 100644
--- /dev/null
+++ b/browser/themes/gnomestripe/devtools/floating-scrollbars.css
@@ -0,0 +1,33 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+scrollbar {
+  -moz-appearance: none !important;
+  position: relative;
+  background-color: transparent;
+  background-image: none;
+  z-index: 2147483647;
+  padding: 2px;
+}
+
+scrollbar[orient="vertical"] {
+  -moz-margin-start: -8px;
+  min-width: 8px;
+  max-width: 8px;
+}
+
+scrollbar[orient="horizontal"] {
+  margin-top: -8px;
+  min-height: 8px;
+  max-height: 8px;
+}
+
+scrollbar thumb {
+  -moz-appearance: none !important;
+  border-width: 0px !important;
+  background-color: rgba(0,0,0,0.2) !important;
+  border-radius: 3px !important;
+}
+
+scrollbar scrollbarbutton, scrollbar gripper {
+  display: none;
+}
--- a/browser/themes/gnomestripe/jar.mn
+++ b/browser/themes/gnomestripe/jar.mn
@@ -160,16 +160,17 @@ browser.jar:
   skin/classic/browser/devtools/debugger-play.png      (devtools/debugger-play.png)
   skin/classic/browser/devtools/debugger-step-in.png   (devtools/debugger-step-in.png)
   skin/classic/browser/devtools/debugger-step-out.png  (devtools/debugger-step-out.png)
   skin/classic/browser/devtools/debugger-step-over.png (devtools/debugger-step-over.png)
   skin/classic/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-background.png (devtools/responsive-background.png)
   skin/classic/browser/devtools/tools-icons-small.png     (devtools/tools-icons-small.png)
+  skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-16-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-24-throbber.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
   skin/classic/browser/sync-desktopIcon.png
new file mode 100644
--- /dev/null
+++ b/browser/themes/pinstripe/devtools/floating-scrollbars.css
@@ -0,0 +1,30 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+scrollbar {
+  -moz-appearance: none;
+  position: relative;
+  background-color: transparent;
+  background-image: none;
+  border: 0px solid transparent;
+  z-index: 2147483647;
+  -moz-box-align: start;
+  padding: 2px;
+}
+
+scrollbar[orient="vertical"] {
+  -moz-margin-start: -8px;
+  min-width: 8px;
+  max-width: 8px;
+}
+
+scrollbar[orient="horizontal"] {
+  margin-top: -8px;
+  min-height: 8px;
+  max-height: 8px;
+}
+
+thumb {
+  -moz-appearance: none !important;
+  background-color: rgba(0,0,0,0.2);
+  border-radius: 3px;
+}
--- a/browser/themes/pinstripe/jar.mn
+++ b/browser/themes/pinstripe/jar.mn
@@ -230,16 +230,17 @@ browser.jar:
   skin/classic/browser/devtools/debugger-play.png           (devtools/debugger-play.png)
   skin/classic/browser/devtools/debugger-step-in.png        (devtools/debugger-step-in.png)
   skin/classic/browser/devtools/debugger-step-out.png       (devtools/debugger-step-out.png)
   skin/classic/browser/devtools/debugger-step-over.png      (devtools/debugger-step-over.png)
   skin/classic/browser/devtools/responsive-se-resizer.png   (devtools/responsive-se-resizer.png)
   skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
   skin/classic/browser/devtools/responsive-background.png   (devtools/responsive-background.png)
   skin/classic/browser/devtools/tools-icons-small.png       (devtools/tools-icons-small.png)
+  skin/classic/browser/devtools/floating-scrollbars.css     (devtools/floating-scrollbars.css)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
   skin/classic/browser/sync-desktopIcon.png
   skin/classic/browser/sync-mobileIcon.png
new file mode 100644
--- /dev/null
+++ b/browser/themes/winstripe/devtools/floating-scrollbars.css
@@ -0,0 +1,33 @@
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+scrollbar {
+  -moz-appearance: none !important;
+  position: relative;
+  background-color: transparent;
+  background-image: none;
+  z-index: 2147483647;
+  padding: 2px;
+}
+
+scrollbar[orient="vertical"] {
+  -moz-margin-start: -8px;
+  min-width: 8px;
+  max-width: 8px;
+}
+
+scrollbar[orient="horizontal"] {
+  margin-top: -8px;
+  min-height: 8px;
+  max-height: 8px;
+}
+
+scrollbar thumb {
+  -moz-appearance: none !important;
+  border-width: 0px !important;
+  background-color: rgba(0,0,0,0.2) !important;
+  border-radius: 3px !important;
+}
+
+scrollbar scrollbarbutton, scrollbar gripper {
+  display: none;
+}
--- a/browser/themes/winstripe/jar.mn
+++ b/browser/themes/winstripe/jar.mn
@@ -187,16 +187,17 @@ browser.jar:
         skin/classic/browser/devtools/debugger-play.png             (devtools/debugger-play.png)
         skin/classic/browser/devtools/debugger-step-in.png          (devtools/debugger-step-in.png)
         skin/classic/browser/devtools/debugger-step-out.png         (devtools/debugger-step-out.png)
         skin/classic/browser/devtools/debugger-step-over.png        (devtools/debugger-step-over.png)
         skin/classic/browser/devtools/responsive-se-resizer.png     (devtools/responsive-se-resizer.png)
         skin/classic/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/browser/devtools/responsive-background.png     (devtools/responsive-background.png)
         skin/classic/browser/devtools/tools-icons-small.png         (devtools/tools-icons-small.png)
+        skin/classic/browser/devtools/floating-scrollbars.css       (devtools/floating-scrollbars.css)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/browser/sync-throbber.png
         skin/classic/browser/sync-16.png
         skin/classic/browser/sync-32.png
         skin/classic/browser/sync-128.png
         skin/classic/browser/sync-bg.png
         skin/classic/browser/sync-desktopIcon.png
         skin/classic/browser/sync-mobileIcon.png
@@ -390,16 +391,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/debugger-play.png         (devtools/debugger-play.png)
         skin/classic/aero/browser/devtools/debugger-step-in.png      (devtools/debugger-step-in.png)
         skin/classic/aero/browser/devtools/debugger-step-out.png     (devtools/debugger-step-out.png)
         skin/classic/aero/browser/devtools/debugger-step-over.png    (devtools/debugger-step-over.png)
         skin/classic/aero/browser/devtools/responsive-se-resizer.png (devtools/responsive-se-resizer.png)
         skin/classic/aero/browser/devtools/responsive-vertical-resizer.png (devtools/responsive-vertical-resizer.png)
         skin/classic/aero/browser/devtools/responsive-background.png (devtools/responsive-background.png)
         skin/classic/aero/browser/devtools/tools-icons-small.png     (devtools/tools-icons-small.png)
+        skin/classic/aero/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/aero/browser/sync-throbber.png
         skin/classic/aero/browser/sync-16.png
         skin/classic/aero/browser/sync-32.png
         skin/classic/aero/browser/sync-128.png
         skin/classic/aero/browser/sync-bg.png
         skin/classic/aero/browser/sync-desktopIcon.png
         skin/classic/aero/browser/sync-mobileIcon.png
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/_Promise.jsm
@@ -0,0 +1,409 @@
+/*
+ * Copyright 2009-2011 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.txt or:
+ * http://opensource.org/licenses/BSD-3-Clause
+ */
+
+
+var EXPORTED_SYMBOLS = [ "Promise" ];
+
+/**
+ * Create an unfulfilled promise
+ *
+ * @param {*=} aTrace A debugging value
+ *
+ * @constructor
+ */
+function Promise(aTrace) {
+  this._status = Promise.PENDING;
+  this._value = undefined;
+  this._onSuccessHandlers = [];
+  this._onErrorHandlers = [];
+  this._trace = aTrace;
+
+  // Debugging help
+  if (Promise.Debug._debug) {
+    this._id = Promise.Debug._nextId++;
+    Promise.Debug._outstanding[this._id] = this;
+  }
+}
+
+/**
+ * Debugging options and tools.
+ */
+Promise.Debug = {
+  /**
+   * Set current debugging mode.
+   *
+   * @param {boolean} value If |true|, maintain _nextId, _outstanding, _recent.
+   * Otherwise, cleanup debugging data.
+   */
+  setDebug: function(value) {
+    Promise.Debug._debug = value;
+    if (!value) {
+      Promise.Debug._outstanding = [];
+      Promise.Debug._recent = [];
+    }
+  },
+
+  _debug: false,
+
+  /**
+   * We give promises and ID so we can track which are outstanding.
+   */
+  _nextId: 0,
+
+  /**
+   * Outstanding promises. Handy for debugging (only).
+   */
+  _outstanding: [],
+
+  /**
+   * Recently resolved promises. Also for debugging only.
+   */
+  _recent: []
+};
+
+
+/**
+ * A promise can be in one of 2 states.
+ * The ERROR and SUCCESS states are terminal, the PENDING state is the only
+ * start state.
+ */
+Promise.ERROR = -1;
+Promise.PENDING = 0;
+Promise.SUCCESS = 1;
+
+/**
+ * Yeay for RTTI
+ */
+Promise.prototype.isPromise = true;
+
+/**
+ * Have we either been resolve()ed or reject()ed?
+ */
+Promise.prototype.isComplete = function() {
+  return this._status != Promise.PENDING;
+};
+
+/**
+ * Have we resolve()ed?
+ */
+Promise.prototype.isResolved = function() {
+  return this._status == Promise.SUCCESS;
+};
+
+/**
+ * Have we reject()ed?
+ */
+Promise.prototype.isRejected = function() {
+  return this._status == Promise.ERROR;
+};
+
+/**
+ * Take the specified action of fulfillment of a promise, and (optionally)
+ * a different action on promise rejection
+ */
+Promise.prototype.then = function(onSuccess, onError) {
+  if (typeof onSuccess === 'function') {
+    if (this._status === Promise.SUCCESS) {
+      onSuccess.call(null, this._value);
+    }
+    else if (this._status === Promise.PENDING) {
+      this._onSuccessHandlers.push(onSuccess);
+    }
+  }
+
+  if (typeof onError === 'function') {
+    if (this._status === Promise.ERROR) {
+      onError.call(null, this._value);
+    }
+    else if (this._status === Promise.PENDING) {
+      this._onErrorHandlers.push(onError);
+    }
+  }
+
+  return this;
+};
+
+/**
+ * Like then() except that rather than returning <tt>this</tt> we return
+ * a promise which resolves when the original promise resolves
+ */
+Promise.prototype.chainPromise = function(onSuccess) {
+  var chain = new Promise();
+  chain._chainedFrom = this;
+  this.then(function(data) {
+    try {
+      chain.resolve(onSuccess(data));
+    }
+    catch (ex) {
+      chain.reject(ex);
+    }
+  }, function(ex) {
+    chain.reject(ex);
+  });
+  return chain;
+};
+
+/**
+ * Supply the fulfillment of a promise
+ */
+Promise.prototype.resolve = function(data) {
+  return this._complete(this._onSuccessHandlers,
+                        Promise.SUCCESS, data, 'resolve');
+};
+
+/**
+ * Renege on a promise
+ */
+Promise.prototype.reject = function(data) {
+  return this._complete(this._onErrorHandlers, Promise.ERROR, data, 'reject');
+};
+
+/**
+ * Internal method to be called on resolve() or reject()
+ * @private
+ */
+Promise.prototype._complete = function(list, status, data, name) {
+  // Complain if we've already been completed
+  if (this._status != Promise.PENDING) {
+    Promise._error("Promise complete.", "Attempted ", name, "() with ", data);
+    Promise._error("Previous status: ", this._status, ", value =", this._value);
+    throw new Error('Promise already complete');
+  }
+
+  if (list.length == 0 && status == Promise.ERROR) {
+    var frame;
+    var text;
+
+    //Complain if a rejection is ignored
+    //(this is the equivalent of an empty catch-all clause)
+    Promise._error("Promise rejection ignored and silently dropped", data);
+    if (data.stack) {// This looks like an exception. Try harder to display it
+      if (data.fileName && data.lineNumber) {
+        Promise._error("Error originating at", data.fileName,
+                       ", line", data.lineNumber );
+      }
+      try {
+        for (frame = data.stack; frame; frame = frame.caller) {
+          text += frame + "\n";
+        }
+        Promise._error("Attempting to extract exception stack", text);
+      } catch (x) {
+        Promise._error("Could not extract exception stack.");
+      }
+    } else {
+      Promise._error("Exception stack not available.");
+    }
+    if (Components && Components.stack) {
+      try {
+        text = "";
+        for (frame = Components.stack; frame; frame = frame.caller) {
+          text += frame + "\n";
+        }
+        Promise._error("Attempting to extract current stack", text);
+      } catch (x) {
+        Promise._error("Could not extract current stack.");
+      }
+    } else {
+      Promise._error("Current stack not available.");
+    }
+  }
+
+
+  this._status = status;
+  this._value = data;
+
+  // Call all the handlers, and then delete them
+  list.forEach(function(handler) {
+    handler.call(null, this._value);
+  }, this);
+  delete this._onSuccessHandlers;
+  delete this._onErrorHandlers;
+
+  // Remove the given {promise} from the _outstanding list, and add it to the
+  // _recent list, pruning more than 20 recent promises from that list
+  delete Promise.Debug._outstanding[this._id];
+  // The original code includes this very useful debugging aid, however there
+  // is concern that it will create a memory leak, so we leave it out here.
+  /*
+  Promise._recent.push(this);
+  while (Promise._recent.length > 20) {
+    Promise._recent.shift();
+  }
+  */
+
+  return this;
+};
+
+/**
+ * Log an error on the most appropriate channel.
+ *
+ * If the console is available, this method uses |console.warn|. Otherwise,
+ * this method falls back to |dump|.
+ *
+ * @param {...*} items Items to log.
+ */
+Promise._error = null;
+if (typeof console != "undefined" && console.warn) {
+  Promise._error = function() {
+    var args = Array.prototype.slice.call(arguments);
+    args.unshift("Promise");
+    console.warn.call(console, args);
+  };
+} else {
+  Promise._error = function() {
+    var i;
+    var len = arguments.length;
+    dump("Promise: ");
+    for (i = 0; i < len; ++i) {
+      dump(arguments[i]+" ");
+    }
+    dump("\n");
+  };
+}
+
+/**
+ * Takes an array of promises and returns a promise that that is fulfilled once
+ * all the promises in the array are fulfilled
+ * @param promiseList The array of promises
+ * @return the promise that is fulfilled when all the array is fulfilled
+ */
+Promise.group = function(promiseList) {
+  if (!Array.isArray(promiseList)) {
+    promiseList = Array.prototype.slice.call(arguments);
+  }
+
+  // If the original array has nothing in it, return now to avoid waiting
+  if (promiseList.length === 0) {
+    return new Promise().resolve([]);
+  }
+
+  var groupPromise = new Promise();
+  var results = [];
+  var fulfilled = 0;
+
+  var onSuccessFactory = function(index) {
+    return function(data) {
+      results[index] = data;
+      fulfilled++;
+      // If the group has already failed, silently drop extra results
+      if (groupPromise._status !== Promise.ERROR) {
+        if (fulfilled === promiseList.length) {
+          groupPromise.resolve(results);
+        }
+      }
+    };
+  };
+
+  promiseList.forEach(function(promise, index) {
+    var onSuccess = onSuccessFactory(index);
+    var onError = groupPromise.reject.bind(groupPromise);
+    promise.then(onSuccess, onError);
+  });
+
+  return groupPromise;
+};
+
+/**
+ * Trap errors.
+ *
+ * This function serves as an asynchronous counterpart to |catch|.
+ *
+ * Example:
+ *  myPromise.chainPromise(a) //May reject
+ *           .chainPromise(b) //May reject
+ *           .chainPromise(c) //May reject
+ *           .trap(d)       //Catch any rejection from a, b or c
+ *           .chainPromise(e) //If either a, b and c or
+ *                            //d has resolved, execute
+ *
+ * Scenario 1:
+ *   If a, b, c resolve, e is executed as if d had not been added.
+ *
+ * Scenario 2:
+ *   If a, b or c rejects, d is executed. If d resolves, we proceed
+ *   with e as if nothing had happened. Otherwise, we proceed with
+ *   the rejection of d.
+ *
+ * @param {Function} aTrap Called if |this| promise is rejected,
+ *   with one argument: the rejection.
+ * @return {Promise} A new promise. This promise resolves if all
+ *   previous promises have resolved or if |aTrap| succeeds.
+ */
+Promise.prototype.trap = function(aTrap) {
+  var promise = new Promise();
+  var resolve = Promise.prototype.resolve.bind(promise);
+  var reject = function(aRejection) {
+    try {
+      //Attempt to handle issue
+      var result = aTrap.call(aTrap, aRejection);
+      promise.resolve(result);
+    } catch (x) {
+      promise.reject(x);
+    }
+  };
+  this.then(resolve, reject);
+  return promise;
+};
+
+/**
+ * Execute regardless of errors.
+ *
+ * This function serves as an asynchronous counterpart to |finally|.
+ *
+ * Example:
+ *  myPromise.chainPromise(a) //May reject
+ *           .chainPromise(b) //May reject
+ *           .chainPromise(c) //May reject
+ *           .always(d)       //Executed regardless
+ *           .chainPromise(e)
+ *
+ * Whether |a|, |b| or |c| resolve or reject, |d| is executed.
+ *
+ * @param {Function} aTrap Called regardless of whether |this|
+ *   succeeds or fails.
+ * @return {Promise} A new promise. This promise holds the same
+ *   resolution/rejection as |this|.
+ */
+Promise.prototype.always = function(aTrap) {
+  var promise = new Promise();
+  var resolve = function(result) {
+    try {
+      aTrap.call(aTrap);
+      promise.resolve(result);
+    } catch (x) {
+      promise.reject(x);
+    }
+  };
+  var reject = function(result) {
+    try {
+      aTrap.call(aTrap);
+      promise.reject(result);
+    } catch (x) {
+      promise.reject(result);
+    }
+  };
+  this.then(resolve, reject);
+  return promise;
+};
+
+
+Promise.prototype.toString = function() {
+  var status;
+  switch (this._status) {
+  case Promise.PENDING:
+    status = "pending";
+    break;
+  case Promise.SUCCESS:
+    status = "resolved";
+    break;
+  case Promise.ERROR:
+    status = "rejected";
+    break;
+  default:
+    status = "invalid status: "+this._status;
+  }
+  return "[Promise " + this._id + " (" + status + ")]";
+};
--- a/toolkit/devtools/debugger/dbg-client.jsm
+++ b/toolkit/devtools/debugger/dbg-client.jsm
@@ -482,16 +482,17 @@ eventSource(TabClient.prototype);
  *        The actor ID for this thread.
  */
 function ThreadClient(aClient, aActor) {
   this._client = aClient;
   this._actor = aActor;
   this._frameCache = [];
   this._scriptCache = {};
   this._pauseGrips = {};
+  this._threadGrips = {};
 }
 
 ThreadClient.prototype = {
   _state: "paused",
   get state() { return this._state; },
   get paused() { return this._state === "paused"; },
 
   _pauseOnExceptions: false,
@@ -856,52 +857,105 @@ ThreadClient.prototype = {
     }
 
     let client = new GripClient(this._client, aGrip);
     this._pauseGrips[aGrip.actor] = client;
     return client;
   },
 
   /**
-   * Return an instance of LongStringClient for the given long string grip.
+   * Get or create a long string client, checking the grip client cache if it
+   * already exists.
+   *
+   * @param aGrip Object
+   *        The long string grip returned by the protocol.
+   * @param aGripCacheName String
+   *        The property name of the grip client cache to check for existing
+   *        clients in.
+   */
+  _longString: function TC__longString(aGrip, aGripCacheName) {
+    if (aGrip.actor in this[aGripCacheName]) {
+      return this[aGripCacheName][aGrip.actor];
+    }
+
+    let client = new LongStringClient(this._client, aGrip);
+    this[aGripCacheName][aGrip.actor] = client;
+    return client;
+  },
+
+  /**
+   * Return an instance of LongStringClient for the given long string grip that
+   * is scoped to the current pause.
    *
    * @param aGrip Object
    *        The long string grip returned by the protocol.
    */
-  longString: function TC_longString(aGrip) {
-    if (aGrip.actor in this._pauseGrips) {
-      return this._pauseGrips[aGrip.actor];
+  pauseLongString: function TC_pauseLongString(aGrip) {
+    return this._longString(aGrip, "_pauseGrips");
+  },
+
+  /**
+   * Return an instance of LongStringClient for the given long string grip that
+   * is scoped to the thread lifetime.
+   *
+   * @param aGrip Object
+   *        The long string grip returned by the protocol.
+   */
+  threadLongString: function TC_threadLongString(aGrip) {
+    return this._longString(aGrip, "_threadGrips");
+  },
+
+  /**
+   * Clear and invalidate all the grip clients from the given cache.
+   *
+   * @param aGripCacheName
+   *        The property name of the grip cache we want to clear.
+   */
+  _clearGripClients: function TC_clearGrips(aGripCacheName) {
+    for each (let grip in this[aGripCacheName]) {
+      grip.valid = false;
     }
-
-    let client = new LongStringClient(this._client, aGrip);
-    this._pauseGrips[aGrip.actor] = client;
-    return client;
+    this[aGripCacheName] = {};
   },
 
   /**
    * Invalidate pause-lifetime grip clients and clear the list of
    * current grip clients.
    */
-  _clearPauseGrips: function TC_clearPauseGrips(aPacket) {
-    for each (let grip in this._pauseGrips) {
-      grip.valid = false;
-    }
-    this._pauseGrips = {};
+  _clearPauseGrips: function TC_clearPauseGrips() {
+    this._clearGripClients("_pauseGrips");
+  },
+
+  /**
+   * Invalidate pause-lifetime grip clients and clear the list of
+   * current grip clients.
+   */
+  _clearThreadGrips: function TC_clearPauseGrips() {
+    this._clearGripClients("_threadGrips");
   },
 
   /**
    * Handle thread state change by doing necessary cleanup and notifying all
    * registered listeners.
    */
   _onThreadState: function TC_onThreadState(aPacket) {
     this._state = ThreadStateTypes[aPacket.type];
     this._clearFrames();
     this._clearPauseGrips();
+    aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips();
     this._client._eventsEnabled && this.notify(aPacket.type, aPacket);
   },
+
+  /**
+   * Return an instance of SourceClient for the given actor.
+   */
+  source: function TC_source(aActor) {
+    return new SourceClient(this._client, aActor);
+  }
+
 };
 
 eventSource(ThreadClient.prototype);
 
 /**
  * Grip clients are used to retrieve information about the relevant object.
  *
  * @param aClient DebuggerClient
@@ -1035,16 +1089,65 @@ LongStringClient.prototype = {
                    type: "substring",
                    start: aStart,
                    end: aEnd };
     this._client.request(packet, aCallback);
   }
 };
 
 /**
+ * A SourceClient provides a way to access the source text of a script.
+ *
+ * @param aClient DebuggerClient
+ *        The debugger client parent.
+ * @param aActor String
+ *        The name of the source actor.
+ */
+function SourceClient(aClient, aActor) {
+  this._actor = aActor;
+  this._client = aClient;
+}
+
+SourceClient.prototype = {
+  /**
+   * Get a long string grip for this SourceClient's source.
+   */
+  source: function SC_source(aCallback) {
+    let packet = {
+      to: this._actor,
+      type: "source"
+    };
+    this._client.request(packet, function (aResponse) {
+      if (aResponse.error) {
+        aCallback(aResponse);
+        return;
+      }
+
+      if (typeof aResponse.source === "string") {
+        aCallback(aResponse);
+        return;
+      }
+
+      let longString = this._client.activeThread.threadLongString(
+        aResponse.source);
+      longString.substring(0, longString.length, function (aResponse) {
+        if (aResponse.error) {
+          aCallback(aResponse);
+          return;
+        }
+
+        aCallback({
+          source: aResponse.substring
+        });
+      });
+    }.bind(this));
+  }
+};
+
+/**
  * Breakpoint clients are used to remove breakpoints that are no longer used.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
  * @param aActor string
  *        The actor ID for this breakpoint.
  * @param aLocation object
  *        The location of the breakpoint. This is an object with two properties:
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -541,20 +541,22 @@ ThreadActor.prototype = {
     }
     // Build the cache.
     let scripts = [];
     for (let url in this._scripts) {
       for (let i = 0; i < this._scripts[url].length; i++) {
         if (!this._scripts[url][i]) {
           continue;
         }
+
         let script = {
           url: url,
           startLine: i,
-          lineCount: this._scripts[url][i].lineCount
+          lineCount: this._scripts[url][i].lineCount,
+          source: this.sourceGrip(this._scripts[url][i], this)
         };
         scripts.push(script);
       }
     }
 
     let packet = { from: this.actorID,
                    scripts: scripts };
     return packet;
@@ -776,39 +778,42 @@ ThreadActor.prototype = {
     aPool.addActor(actor);
     aEnvironment.actor = actor;
 
     return actor;
   },
 
   /**
    * Create a grip for the given debuggee value.  If the value is an
-   * object, will create a pause-lifetime actor.
+   * object, will create an actor with the given lifetime.
    */
-  createValueGrip: function TA_createValueGrip(aValue) {
+  createValueGrip: function TA_createValueGrip(aValue, aPool=false) {
+    if (!aPool) {
+      aPool = this._pausePool;
+    }
     let type = typeof(aValue);
 
     if (type === "string" && this._stringIsLong(aValue)) {
-      return this.longStringGrip(aValue);
+      return this.longStringGrip(aValue, aPool);
     }
 
     if (type === "boolean" || type === "string" || type === "number") {
       return aValue;
     }
 
     if (aValue === null) {
       return { type: "null" };
     }
 
     if (aValue === undefined) {
       return { type: "undefined" }
     }
 
     if (typeof(aValue) === "object") {
-      return this.pauseObjectGrip(aValue);
+      return this.objectGrip(aValue, aPool);
     }
 
     dbg_assert(false, "Failed to provide a grip for: " + aValue);
     return null;
   },
 
   /**
    * Return a protocol completion value representing the given
@@ -876,47 +881,85 @@ ThreadActor.prototype = {
     return this.objectGrip(aValue, this.threadLifetimePool);
   },
 
   /**
    * Create a grip for the given string.
    *
    * @param aString String
    *        The string we are creating a grip for.
+   * @param aPool ActorPool
+   *        The actor pool where the new actor will be added.
    */
-  longStringGrip: function TA_longStringGrip(aString) {
-    if (!this._pausePool) {
-      throw new Error("LongString grip requested while not paused.");
+  longStringGrip: function TA_longStringGrip(aString, aPool) {
+    if (!aPool.longStringActors) {
+      aPool.longStringActors = {};
     }
 
-    if (!this._pausePool.longStringActors) {
-      this._pausePool.longStringActors = {};
-    }
-
-    if (this._pausePool.longStringActors.hasOwnProperty(aString)) {
-      return this._pausePool.longStringActors[aString].grip();
+    if (aPool.longStringActors.hasOwnProperty(aString)) {
+      return aPool.longStringActors[aString].grip();
     }
 
     let actor = new LongStringActor(aString, this);
-    this._pausePool.addActor(actor);
-    this._pausePool.longStringActors[aString] = actor;
+    aPool.addActor(actor);
+    aPool.longStringActors[aString] = actor;
     return actor.grip();
   },
 
   /**
+   * Create a long string grip that is scoped to a pause.
+   *
+   * @param aString String
+   *        The string we are creating a grip for.
+   */
+  pauseLongStringGrip: function TA_pauseLongStringGrip (aString) {
+    return this.longStringGrip(aString, this._pausePool);
+  },
+
+  /**
+   * Create a long string grip that is scoped to a thread.
+   *
+   * @param aString String
+   *        The string we are creating a grip for.
+   */
+  threadLongStringGrip: function TA_pauseLongStringGrip (aString) {
+    return this.longStringGrip(aString, this._threadLifetimePool);
+  },
+
+  /**
    * Returns true if the string is long enough to use a LongStringActor instead
    * of passing the value directly over the protocol.
    *
    * @param aString String
    *        The string we are checking the length of.
    */
   _stringIsLong: function TA__stringIsLong(aString) {
     return aString.length >= DebuggerServer.LONG_STRING_LENGTH;
   },
 
+  /**
+   * Create a source grip for the given script.
+   */
+  sourceGrip: function TA_sourceGrip(aScript) {
+    // TODO: Once we have Debugger.Source, this should be replaced with a
+    // weakmap mapping Debugger.Source instances to SourceActor instances.
+    if (!this.threadLifetimePool.sourceActors) {
+      this.threadLifetimePool.sourceActors = {};
+    }
+
+    if (this.threadLifetimePool.sourceActors[aScript.url]) {
+      return this.threadLifetimePool.sourceActors[aScript.url].grip();
+    }
+
+    let actor = new SourceActor(aScript, this);
+    this.threadLifetimePool.addActor(actor);
+    this.threadLifetimePool.sourceActors[aScript.url] = actor;
+    return actor.grip();
+  },
+
   // JS Debugger API hooks.
 
   /**
    * A function that the engine calls when a call to a debug event hook,
    * breakpoint handler, watchpoint handler, or similar function throws some
    * exception.
    *
    * @param aException exception
@@ -976,17 +1019,18 @@ ThreadActor.prototype = {
   onNewScript: function TA_onNewScript(aScript, aGlobal) {
     if (this._addScript(aScript)) {
       // Notify the client.
       this.conn.send({
         from: this.actorID,
         type: "newScript",
         url: aScript.url,
         startLine: aScript.startLine,
-        lineCount: aScript.lineCount
+        lineCount: aScript.lineCount,
+        source: this.sourceGrip(aScript, this)
       });
     }
   },
 
   /**
    * Add the provided script to the server cache.
    *
    * @param aScript Debugger.Script
@@ -1129,16 +1173,186 @@ function update(aTarget, aNewAttrs) {
     if (desc) {
       Object.defineProperty(aTarget, key, desc);
     }
   }
 }
 
 
 /**
+ * A SourceActor provides information about the source of a script.
+ *
+ * @param aScript Debugger.Script
+ *        The script whose source we are representing.
+ * @param aThreadActor ThreadActor
+ *        The current thread actor.
+ */
+function SourceActor(aScript, aThreadActor) {
+  this._threadActor = aThreadActor;
+  this._script = aScript;
+}
+
+SourceActor.prototype = {
+  constructor: SourceActor,
+  actorPrefix: "source",
+
+  get threadActor() { return this._threadActor; },
+
+  grip: function SA_grip() {
+    return this.actorID;
+  },
+
+  disconnect: function LSA_disconnect() {
+    if (this.registeredPool && this.registeredPool.sourceActors) {
+      delete this.registeredPool.sourceActors[this.actorID];
+    }
+  },
+
+  /**
+   * Handler for the "source" packet.
+   */
+  onSource: function SA_onSource(aRequest) {
+    this
+      ._loadSource()
+      .chainPromise(function(aSource) {
+        return this._threadActor.createValueGrip(
+          aSource, this.threadActor.threadLifetimePool);
+      }.bind(this))
+      .chainPromise(function (aSourceGrip) {
+        return {
+          from: this.actorID,
+          source: aSourceGrip
+        };
+      }.bind(this))
+      .trap(function (aError) {
+        return {
+          "from": this.actorID,
+          "error": "loadSourceError",
+          "message": "Could not load the source for " + this._script.url + "."
+        };
+      }.bind(this))
+      .chainPromise(function (aPacket) {
+        this.conn.send(aPacket);
+      }.bind(this));
+  },
+
+  /**
+   * Convert a given string, encoded in a given character set, to unicode.
+   * @param string aString
+   *        A string.
+   * @param string aCharset
+   *        A character set.
+   * @return string
+   *         A unicode string.
+   */
+  _convertToUnicode: function SS__convertToUnicode(aString, aCharset) {
+    // Decoding primitives.
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+        .createInstance(Ci.nsIScriptableUnicodeConverter);
+
+    try {
+      converter.charset = aCharset || "UTF-8";
+      return converter.ConvertToUnicode(aString);
+    } catch(e) {
+      return aString;
+    }
+  },
+
+  /**
+   * Performs a request to load the desired URL and returns a promise.
+   *
+   * @param aURL String
+   *        The URL we will request.
+   * @returns Promise
+   *
+   * XXX: It may be better to use nsITraceableChannel to get to the sources
+   * without relying on caching when we can (not for eval, etc.):
+   * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
+   */
+  _loadSource: function SA__loadSource() {
+    let promise = new Promise();
+    let url = this._script.url;
+    let scheme;
+    try {
+      scheme = Services.io.extractScheme(url);
+    } catch (e) {
+      // In the xpcshell tests, the script url is the absolute path of the test
+      // file, which will make a malformed URI error be thrown. Add the file
+      // scheme prefix ourselves.
+      url = "file://" + url;
+      scheme = Services.io.extractScheme(url);
+    }
+
+    switch (scheme) {
+      case "file":
+      case "chrome":
+      case "resource":
+        try {
+          NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
+            if (!Components.isSuccessCode(aStatus)) {
+              promise.reject(new Error("Request failed"));
+              return;
+            }
+
+            let source = NetUtil.readInputStreamToString(aStream, aStream.available());
+            promise.resolve(this._convertToUnicode(source));
+            aStream.close();
+          }.bind(this));
+        } catch (ex) {
+          promise.reject(new Error("Request failed"));
+        }
+        break;
+
+      default:
+        let channel;
+        try {
+          channel = Services.io.newChannel(url, null, null);
+        } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
+          // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
+          // newChannel won't be able to handle it.
+          url = "file:///" + url;
+          channel = Services.io.newChannel(url, null, null);
+        }
+        let chunks = [];
+        let streamListener = {
+          onStartRequest: function(aRequest, aContext, aStatusCode) {
+            if (!Components.isSuccessCode(aStatusCode)) {
+              promise.reject("Request failed");
+            }
+          },
+          onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
+            chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
+          },
+          onStopRequest: function(aRequest, aContext, aStatusCode) {
+            if (!Components.isSuccessCode(aStatusCode)) {
+              promise.reject("Request failed");
+              return;
+            }
+
+            promise.resolve(this._convertToUnicode(chunks.join(""),
+                                                   channel.contentCharset));
+          }.bind(this)
+        };
+
+        channel.loadFlags = channel.LOAD_FROM_CACHE;
+        channel.asyncOpen(streamListener, null);
+        break;
+    }
+
+    return promise;
+  }
+
+};
+
+SourceActor.prototype.requestTypes = {
+  "source": SourceActor.prototype.onSource
+};
+
+
+/**
  * Creates an actor for the specified object.
  *
  * @param aObj Debugger.Object
  *        The debuggee object.
  * @param aThreadActor ThreadActor
  *        The parent thread actor for this object.
  */
 function ObjectActor(aObj, aThreadActor)
--- a/toolkit/devtools/debugger/server/dbg-server.js
+++ b/toolkit/devtools/debugger/server/dbg-server.js
@@ -19,16 +19,18 @@ const DBG_STRINGS_URI = "chrome://global
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 addDebuggerToGlobal(this);
 
+Cu.import("resource://gre/modules/devtools/_Promise.jsm");
+
 function dumpn(str) {
   if (wantLogging) {
     dump("DBG-SERVER: " + str + "\n");
   }
 }
 
 function dbg_assert(cond, e) {
   if (!cond) {
--- a/toolkit/devtools/debugger/tests/unit/test_listscripts-01.js
+++ b/toolkit/devtools/debugger/tests/unit/test_listscripts-01.js
@@ -30,18 +30,19 @@ function test_simple_listscripts()
     gThreadClient.getScripts(function (aResponse) {
       let script = aResponse.scripts
         .filter(function (s) {
           return s.url.match(/test_listscripts-01.js$/);
         })[0];
       // Check the return value.
       do_check_true(!!script);
       do_check_eq(script.url, path);
-      do_check_eq(script.startLine, 46);
+      do_check_eq(script.startLine, gDebuggee.line0);
       do_check_eq(script.lineCount, 4);
+      do_check_true(!!script.source);
       gThreadClient.resume(function () {
         finishClient(gClient);
       });
     });
   });
 
   gDebuggee.eval("var line0 = Error().lineNumber;\n" +
        "debugger;\n" +   // line0 + 1
--- a/toolkit/devtools/debugger/tests/unit/test_longstringgrips-01.js
+++ b/toolkit/devtools/debugger/tests/unit/test_longstringgrips-01.js
@@ -43,17 +43,17 @@ function test_longstring_grip()
     do_check_eq(args.length, 1);
     let grip = args[0];
 
     try {
       do_check_eq(grip.type, "longString");
       do_check_eq(grip.length, longString.length);
       do_check_eq(grip.initial, longString.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH));
 
-      let longStringClient = gThreadClient.longString(grip);
+      let longStringClient = gThreadClient.pauseLongString(grip);
       longStringClient.substring(22, 28, function (aResponse) {
         try {
           do_check_eq(aResponse.substring, "monkey");
         } finally {
           gThreadClient.resume(function() {
             finishClient(gClient);
           });
         }
--- a/toolkit/devtools/debugger/tests/unit/test_longstringgrips-02.js
+++ b/toolkit/devtools/debugger/tests/unit/test_longstringgrips-02.js
@@ -31,17 +31,17 @@ function test_longstring_grip()
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     try {
       let fakeLongStringGrip = {
         type: "longString",
         length: 1000000,
         actor: "123fakeActor123",
         initial: ""
       };
-      let longStringClient = gThreadClient.longString(fakeLongStringGrip);
+      let longStringClient = gThreadClient.pauseLongString(fakeLongStringGrip);
       longStringClient.substring(22, 28, function (aResponse) {
         try {
           do_check_true(!!aResponse.error,
                         "We should not get a response, but an error.");
         } finally {
           gThreadClient.resume(function() {
             finishClient(gClient);
           });
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_source-01.js
@@ -0,0 +1,77 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+// This test ensures that we can create SourceActors and SourceClients properly,
+// and that they can communicate over the protocol to fetch the source text for
+// a given script.
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-grips");
+  gDebuggee.eval(function stopMe(arg1) {
+    debugger;
+  }.toString());
+
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestGlobalClientAndResume(gClient, "test-grips", function(aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      gThreadClient.addListener("unsolicitedPause", unsolicitedPauseListener);
+      test_source();
+    });
+  });
+  do_test_pending();
+}
+
+function unsolicitedPauseListener(aEvent, aPacket, aContinue) {
+  gContinue = aContinue;
+}
+
+function test_source()
+{
+  DebuggerServer.LONG_STRING_LENGTH = 200;
+
+  gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+    gThreadClient.getScripts(function (aResponse) {
+      do_check_true(!!aResponse);
+      do_check_true(!!aResponse.scripts);
+
+      let script = aResponse.scripts.filter(function (s) {
+        return s.url.match(/test_source-01.js$/);
+      })[0];
+
+      do_check_true(!!script);
+      do_check_true(!!script.source);
+
+      let sourceClient = gThreadClient.source(script.source);
+      sourceClient.source(function (aResponse) {
+        do_check_true(!!aResponse);
+        do_check_true(!aResponse.error);
+        do_check_true(!!aResponse.source);
+
+        let f = do_get_file("test_source-01.js", false);
+        let s = Cc["@mozilla.org/network/file-input-stream;1"]
+          .createInstance(Ci.nsIFileInputStream);
+        s.init(f, -1, -1, false);
+
+        do_check_eq(NetUtil.readInputStreamToString(s, s.available()),
+                    aResponse.source);
+
+        s.close();
+        gThreadClient.resume(function () {
+          finishClient(gClient);
+        });
+      });
+    });
+  });
+
+  gDebuggee.eval('stopMe()');
+}
--- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini
@@ -59,9 +59,10 @@ tail =
 [test_framebindings-03.js]
 [test_framebindings-04.js]
 [test_framebindings-05.js]
 [test_pause_exceptions-01.js]
 [test_pause_exceptions-02.js]
 [test_longstringactor.js]
 [test_longstringgrips-01.js]
 [test_longstringgrips-02.js]
+[test_source-01.js]
 [test_breakpointstore.js]