merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 18 Dec 2014 13:21:40 +0100
changeset 220271 088baba391f723981976db466690e57cefc6034a
parent 220251 5c7a6294b82a7b8d5c2cbd2be3d4ed98db79a2d4 (current diff)
parent 220270 f23bac4bf8062ffc62d220adcf89a07b0d31d7ab (diff)
child 220330 9bb8b0b4daae5f534c5d809a5a502375571474ae
push id27982
push usercbook@mozilla.com
push dateThu, 18 Dec 2014 12:24:30 +0000
treeherdermozilla-central@088baba391f7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
toolkit/components/reader/content/ReaderMode.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1658,16 +1658,17 @@ pref("shumway.disabled", true);
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 pref("loop.enabled", true);
 pref("loop.server", "https://loop.services.mozilla.com/v0");
 pref("loop.seenToS", "unseen");
 pref("loop.gettingStarted.seen", false);
 pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start");
+pref("loop.gettingStarted.resumeOnFirstJoin", false);
 pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
 pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -326,17 +326,19 @@ let gFxAccounts = {
       break;
     case "error":
       this.openSignInAgainPage("menupanel");
       break;
     case "migrate-signup":
       fxaMigrator.createFxAccount(window);
       break;
     case "migrate-verify":
-      fxaMigrator.resendVerificationMail();
+      // Instead of using the migrator module directly here the UX calls for
+      // us to open prefs which has a "resend" button.
+      this.openPreferences();
       break;
     default:
       this.openAccountsPage(null, { entryPoint: "menupanel" });
       break;
     }
 
     PanelUI.hide();
   },
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -1,16 +1,17 @@
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 // the "exported" symbols
 let LoopUI;
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
   LoopUI = {
     get toolbarButton() {
       delete this.toolbarButton;
@@ -79,18 +80,80 @@ XPCOMUtils.defineLazyModuleGetter(this, 
               showTab();
             });
           }, true);
         };
 
         // Used to clear the temporary "login" state from the button.
         Services.obs.notifyObservers(null, "loop-status-changed", null);
 
-        PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
-                             "loop", null, "about:looppanel", null, callback);
+        this.shouldResumeTour().then((resume) => {
+          if (resume) {
+            // Assume the conversation with the visitor wasn't open since we would
+            // have resumed the tour as soon as the visitor joined if it was (and
+            // the pref would have been set to false already.
+            MozLoopService.resumeTour("waiting");
+            resolve();
+            return;
+          }
+
+          PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
+                               "loop", null, "about:looppanel", null, callback);
+        });
+      });
+    },
+
+    /**
+     * Method to know whether actions to open the panel should instead resume the tour.
+     *
+     * We need the panel to be opened via UITour so that it gets @noautohide.
+     *
+     * @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
+     *                   opening the panel.
+     */
+    shouldResumeTour: Task.async(function* () {
+      // Resume the FTU tour if this is the first time a room was joined by
+      // someone else since the tour.
+      if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
+        return false;
+      }
+
+      if (!LoopRooms.participantsCount) {
+        // Nobody is in the rooms
+        return false;
+      }
+
+      let roomsWithNonOwners = yield this.roomsWithNonOwners();
+      if (!roomsWithNonOwners.length) {
+        // We were the only one in a room but we want to know about someone else joining.
+        return false;
+      }
+
+      return true;
+    }),
+
+    /**
+     * @return {Promise} resolved with an array of Rooms with participants (excluding owners)
+     */
+    roomsWithNonOwners: function() {
+      return new Promise(resolve => {
+        LoopRooms.getAll((error, rooms) => {
+          let roomsWithNonOwners = [];
+          for (let room of rooms) {
+            if (!("participants" in room)) {
+              continue;
+            }
+            let numNonOwners = room.participants.filter(participant => !participant.owner).length;
+            if (!numNonOwners) {
+              continue;
+            }
+            roomsWithNonOwners.push(room);
+          }
+          resolve(roomsWithNonOwners);
+        });
       });
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
     init: function() {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7471,16 +7471,17 @@ let gRemoteTabsUI = {
  *        matching when comparing URIs and overwrite the initial query string with
  *        the one from the new URI.
  * @return True if an existing tab was found, false otherwise
  */
 function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) {
   // Certain URLs can be switched to irrespective of the source or destination
   // window being in private browsing mode:
   const kPrivateBrowsingWhitelist = new Set([
+    "about:addons",
     "about:customizing",
   ]);
 
   let ignoreFragment = aOpenParams.ignoreFragment;
   let ignoreQueryString = aOpenParams.ignoreQueryString;
   let replaceQueryString = aOpenParams.replaceQueryString;
 
   // These properties are only used by switchToTabHavingURI and should
@@ -7732,17 +7733,18 @@ var ResponsiveUI = {
 
 XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
   let tmp = {};
   Cu.import("resource:///modules/devtools/responsivedesign.jsm", tmp);
   return tmp.ResponsiveUIManager;
 });
 
 function openEyedropper() {
-  var eyedropper = new this.Eyedropper(this);
+  var eyedropper = new this.Eyedropper(this, { context: "menu",
+                                               copyOnSelect: true });
   eyedropper.open();
 }
 
 Object.defineProperty(this, "Eyedropper", {
   get: function() {
     let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
     return devtools.require("devtools/eyedropper/eyedropper").Eyedropper;
   },
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -481,8 +481,9 @@ skip-if = e10s
 skip-if = e10s # Bug 1100687 - test directly manipulates content (content.document.getElementById)
 [browser_bug1045809.js]
 [browser_e10s_switchbrowser.js]
 [browser_blockHPKP.js]
 skip-if = e10s # bug 1100687 - test directly manipulates content (content.document.getElementById)
 [browser_mcb_redirect.js]
 skip-if = e10s # bug 1084504 - [e10s] Mixed content detection does not take redirection into account
 [browser_windowactivation.js]
+[browser_bug963945.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug963945.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This test ensures the about:addons tab is only
+ * opened one time when in private browsing.
+ */
+
+function test() {
+  waitForExplicitFinish();
+	
+  var win = OpenBrowserWindow({private: true});
+
+  whenDelayedStartupFinished(win, function() {
+    win.gBrowser.loadURI("about:addons");
+
+    waitForFocus(function() {
+      EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
+
+      is(win.gBrowser.tabs.length, 1, "about:addons tab was re-focused.");
+      is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
+
+      win.close();
+      finish();    
+    });
+  });
+}
+
+
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -934,22 +934,34 @@
           if (searchBar && searchBar.textbox == this.mInput) {
             // Handle search bar popup clicks
             var search = controller.getValueAt(this.selectedIndex);
 
             // close the autocomplete popup and revert the entered search term
             this.closePopup();
             controller.handleEscape();
 
-            // Fill in the search bar's value
-            searchBar.value = search;
-
             // open the search results according to the clicking subtlety
             var where = whereToOpenLink(aEvent, false, true);
+
+            // But open ctrl/cmd clicks on autocomplete items in a new background tab.
+            if (where == "tab" && (aEvent instanceof MouseEvent) &&
+                (aEvent.button == 1 ||
+#ifdef XP_MACOSX
+                 aEvent.metaKey))
+#else
+                 aEvent.ctrlKey))
+#endif
+              where = "tab-background";
+
             searchBar.doSearch(search, where);
+            if (where == "tab-background")
+              searchBar.focus();
+            else
+              searchBar.value = search;
           }
         ]]></body>
       </method>
     </implementation>
   </binding>
 
   <!-- Note: this binding is applied to the autocomplete popup used in the Search bar -->
   <binding id="browser-search-autocomplete-result-popup" extends="chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup">
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -150,17 +150,17 @@ let LoopRoomsInternal = {
    *
    * @param {String}   [version] If set, we will fetch a list of changed rooms since
    *                             `version`. Optional.
    * @param {Function} callback  Function that will be invoked once the operation
    *                             finished. The first argument passed will be an
    *                             `Error` object or `null`. The second argument will
    *                             be the list of rooms, if it was fetched successfully.
    */
-  getAll: function(version = null, callback) {
+  getAll: function(version = null, callback = null) {
     if (!callback) {
       callback = version;
       version = null;
     }
 
     Task.spawn(function* () {
       if (!gDirty) {
         callback(null, [...this.rooms.values()]);
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1094,16 +1094,43 @@ this.MozLoopService = {
           sound: "room-joined",
           title: room.roomName,
           message: MozLoopServiceInternal.localizedStrings.get("rooms_room_joined_label"),
           selectTab: "rooms"
         });
       }
     });
 
+    // Resume the tour (re-opening the tab, if necessary) if someone else joins
+    // a room of ours and it's currently open.
+    LoopRooms.on("joined", (e, room, participant) => {
+      let isOwnerInRoom = false;
+      let isOtherInRoom = false;
+
+      if (!room.participants) {
+        return;
+      }
+
+      // The particpant that joined isn't necessarily included in room.participants (depending on
+      // when the broadcast happens) so concatenate.
+      for (let participant of room.participants.concat(participant)) {
+        if (participant.owner) {
+          isOwnerInRoom = true;
+        } else {
+          isOtherInRoom = true;
+        }
+      }
+
+      if (!isOwnerInRoom || !isOtherInRoom) {
+        return;
+      }
+
+      this.resumeTour("open");
+    });
+
     // If expiresTime is not in the future and the user hasn't
     // previously authenticated then skip registration.
     if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
         !LoopRooms.getGuestCreatedRoom() &&
         !MozLoopServiceInternal.fxAOAuthTokenData) {
       return Promise.resolve("registration not needed");
     }
 
@@ -1472,32 +1499,73 @@ this.MozLoopService = {
       let win = Services.wm.getMostRecentWindow("navigator:browser");
       win.switchToTabHavingURI(url.toString(), true);
     } catch (ex) {
       log.error("Error opening FxA settings", ex);
     }
   }),
 
   /**
+   * Gets the tour URL.
+   *
+   * @param {String} aSrc A string representing the entry point to begin the tour, optional.
+   * @param {Object} aAdditionalParams An object with keys used as query parameter names
+   */
+  getTourURL: function(aSrc = null, aAdditionalParams = {}) {
+    let urlStr = this.getLoopPref("gettingStarted.url");
+    let url = new URL(Services.urlFormatter.formatURL(urlStr));
+    for (let paramName in aAdditionalParams) {
+      url.searchParams.append(paramName, aAdditionalParams[paramName]);
+    }
+    if (aSrc) {
+      url.searchParams.set("utm_source", "firefox-browser");
+      url.searchParams.set("utm_medium", "firefox-browser");
+      url.searchParams.set("utm_campaign", aSrc);
+    }
+    return url;
+  },
+
+  resumeTour: function(aIncomingConversationState) {
+    let url = this.getTourURL("resume-with-conversation", {
+      incomingConversation: aIncomingConversationState,
+    });
+
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+
+    this.setLoopPref("gettingStarted.resumeOnFirstJoin", false);
+
+    // The query parameters of the url can vary but we always want to re-use a Loop tour tab that's
+    // already open so we ignore the fragment and query string.
+    let hadExistingTab = win.switchToTabHavingURI(url, true, {
+      ignoreFragment: true,
+      ignoreQueryString: true,
+    });
+
+    // If the tab was already open, send an event instead of using the query
+    // parameter above (that we don't replace on existing tabs to avoid a reload).
+    if (hadExistingTab) {
+      UITour.notify("Loop:IncomingConversation", {
+        conversationOpen: aIncomingConversationState === "open",
+      });
+    }
+  },
+
+  /**
    * Opens the Getting Started tour in the browser.
    *
-   * @param {String} aSrc
-   *   - The UI element that the user used to begin the tour, optional.
+   * @param {String} [aSrc] A string representing the entry point to begin the tour, optional.
    */
   openGettingStartedTour: Task.async(function(aSrc = null) {
     try {
-      let urlStr = Services.prefs.getCharPref("loop.gettingStarted.url");
-      let url = new URL(Services.urlFormatter.formatURL(urlStr));
-      if (aSrc) {
-        url.searchParams.set("utm_source", "firefox-browser");
-        url.searchParams.set("utm_medium", "firefox-browser");
-        url.searchParams.set("utm_campaign", aSrc);
-      }
+      let url = this.getTourURL(aSrc);
       let win = Services.wm.getMostRecentWindow("navigator:browser");
-      win.switchToTabHavingURI(url, true, {replaceQueryString: true});
+      win.switchToTabHavingURI(url, true, {
+        ignoreFragment: true,
+        replaceQueryString: true,
+      });
     } catch (ex) {
       log.error("Error opening Getting Started tour", ex);
     }
   }),
 
   /**
    * Performs a hawk based request to the loop server.
    *
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -161,21 +161,24 @@ loop.panel = (function(_, mozL10n) {
             )
           )
         )
       );
     }
   });
 
   var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
+    mixins: [sharedMixins.WindowCloseMixin],
+
     handleButtonClick: function() {
       navigator.mozLoop.openGettingStartedTour("getting-started");
       navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
       var event = new CustomEvent("GettingStartedSeen");
       window.dispatchEvent(event);
+      this.closeWindow();
     },
 
     render: function() {
       if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
         return null;
       }
       return (
         React.DOM.div({id: "fte-getstarted"}, 
@@ -264,17 +267,17 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
   var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
-    mixins: [sharedMixins.DropdownMenuMixin],
+    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
 
     handleClickSettingsEntry: function() {
       // XXX to be implemented at the same time as unhiding the entry
     },
 
     handleClickAccountEntry: function() {
       navigator.mozLoop.openFxASettings();
     },
@@ -295,16 +298,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
     openGettingStartedTour: function() {
       navigator.mozLoop.openGettingStartedTour("settings-menu");
+      this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
 
       // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
       if (!navigator.mozLoop.fxAEnabled) {
         return null;
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -161,21 +161,24 @@ loop.panel = (function(_, mozL10n) {
             </li>
           </ul>
         </div>
       );
     }
   });
 
   var GettingStartedView = React.createClass({
+    mixins: [sharedMixins.WindowCloseMixin],
+
     handleButtonClick: function() {
       navigator.mozLoop.openGettingStartedTour("getting-started");
       navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
       var event = new CustomEvent("GettingStartedSeen");
       window.dispatchEvent(event);
+      this.closeWindow();
     },
 
     render: function() {
       if (navigator.mozLoop.getLoopPref("gettingStarted.seen")) {
         return null;
       }
       return (
         <div id="fte-getstarted">
@@ -264,17 +267,17 @@ loop.panel = (function(_, mozL10n) {
       );
     }
   });
 
   /**
    * Panel settings (gear) menu.
    */
   var SettingsDropdown = React.createClass({
-    mixins: [sharedMixins.DropdownMenuMixin],
+    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
 
     handleClickSettingsEntry: function() {
       // XXX to be implemented at the same time as unhiding the entry
     },
 
     handleClickAccountEntry: function() {
       navigator.mozLoop.openFxASettings();
     },
@@ -295,16 +298,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
     openGettingStartedTour: function() {
       navigator.mozLoop.openGettingStartedTour("settings-menu");
+      this.closeWindow();
     },
 
     render: function() {
       var cx = React.addons.classSet;
 
       // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
       if (!navigator.mozLoop.fxAEnabled) {
         return null;
--- a/browser/components/preferences/in-content/subdialogs.js
+++ b/browser/components/preferences/in-content/subdialogs.js
@@ -84,16 +84,19 @@ let gSubDialog = {
         Cu.reportError(ex);
       }
       this._closingCallback = null;
     }
 
     this._overlay.style.visibility = "";
     // Clear the sizing inline styles.
     this._frame.removeAttribute("style");
+    // Clear the sizing attributes
+    this._box.removeAttribute("width");
+    this._box.removeAttribute("height");
 
     setTimeout(() => {
       // Unload the dialog after the event listeners run so that the load of about:blank isn't
       // cancelled by the ESC <key>.
       this._frame.loadURI("about:blank");
     }, 0);
   },
 
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -4,33 +4,38 @@
 
 Components.utils.import("resource://services-sync/main.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
   return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+  "resource://gre/modules/FxAccounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
+  "resource://services-sync/FxaMigrator.jsm");
+
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
 const PAGE_NEEDS_UPDATE = 2;
 const PAGE_PLEASE_WAIT = 3;
 const FXA_PAGE_LOGGED_OUT = 4;
 const FXA_PAGE_LOGGED_IN = 5;
 
 // Indexes into the "login status" deck.
 // We are in a successful verified state - everything should work!
 const FXA_LOGIN_VERIFIED = 0;
 // We have logged in to an unverified account.
 const FXA_LOGIN_UNVERIFIED = 1;
 // We are logged in locally, but the server rejected our credentials.
 const FXA_LOGIN_FAILED = 2;
 
 let gSyncPane = {
-  _stringBundle: null,
   prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
               "engine.tabs", "engine.history"],
 
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
   },
 
   set page(val) {
@@ -86,31 +91,48 @@ let gSyncPane = {
 
   _init: function () {
     let topics = ["weave:service:login:error",
                   "weave:service:login:finish",
                   "weave:service:start-over:finish",
                   "weave:service:setup-complete",
                   "weave:service:logout:finish",
                   FxAccountsCommon.ONVERIFIED_NOTIFICATION];
+    let migrateTopic = "fxa-migration:state-changed";
 
     // Add the observers now and remove them on unload
     //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
     //        of `this`. Fix in a followup. (bug 583347)
     topics.forEach(function (topic) {
       Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this);
     }, this);
+    // The FxA migration observer is a special case.
+    Weave.Svc.Obs.add(migrateTopic, this.updateMigrationState, this);
+
     window.addEventListener("unload", function() {
       topics.forEach(function (topic) {
         Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
       }, gSyncPane);
+      Weave.Svc.Obs.remove(migrateTopic, gSyncPane.updateMigrationState, gSyncPane);
     }, false);
 
-    this._stringBundle =
-      Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
+    // ask the migration module to broadcast its current state (and nothing will
+    // happen if it's not loaded - which is good, as that means no migration
+    // is pending/necessary) - we don't want to suck that module in just to
+    // find there's nothing to do.
+    Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
+
+    XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
+      return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
+    }),
+
+    XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
+      return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+    }),
+
     this.updateWeavePrefs();
   },
 
   _setupEventListeners: function() {
     function setEventListener(aId, aEventType, aCallback)
     {
       document.getElementById(aId)
               .addEventListener(aEventType, aCallback.bind(gSyncPane));
@@ -187,28 +209,38 @@ let gSyncPane = {
     setEventListener("rejectUnlinkFxaAccount", "click", function () {
       gSyncPane.unlinkFirefoxAccount(true);
     });
     setEventListener("fxaSyncComputerName", "change", function () {
       gSyncUtils.changeName(this);
     });
     setEventListener("tosPP-small-ToS", "click", gSyncPane.openToS);
     setEventListener("tosPP-small-PP", "click", gSyncPane.openPrivacyPolicy);
+    setEventListener("sync-migrate-upgrade", "click", function () {
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      fxaMigrator.createFxAccount(win);
+    });
+    setEventListener("sync-migrate-forget", "click", function () {
+      fxaMigrator.forgetFxAccount();
+    });
+    setEventListener("sync-migrate-resend", "click", function () {
+      let win = Services.wm.getMostRecentWindow("navigator:browser");
+      fxaMigrator.resendVerificationMail(win);
+    });
   },
 
   updateWeavePrefs: function () {
     let service = Components.classes["@mozilla.org/weave/service;1"]
                   .getService(Components.interfaces.nsISupports)
                   .wrappedJSObject;
     // service.fxAccountsEnabled is false iff sync is already configured for
     // the legacy provider.
     if (service.fxAccountsEnabled) {
       // determine the fxa status...
       this.page = PAGE_PLEASE_WAIT;
-      Components.utils.import("resource://gre/modules/FxAccounts.jsm");
       fxAccounts.getSignedInUser().then(data => {
         if (!data) {
           this.page = FXA_PAGE_LOGGED_OUT;
           return;
         }
         this.page = FXA_PAGE_LOGGED_IN;
         // We are logged in locally, but maybe we are in a state where the
         // server rejected our credentials (eg, password changed on the server)
@@ -257,16 +289,55 @@ let gSyncPane = {
     } else {
       this.page = PAGE_HAS_ACCOUNT;
       document.getElementById("accountName").textContent = Weave.Service.identity.account;
       document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP-normal").hidden = this._usingCustomServer;
     }
   },
 
+  updateMigrationState: function(subject, state) {
+    let selIndex;
+    switch (state) {
+      case fxaMigrator.STATE_USER_FXA: {
+        let sb = this._accountsStringBundle;
+        let button = document.getElementById("sync-migrate-upgrade");
+        button.setAttribute("label", sb.GetStringFromName("upgradeToFxA.label"));
+        button.setAttribute("accesskey", sb.GetStringFromName("upgradeToFxA.accessKey"));
+        selIndex = 0;
+        break;
+      }
+      case fxaMigrator.STATE_USER_FXA_VERIFIED: {
+        let sb = this._accountsStringBundle;
+        let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
+        let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
+        let elt = document.getElementById("sync-migrate-verify-label");
+        elt.setAttribute("value", label);
+        // The "resend" button.
+        let button = document.getElementById("sync-migrate-resend");
+        button.setAttribute("label", sb.GetStringFromName("resendVerificationEmail.label"));
+        button.setAttribute("accesskey", sb.GetStringFromName("resendVerificationEmail.accessKey"));
+        // The "forget" button.
+        button = document.getElementById("sync-migrate-forget");
+        button.setAttribute("label", sb.GetStringFromName("forgetMigration.label"));
+        button.setAttribute("accesskey", sb.GetStringFromName("forgetMigration.accessKey"));
+        selIndex = 1;
+        break;
+      }
+      default:
+        if (state) { // |null| is expected, but everything else is not.
+          Cu.reportError("updateMigrationState has unknown state: " + state);
+        }
+        document.getElementById("sync-migration").hidden = true;
+        return;
+    }
+    document.getElementById("sync-migration").hidden = false;
+    document.getElementById("sync-migration-deck").selectedIndex = selIndex;
+  },
+
   startOver: function (showDialog) {
     if (showDialog) {
       let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
                   Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
                   Services.prompt.BUTTON_POS_1_DEFAULT;
       let buttonChoice =
         Services.prompt.confirmEx(window,
                                   this._stringBundle.GetStringFromName("syncUnlink.title"),
@@ -370,24 +441,23 @@ let gSyncPane = {
   },
 
   manageFirefoxAccount: function() {
     let url = Services.prefs.getCharPref("identity.fxaccounts.settings.uri");
     this.openContentInBrowser(url);
   },
 
   verifyFirefoxAccount: function() {
-    Components.utils.import("resource://gre/modules/FxAccounts.jsm");
     fxAccounts.resendVerificationEmail().then(() => {
       fxAccounts.getSignedInUser().then(data => {
-        let sb = this._stringBundle;
-        let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle");
-        let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading",
+        let sb = this._accountsStringBundle;
+        let title = sb.GetStringFromName("verificationSentTitle");
+        let heading = sb.formatStringFromName("verificationSentHeading",
                                               [data.email], 1);
-        let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription");
+        let description = sb.GetStringFromName("verificationSentDescription");
 
         let factory = Cc["@mozilla.org/prompter;1"]
                         .getService(Ci.nsIPromptFactory);
         let prompt = factory.getPrompt(window, Ci.nsIPrompt);
         let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
         bag.setPropertyAsBool("allowTabModal", true);
 
         prompt.alert(title, heading + "\n\n" + description);
@@ -425,17 +495,16 @@ let gSyncPane = {
 
       let pressed = prompt.confirmEx(title, body, buttonFlags,
                                      continueLabel, null, null, null, {});
 
       if (pressed != 0) { // 0 is the "continue" button
         return;
       }
     }
-    Cu.import("resource://gre/modules/FxAccounts.jsm");
     fxAccounts.signOut().then(() => {
       this.updateWeavePrefs();
     });
   },
 
   openQuotaDialog: function () {
     let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
     if (win)
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -32,16 +32,41 @@
 
 <hbox id="header-sync"
       class="header"
       hidden="true"
       data-category="paneSync">
   <label class="header-name">&paneSync.title;</label>
 </hbox>
 
+<hbox id="sync-migration-container"
+       data-category="paneSync"
+       hidden="true">
+
+  <vbox id="sync-migration" flex="1" hidden="true">
+
+    <deck id="sync-migration-deck">
+      <!-- When we are in the "need FxA user" state -->
+      <hbox align="center">
+        <label>&migrate.upgradeNeeded;</label>
+        <spacer flex="1"/>
+        <button id="sync-migrate-upgrade"/>
+      </hbox>
+
+      <!-- When we are in the "need the user to be verified" state -->
+      <hbox align="center">
+        <label id="sync-migrate-verify-label"/>
+        <spacer flex="1"/>
+        <button id="sync-migrate-forget"/>
+        <button id="sync-migrate-resend"/>
+      </hbox>
+    </deck>
+  </vbox>
+</hbox>
+
 <deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
   <!-- These panels are for the "legacy" sync provider -->
   <vbox id="noAccount" align="center">
     <spacer flex="1"/>
     <description id="syncDesc">
       &weaveDesc.label;
     </description>
     <separator/>
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -280,21 +280,21 @@ let gSyncPane = {
     let url = Services.prefs.getCharPref("identity.fxaccounts.settings.uri");
     this.openContentInBrowser(url);
   },
 
   verifyFirefoxAccount: function() {
     Components.utils.import("resource://gre/modules/FxAccounts.jsm");
     fxAccounts.resendVerificationEmail().then(() => {
       fxAccounts.getSignedInUser().then(data => {
-        let sb = this._stringBundle;
-        let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle");
-        let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading",
+        let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+        let title = sb.GetStringFromName("verificationSentTitle");
+        let heading = sb.formatStringFromName("verificationSentHeading",
                                               [data.email], 1);
-        let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription");
+        let description = sb.GetStringFromName("verificationSentDescription");
 
         Services.prompt.alert(window, title, heading + "\n\n" + description);
       });
     });
   },
 
   openOldSyncSupportPage: function() {
     let url = Services.urlFormatter.formatURLPref('app.support.baseURL') + "old-sync"
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -467,28 +467,39 @@
       <method name="handleSearchCommand">
         <parameter name="aEvent"/>
         <parameter name="aEngine"/>
         <body><![CDATA[
           var textBox = this._textbox;
           var textValue = textBox.value;
 
           var where = "current";
+
+          // Open ctrl/cmd clicks on one-off buttons in a new background tab.
           if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
             if (aEvent.button == 2)
               return;
             where = whereToOpenLink(aEvent, false, true);
           }
           else {
             var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab");
-            if ((aEvent && aEvent.altKey) ^ newTabPref)
+            if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
               where = "tab";
+            if ((aEvent instanceof MouseEvent) && (aEvent.button == 1 ||
+#ifdef XP_MACOSX
+                                                   aEvent.metaKey))
+#else
+                                                   aEvent.ctrlKey))
+#endif
+              where = "tab-background";
           }
 
           this.doSearch(textValue, where, aEngine);
+          if (where == "tab-background")
+            this.focus();
         ]]></body>
       </method>
 
       <method name="doSearch">
         <parameter name="aData"/>
         <parameter name="aWhere"/>
         <parameter name="aEngine"/>
         <body><![CDATA[
@@ -504,17 +515,23 @@
                   Components.utils.reportError("Saving search to form history failed: " + aError.message);
               }});
           }
 
           let engine = aEngine || this.currentEngine;
           var submission = engine.getSubmission(aData, null, "searchbar");
           BrowserSearch.recordSearchInHealthReport(engine, "searchbar");
           // null parameter below specifies HTML response for search
-          openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
+          let params = {
+            postData: submission.postData,
+            inBackground: aWhere == "tab-background"
+          };
+          openUILinkIn(submission.uri.spec,
+                       aWhere == "tab-background" ? "tab" : aWhere,
+                       params);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="command"><![CDATA[
         const target = event.originalTarget;
         if (target.engine) {
--- a/browser/devtools/eyedropper/commands.js
+++ b/browser/devtools/eyedropper/commands.js
@@ -33,17 +33,19 @@ exports.items = [{
     offChange: function(target, changeHandler) {
       eventEmitter.off("changed", changeHandler);
     },
   },
   exec: function(args, context) {
     let chromeWindow = context.environment.chromeWindow;
     let target = context.environment.target;
 
-    let dropper = EyedropperManager.createInstance(chromeWindow);
+    let dropper = EyedropperManager.createInstance(chromeWindow,
+                                                   { context: "command",
+                                                     copyOnSelect: true });
     dropper.open();
 
     eventEmitter.emit("changed", { target: target });
 
     dropper.once("destroy", () => {
       eventEmitter.emit("changed", { target: target });
     });
   }
--- a/browser/devtools/eyedropper/eyedropper.js
+++ b/browser/devtools/eyedropper/eyedropper.js
@@ -1,14 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const {Cc, Ci, Cu} = require("chrome");
 const {rgbToHsl} = require("devtools/css-color").colorUtils;
+const Telemetry = require("devtools/shared/telemetry");
 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"]
@@ -56,23 +57,23 @@ const HSL_BOX_WIDTH = 158;
  */
 let EyedropperManager = {
   _instances: new WeakMap(),
 
   getInstance: function(chromeWindow) {
     return this._instances.get(chromeWindow);
   },
 
-  createInstance: function(chromeWindow) {
+  createInstance: function(chromeWindow, options) {
     let dropper = this.getInstance(chromeWindow);
     if (dropper) {
       return dropper;
     }
 
-    dropper = new Eyedropper(chromeWindow);
+    dropper = new Eyedropper(chromeWindow, options);
     this._instances.set(chromeWindow, dropper);
 
     dropper.on("destroy", () => {
       this.deleteInstance(chromeWindow);
     });
 
     return dropper;
   },
@@ -95,19 +96,19 @@ exports.EyedropperManager = EyedropperMa
  *
  * eyedropper.once("select", (ev, color) => {
  *   console.log(color);  // "rgb(20, 50, 230)"
  * })
  *
  * @param {DOMWindow} chromeWindow
  *        window to inspect
  * @param {object} opts
- *        optional options object, with 'copyOnSelect'
+ *        optional options object, with 'copyOnSelect', 'context'
  */
-function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
+function Eyedropper(chromeWindow, opts = { copyOnSelect: true, context: "other" }) {
   this.copyOnSelect = opts.copyOnSelect;
 
   this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
   this._onMouseMove = this._onMouseMove.bind(this);
   this._onMouseDown = this._onMouseDown.bind(this);
   this._onKeyDown = this._onKeyDown.bind(this);
   this._onFrameLoaded = this._onFrameLoaded.bind(this);
 
@@ -129,16 +130,28 @@ function Eyedropper(chromeWindow, opts =
     y: 0,          // the top coordinate of the center of the inspected region
     width: CANVAS_WIDTH,      // width of canvas to draw zoomed area onto
     height: CANVAS_WIDTH      // height of canvas
   };
 
   let mm = this._contentTab.linkedBrowser.messageManager;
   mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true);
 
+  // record if this was opened via the picker or standalone
+  var telemetry = new Telemetry();
+  if (opts.context == "command") {
+    telemetry.toolOpened("eyedropper");
+  }
+  else if (opts.context == "menu") {
+    telemetry.toolOpened("menueyedropper");
+  }
+  else if (opts.context == "picker") {
+    telemetry.toolOpened("pickereyedropper");
+  }
+
   EventEmitter.decorate(this);
 }
 
 exports.Eyedropper = Eyedropper;
 
 Eyedropper.prototype = {
   /**
    * Get the number of cells (blown-up pixels) per direction in the grid.
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -11,17 +11,17 @@ const ENSURE_SELECTION_VISIBLE_DELAY = 5
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
 const MAX_LABEL_LENGTH = 40;
 
-let promise = require("devtools/toolkit/deprecated-sync-thenables");
+let promise = require("resource://gre/modules/Promise.jsm").Promise;
 
 const LOW_PRIORITY_ELEMENTS = {
   "HEAD": true,
   "BASE": true,
   "BASEFONT": true,
   "ISINDEX": true,
   "LINK": true,
   "META": true,
@@ -133,23 +133,24 @@ HTMLBreadcrumbs.prototype = {
       if (selection != this.selection.nodeFront) {
         return promise.reject("selection-changed");
       }
       return result;
     }
   },
 
   /**
-   * Print any errors (except selection guard errors).
+   * Warn if rejection was caused by selection change, print an error otherwise.
    */
   selectionGuardEnd: function(err) {
-    if (err != "selection-changed") {
+    if (err === "selection-changed") {
+      console.warn("Asynchronous operation was aborted as selection changed.");
+    } else {
       console.error(err);
     }
-    promise.reject(err);
   },
 
   /**
    * Build a string that represents the node: tagName#id.class1.class2.
    *
    * @param aNode The node to pretty-print
    * @returns a string
    */
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -3,17 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 Cu.import("resource://gre/modules/Services.jsm");
 
-let promise = require("devtools/toolkit/deprecated-sync-thenables");
+let promise = require("resource://gre/modules/Promise.jsm").Promise;
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let clipboard = require("sdk/clipboard");
 
 loader.lazyGetter(this, "MarkupView", () => require("devtools/markupview/markup-view").MarkupView);
 loader.lazyGetter(this, "HTMLBreadcrumbs", () => require("devtools/inspector/breadcrumbs").HTMLBreadcrumbs);
 loader.lazyGetter(this, "ToolSidebar", () => require("devtools/framework/sidebar").ToolSidebar);
 loader.lazyGetter(this, "SelectorSearch", () => require("devtools/inspector/selector-search").SelectorSearch);
 
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,15 +1,15 @@
 /* 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 promise = require("devtools/toolkit/deprecated-sync-thenables");
+const promise = require("resource://gre/modules/Promise.jsm").Promise;
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
--- a/browser/devtools/shared/telemetry.js
+++ b/browser/devtools/shared/telemetry.js
@@ -165,16 +165,28 @@ Telemetry.prototype = {
       userHistogram: "DEVTOOLS_SCRATCHPAD_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_SCRATCHPAD_TIME_ACTIVE_SECONDS"
     },
     responsive: {
       histogram: "DEVTOOLS_RESPONSIVE_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
     },
+    eyedropper: {
+      histogram: "DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG",
+    },
+    menueyedropper: {
+      histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG",
+    },
+    pickereyedropper: {
+      histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN",
+      userHistogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG",
+    },
     developertoolbar: {
       histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",
       timerHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_TIME_ACTIVE_SECONDS"
     },
     webide: {
       histogram: "DEVTOOLS_WEBIDE_OPENED_BOOLEAN",
       userHistogram: "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG",
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -45,16 +45,17 @@ support-files =
 [browser_prefs.js]
 [browser_require_basic.js]
 [browser_spectrum.js]
 [browser_theme.js]
 [browser_tableWidget_basic.js]
 [browser_tableWidget_keyboard_interaction.js]
 [browser_tableWidget_mouse_interaction.js]
 skip-if = buildapp == 'mulet'
+[browser_telemetry_button_eyedropper.js]
 [browser_telemetry_button_paintflashing.js]
 [browser_telemetry_button_responsive.js]
 [browser_telemetry_button_scratchpad.js]
 [browser_telemetry_button_tilt.js]
 skip-if = e10s # Bug 1086492 - Disable tilt for e10s
                # Bug 937166 - Make tilt work in E10S mode
 [browser_telemetry_sidebar.js]
 [browser_telemetry_toolbox.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_telemetry_button_eyedropper.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
+
+let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
+let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+let Telemetry = require("devtools/shared/telemetry");
+
+let { EyedropperManager } = require("devtools/eyedropper/eyedropper");
+
+
+function init() {
+  Telemetry.prototype.telemetryInfo = {};
+  Telemetry.prototype._oldlog = Telemetry.prototype.log;
+  Telemetry.prototype.log = function(histogramId, value) {
+    if (!this.telemetryInfo) {
+      // Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
+      return;
+    }
+    if (histogramId) {
+      if (!this.telemetryInfo[histogramId]) {
+        this.telemetryInfo[histogramId] = [];
+      }
+
+      this.telemetryInfo[histogramId].push(value);
+    }
+  };
+
+  testButton("command-button-eyedropper");
+}
+
+function testButton(id) {
+  info("Testing " + id);
+
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    info("inspector opened");
+
+    let button = toolbox.doc.querySelector("#" + id);
+    ok(button, "Captain, we have the button");
+
+    // open the eyedropper
+    button.click();
+
+    checkResults("_EYEDROPPER_");
+  }).then(null, console.error);
+}
+
+function clickButton(node, clicks) {
+  for (let i = 0; i < clicks; i++) {
+    info("Clicking button " + node.id);
+    node.click();
+  }
+}
+
+function checkResults(histIdFocus) {
+  let result = Telemetry.prototype.telemetryInfo;
+
+  for (let [histId, value] of Iterator(result)) {
+    if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
+        !histId.contains(histIdFocus)) {
+      // Inspector stats are tested in
+      // browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
+      // because we only open the inspector once for this test.
+      continue;
+    }
+
+    if (histId.endsWith("OPENED_PER_USER_FLAG")) {
+      ok(value.length === 1 && value[0] === true,
+         "Per user value " + histId + " has a single value of true");
+    } else if (histId.endsWith("OPENED_BOOLEAN")) {
+      ok(value.length == 1, histId + " has one entry");
+
+      let okay = value.every(function(element) {
+        return element === true;
+      });
+
+      ok(okay, "All " + histId + " entries are === true");
+    }
+  }
+
+  finishUp();
+}
+
+function finishUp() {
+  gBrowser.removeCurrentTab();
+
+  Telemetry.prototype.log = Telemetry.prototype._oldlog;
+  delete Telemetry.prototype._oldlog;
+  delete Telemetry.prototype.telemetryInfo;
+
+  TargetFactory = Services = promise = require = null;
+
+  finish();
+}
+
+function test() {
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    waitForFocus(init, content);
+  }, true);
+
+  content.location = TEST_URI;
+}
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -1081,17 +1081,18 @@ SwatchColorPickerTooltip.prototype = Her
     let toolboxWindow;
     if (windowType != "navigator:browser") {
       // this means the toolbox is in a seperate window. We need to make
       // sure we'll be inspecting the browser window instead
       toolboxWindow = chromeWindow;
       chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
       chromeWindow.focus();
     }
-    let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false });
+    let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false,
+                                                 context: "picker" });
 
     dropper.once("select", (event, color) => {
       if (toolboxWindow) {
         toolboxWindow.focus();
       }
       this._selectColor(color);
     });
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_eyedropper.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_eyedropper.js
@@ -1,14 +1,27 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+// So we can test collecting telemetry on the eyedropper
+let oldCanRecord = Services.telemetry.canRecord;
+Services.telemetry.canRecord = true;
+registerCleanupFunction(function () {
+  Services.telemetry.canRecord = oldCanRecord;
+});
+const HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN";
+const FLAG_HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG";
+const EXPECTED_TELEMETRY = {
+  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": 2,
+  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
+}
+
 const PAGE_CONTENT = [
   '<style type="text/css">',
   '  body {',
   '    background-color: white;',
   '    padding: 0px',
   '  }',
   '',
   '  #div1 {',
@@ -29,16 +42,19 @@ const PAGE_CONTENT = [
 
 const ORIGINAL_COLOR = "rgb(255, 0, 153)";  // #f09
 const EXPECTED_COLOR = "rgb(255, 255, 85)"; // #ff5
 
 // Test opening the eyedropper from the color picker. Pressing escape
 // to close it, and clicking the page to select a color.
 
 add_task(function*() {
+  // clear telemetry so we can get accurate counts
+  clearTelemetry();
+
   yield addTab("data:text/html;charset=utf-8,rule view eyedropper test");
   content.document.body.innerHTML = PAGE_CONTENT;
 
   let {toolbox, inspector, view} = yield openRuleView();
   yield selectNode("#div2", inspector);
 
   let property = getRuleViewProperty(view, "#div2", "background-color");
   let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
@@ -52,16 +68,18 @@ add_task(function*() {
 
   yield testESC(swatch, dropper);
 
   dropper = yield openEyedropper(view, swatch);
 
   ok(dropper, "dropper opened");
 
   yield testSelect(swatch, dropper);
+
+  checkTelemetry();
 });
 
 function testESC(swatch, dropper) {
   let deferred = promise.defer();
 
   dropper.once("destroy", () => {
     let color = swatch.style.backgroundColor;
     is(color, ORIGINAL_COLOR, "swatch didn't change after pressing ESC");
@@ -92,16 +110,33 @@ function testSelect(swatch, dropper) {
     });
   });
 
   inspectPage(dropper);
 
   return deferred.promise;
 }
 
+function clearTelemetry() {
+  for (let histogramId in EXPECTED_TELEMETRY) {
+    let histogram = Services.telemetry.getHistogramById(histogramId);
+    histogram.clear();
+  }
+}
+
+function checkTelemetry() {
+  for (let histogramId in EXPECTED_TELEMETRY) {
+    let expected = EXPECTED_TELEMETRY[histogramId];
+    let histogram = Services.telemetry.getHistogramById(histogramId);
+    let snapshot = histogram.snapshot();
+
+    is (snapshot.counts[1], expected,
+        "eyedropper telemetry value correct for " + histogramId);
+  }
+}
 
 /* Helpers */
 
 function openEyedropper(view, swatch) {
   let deferred = promise.defer();
 
   let tooltip = view.tooltips.colorPicker.tooltip;
 
--- a/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js
+++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-params-objects.js
@@ -23,17 +23,25 @@ function spawnTest() {
   let nodeIds = actors.map(actor => actor.actorID);
 
   click(panelWin, findGraphNode(panelWin, nodeIds[2]));
   yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
   checkVariableView(gVars, 0, {
     "curve": "Float32Array"
   }, "WaveShaper's `curve` is listed as an `Float32Array`.");
 
+  let aVar = gVars.getScopeAtIndex(0).get("curve")
+  let state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+  ok(state, "Float32Array property should not have a dropdown.");
+
   click(panelWin, findGraphNode(panelWin, nodeIds[1]));
   yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
   checkVariableView(gVars, 0, {
     "buffer": "AudioBuffer"
   }, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
 
+  aVar = gVars.getScopeAtIndex(0).get("buffer")
+  state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
+  ok(state, "AudioBuffer property should not have a dropdown.");
+
   yield teardown(panel);
   finish();
 }
--- a/browser/devtools/webaudioeditor/views/inspector.js
+++ b/browser/devtools/webaudioeditor/views/inspector.js
@@ -145,17 +145,20 @@ let InspectorView = {
     // when there are no props i.e. AudioDestinationNode
     this._togglePropertiesView(!!props.length);
 
     props.forEach(({ param, value, flags }) => {
       let descriptor = {
         value: value,
         writable: !flags || !flags.readonly,
       };
-      audioParamsScope.addItem(param, descriptor);
+      let item = audioParamsScope.addItem(param, descriptor);
+
+      // No items should currently display a dropdown
+      item.twisty = false;
     });
 
     audioParamsScope.expanded = true;
 
     window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
   }),
 
   _togglePropertiesView: function (show) {
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -12,8 +12,22 @@ upgradeToFxA.accessKey = U
 
 # LOCALIZATION NOTE (needVerifiedUserShort, needVerifiedUserLong)
 # %S = Email address of user's Firefox Account
 needVerifiedUserShort = %S not verified
 needVerifiedUserLong = Please click the verification link in the email sent to %S
 
 resendVerificationEmail.label = Resend
 resendVerificationEmail.accessKey = R
+
+forgetMigration.label = Forget
+forgetMigration.accessKey = F
+
+# These strings are used in a dialog we display after the user requests we resend
+# a verification email.
+verificationSentTitle = Verification Sent
+# LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
+verificationSentHeading = A verification link has been sent to %S
+verificationSentDescription = Please check your email and click the link to begin syncing.
+
+verificationNotSentTitle = Unable to Send Verification
+verificationNotSentHeading = We are unable to send a verification mail at this time
+verificationNotSentDescription = Please try again later.
--- a/browser/locales/en-US/chrome/browser/preferences/preferences.properties
+++ b/browser/locales/en-US/chrome/browser/preferences/preferences.properties
@@ -132,14 +132,8 @@ updateAutoDesktop.accessKey=A
 syncUnlink.title=Do you want to unlink your device?
 syncUnlink.label=This device will no longer be associated with your Sync account. All of your personal data, both on this device and in your Sync account, will remain intact.
 syncUnlinkConfirm.label=Unlink
 
 # LOCALIZATION NOTE (featureEnableRequiresRestart, featureDisableRequiresRestart, restartTitle): %S = brandShortName
 featureEnableRequiresRestart=%S must restart to enable this feature.
 featureDisableRequiresRestart=%S must restart to disable this feature.
 shouldRestartTitle=Restart %S
-
-###Preferences::Sync::Firefox Accounts
-firefoxAccountsVerificationSentTitle=Verification Sent
-# LOCALIZATION NOTE: %S = user's email address.
-firefoxAccountsVerificationSentHeading=A verification link has been sent to %S
-firefoxAccountVerificationSentDescription=Please check your email and click the link to begin syncing.
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -73,8 +73,11 @@ both, to better adapt this sentence to t
 <!ENTITY verify.label                "Verify Email">
 <!ENTITY forget.label                "Forget this Email">
 
 <!ENTITY welcome.description "Access your tabs, bookmarks, passwords and more wherever you use &brandShortName;.">
 <!ENTITY welcome.signIn.label "Sign In">
 <!ENTITY welcome.createAccount.label "Create Account">
 
 <!ENTITY welcome.useOldSync.label "Using an older version of Sync?">
+
+<!-- Sync Migration -->
+<!ENTITY migrate.upgradeNeeded      "The sync account system is being discontinued. A new Firefox Account is required to sync.">
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -538,16 +538,26 @@ this.UITour = {
           log.warn("getConfiguration: No configuration option specified");
           return false;
         }
 
         this.getConfiguration(messageManager, window, data.configuration, data.callbackID);
         break;
       }
 
+      case "setConfiguration": {
+        if (typeof data.configuration != "string") {
+          log.warn("setConfiguration: No configuration option specified");
+          return false;
+        }
+
+        this.setConfiguration(data.configuration, data.value);
+        break;
+      }
+
       case "showFirefoxAccounts": {
         // 'signup' is the only action that makes sense currently, so we don't
         // accept arbitrary actions just to be safe...
         // We want to replace the current tab.
         contentDocument.location.href = "about:accounts?action=signup&entrypoint=uitour";
         break;
       }
 
@@ -1316,17 +1326,17 @@ this.UITour = {
       panel.setAttribute("noautohide", true);
       if (panel.state != "open") {
         this.recreatePopup(panel);
         this.availableTargetsCache.clear();
       }
 
       // An event object is expected but we don't want to toggle the panel with a click if the panel
       // is already open.
-      aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }).then(() => {
+      aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }, "rooms").then(() => {
         if (aOpenCallback) {
           aOpenCallback();
         }
       });
       panel.addEventListener("popuphidden", this.onPanelHidden);
       panel.addEventListener("popuphiding", this.hideLoopPanelAnnotations);
     } else if (aMenuName == "searchEngines") {
       this.getTarget(aWindow, "searchProvider").then(target => {
@@ -1474,16 +1484,28 @@ this.UITour = {
         });
         break;
       default:
         log.error("getConfiguration: Unknown configuration requested: " + aConfiguration);
         break;
     }
   },
 
+  setConfiguration: function(aConfiguration, aValue) {
+    switch (aConfiguration) {
+      case "Loop:ResumeTourOnFirstJoin":
+        // Ignore aValue in this case to avoid accidentally setting it to false.
+        Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+        break;
+      default:
+        log.error("setConfiguration: Unknown configuration requested: " + aConfiguration);
+        break;
+    }
+  },
+
   getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
     Task.spawn(function*() {
       let window = aChromeWindow;
       let data = this.availableTargetsCache.get(window);
       if (data) {
         log.debug("getAvailableTargets: Using cached targets list", data.targets.join(","));
         this.sendPageCallback(aMessageManager, aCallbackID, data);
         return;
--- a/browser/modules/test/browser_UITour_loop.js
+++ b/browser/modules/test/browser_UITour_loop.js
@@ -168,54 +168,123 @@ let tests = [
     currentTarget = "loop-roomList";
     yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be on the side");
     is(popup.popupBoxObject.alignmentPosition, "start_before", "Check " + currentTarget + " position");
 
     currentTarget = "loop-signInUpLink";
     yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath");
     is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position");
   }),
+  taskify(function* test_setConfiguration() {
+    is(Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin"), false, "pref should be false but exist");
+    gContentAPI.setConfiguration("Loop:ResumeTourOnFirstJoin", true);
+
+    yield waitForConditionPromise(() => {
+      return Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin");
+    }, "Pref should change to true via setConfiguration");
+
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
+  taskify(function* test_resumeViaMenuPanel_roomClosedTabOpen() {
+    Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+
+    // Create a fake room and then add a fake non-owner participant
+    let roomsMap = setupFakeRoom();
+    roomsMap.get("fakeTourRoom").participants = [{
+      owner: false,
+    }];
+
+    // Set the tour URL to be the current page with a different query param
+    let gettingStartedURL = gTestTab.linkedBrowser.currentURI.resolve("?gettingstarted=1");
+    Services.prefs.setCharPref("loop.gettingStarted.url", gettingStartedURL);
+
+    let observationPromise = new Promise((resolve) => {
+      gContentAPI.observe((event, params) => {
+        is(event, "Loop:IncomingConversation", "Page should have been notified about incoming conversation");
+        ise(params.conversationOpen, false, "conversationOpen should be false");
+        is(gBrowser.selectedTab, gTestTab, "The same tab should be selected");
+        resolve();
+      });
+    });
+
+    // Now open the menu while that non-owner is in the fake room to trigger resuming the tour
+    yield showMenuPromise("loop");
+
+    yield observationPromise;
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
+  taskify(function* test_resumeViaMenuPanel_roomClosedTabClosed() {
+    Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
+
+    // Create a fake room and then add a fake non-owner participant
+    let roomsMap = setupFakeRoom();
+    roomsMap.get("fakeTourRoom").participants = [{
+      owner: false,
+    }];
+
+    // Set the tour URL to a page that's not open yet
+    Services.prefs.setCharPref("loop.gettingStarted.url", gBrowser.currentURI.prePath);
+
+    let newTabPromise = waitForConditionPromise(() => {
+      return gBrowser.currentURI.path.contains("incomingConversation=waiting");
+    }, "New tab with incomingConversation=waiting should have opened");
+
+    // Now open the menu while that non-owner is in the fake room to trigger resuming the tour
+    yield showMenuPromise("loop");
+
+    yield newTabPromise;
+
+    yield gBrowser.removeCurrentTab();
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
+  }),
 ];
 
 // End tests
 
 function checkLoopPanelIsHidden() {
   ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up");
   ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
   isnot(loopPanel.state, "open", "The panel shouldn't be open");
   is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed");
 }
 
 function setupFakeRoom() {
   let room = {};
   for (let prop of ["roomToken", "roomName", "roomOwner", "roomUrl", "participants"])
     room[prop] = "fakeTourRoom";
-  LoopRooms.stubCache(new Map([
+  let roomsMap = new Map([
     [room.roomToken, room]
-  ]));
+  ]);
+  LoopRooms.stubCache(roomsMap);
+  return roomsMap;
 }
 
 if (Services.prefs.getBoolPref("loop.enabled")) {
   loopButton = window.LoopUI.toolbarButton.node;
   // The targets to highlight only appear after getting started is launched.
   Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
   Services.prefs.setCharPref("loop.server", "http://localhost");
   Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
 
   registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
     Services.prefs.clearUserPref("loop.gettingStarted.seen");
+    Services.prefs.clearUserPref("loop.gettingStarted.url");
     Services.prefs.clearUserPref("loop.server");
     Services.prefs.clearUserPref("services.push.serverURL");
 
     // Copied from browser/components/loop/test/mochitest/head.js
     // Remove the iframe after each test. This also avoids mochitest complaining
     // about leaks on shutdown as we intentionally hold the iframe open for the
     // life of the application.
     let frameId = loopButton.getAttribute("notificationFrameId");
     let frame = document.getElementById(frameId);
     if (frame) {
       frame.remove();
     }
+
+    // Remove the stubbed rooms
+    LoopRooms.stubCache(null);
   });
 } else {
   ok(true, "Loop is disabled so skip the UITour Loop tests");
   tests = [];
 }
--- a/browser/modules/test/uitour.js
+++ b/browser/modules/test/uitour.js
@@ -218,16 +218,23 @@ if (typeof Mozilla == 'undefined') {
 
 	Mozilla.UITour.getConfiguration = function(configName, callback) {
 		_sendEvent('getConfiguration', {
 			callbackID: _waitForCallback(callback),
 			configuration: configName,
 		});
 	};
 
+	Mozilla.UITour.setConfiguration = function(configName, configValue) {
+		_sendEvent('setConfiguration', {
+			configuration: configName,
+			value: configValue,
+		});
+	};
+
 	Mozilla.UITour.showFirefoxAccounts = function() {
 		_sendEvent('showFirefoxAccounts');
 	};
 
 	Mozilla.UITour.resetFirefox = function() {
 		_sendEvent('resetFirefox');
 	};
 
--- a/browser/themes/linux/preferences/preferences.css
+++ b/browser/themes/linux/preferences/preferences.css
@@ -82,16 +82,28 @@ label.small {
 #BrowserPreferences[animated="true"] #handlersView {
   height: 25em;
 }
 
 #BrowserPreferences[animated="false"] #handlersView {
   -moz-box-flex: 1;
 }
 
+/* Privacy Pane */
+
+/* styles for the link elements copied from .text-link in global.css */
+.inline-link {
+  color: -moz-nativehyperlinktext;
+  text-decoration: none;
+}
+
+.inline-link:hover {
+  text-decoration: underline;
+}
+
 /* Modeless Window Dialogs */
 .windowDialog,
 .windowDialog prefpane {
   padding: 0px;
 }
 
 .contentPane {
   margin: 9px 8px 5px 8px;
--- a/browser/themes/osx/preferences/preferences.css
+++ b/browser/themes/osx/preferences/preferences.css
@@ -180,16 +180,34 @@ caption {
   font-size: 90%;
 }
 
 #isNotDefaultLabel {
   font-weight: bold;
 }
 
 /**
+ * Privacy Pane
+ */
+
+html|a.inline-link {
+  color: -moz-nativehyperlinktext;
+  text-decoration: none;
+}
+
+html|a.inline-link:hover {
+  text-decoration: underline;
+}
+
+html|a.inline-link:-moz-focusring {
+  outline-width: 0;
+  box-shadow: @focusRingShadow@;
+}
+
+/**
  * Update Preferences
  */
 #autoInstallOptions {
   -moz-margin-start: 20px;
 }
 
 .updateControls {
   -moz-margin-start: 10px;
--- a/browser/themes/shared/incontentprefs/preferences.inc.css
+++ b/browser/themes/shared/incontentprefs/preferences.inc.css
@@ -168,26 +168,16 @@ treecol {
 }
 
 /* XXX This style is for bug 740213 and should be removed once that
    bug has a solution. */
 description > html|a {
   cursor: pointer;
 }
 
-#offlineAppsList,
-#syncEnginesList {
-  -moz-appearance: none;
-  color: #333333;
-  padding: 10px;
-  border: 1px solid #C1C1C1;
-  border-radius: 2px;
-  background-color: #FBFBFB;
-}
-
 #noFxaAccount {
   /* Overriding the margins from the base preferences.css theme file.
      These overrides can be simplified by fixing bug 1027174 */
   margin: 0;
 }
 
 #weavePrefsDeck > vbox > label,
 #weavePrefsDeck > vbox > groupbox,
@@ -288,8 +278,25 @@ description > html|a {
   /* Default dialog dimensions */
   height: 20em;
   width: 66ch;
 }
 
 /**
  * End Dialog
  */
+
+/**
+ * Sync migration
+ */
+#sync-migration {
+  border: 1px solid rgba(0, 0, 0, 0.32);
+  background-color: InfoBackground;
+  color: InfoText;
+  text-shadow: none;
+  margin: 5px 0 0 0;
+  animation: fadein 3000ms;
+}
+
+@keyframes fadein {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
--- a/browser/themes/windows/preferences/preferences.css
+++ b/browser/themes/windows/preferences/preferences.css
@@ -78,16 +78,28 @@ label.small {
 #BrowserPreferences[animated="true"] #handlersView {
   height: 25em;
 }
 
 #BrowserPreferences[animated="false"] #handlersView {
   -moz-box-flex: 1;
 }
 
+/* Privacy Pane */
+
+/* styles for the link elements copied from .text-link in global.css */
+.inline-link {
+  color: -moz-nativehyperlinktext;
+  text-decoration: none;
+}
+
+.inline-link:hover {
+  text-decoration: underline;
+}
+
 /* Modeless Window Dialogs */
 .windowDialog,
 .windowDialog prefpane {
   padding: 0;
 }
 
 .contentPane {
   margin: 9px 8px 5px;
--- a/docshell/test/browser/browser_timelineMarkers-02.js
+++ b/docshell/test/browser/browser_timelineMarkers-02.js
@@ -11,16 +11,17 @@ let URL = '<!DOCTYPE html><style>' +
           'div {width:100px;height:100px;background:red;} ' +
           '.resize-change-color {width:50px;height:50px;background:blue;} ' +
           '.change-color {width:50px;height:50px;background:yellow;} ' +
           '.add-class {}' +
           '</style><div></div>';
 
 let TESTS = [{
   desc: "Changing the width of the test element",
+  searchFor: "Paint",
   setup: function(div) {
     div.setAttribute("class", "resize-change-color");
   },
   check: function(markers) {
     ok(markers.length > 0, "markers were returned");
     console.log(markers);
     info(JSON.stringify(markers.filter(m => m.name == "Paint")));
     ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
@@ -30,16 +31,17 @@ let TESTS = [{
       ok(marker.rectangles.length >= 1, "marker has one rectangle");
       // One of the rectangles should contain the div.
       ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 100, 100)));
     }
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "Changing the test element's background color",
+  searchFor: "Paint",
   setup: function(div) {
     div.setAttribute("class", "change-color");
   },
   check: function(markers) {
     ok(markers.length > 0, "markers were returned");
     ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
     ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
     for (let marker of markers.filter(m => m.name == "Paint")) {
@@ -47,27 +49,29 @@ let TESTS = [{
       ok(marker.rectangles.length >= 1, "marker has one rectangle");
       // One of the rectangles should contain the div.
       ok(marker.rectangles.some(r => rectangleContains(r, 0, 0, 50, 50)));
     }
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "Changing the test element's classname",
+  searchFor: "Paint",
   setup: function(div) {
     div.setAttribute("class", "change-color add-class");
   },
   check: function(markers) {
     ok(markers.length > 0, "markers were returned");
     ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
     ok(!markers.some(m => m.name == "Paint"), "markers doesn't include Paint");
     ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
   }
 }, {
   desc: "sync console.time/timeEnd",
+  searchFor: "ConsoleTime",
   setup: function(div, docShell) {
     content.console.time("FOOBAR");
     content.console.timeEnd("FOOBAR");
     let markers = docShell.popProfileTimelineMarkers();
     is(markers.length, 1, "Got one marker");
     is(markers[0].name, "ConsoleTime", "Got ConsoleTime marker");
     is(markers[0].causeName, "FOOBAR", "Got ConsoleTime FOOBAR detail");
     content.console.time("FOO");
@@ -97,25 +101,25 @@ let test = Task.async(function*() {
                         .getInterface(Ci.nsIWebNavigation)
                         .QueryInterface(Ci.nsIDocShell);
 
   let div = content.document.querySelector("div");
 
   info("Start recording");
   docShell.recordProfileTimelineMarkers = true;
 
-  for (let {desc, setup, check} of TESTS) {
+  for (let {desc, searchFor, setup, check} of TESTS) {
 
     info("Running test: " + desc);
 
     info("Flushing the previous markers if any");
     docShell.popProfileTimelineMarkers();
 
     info("Running the test setup function");
-    let onMarkers = waitForMarkers(docShell);
+    let onMarkers = waitForMarkers(docShell, searchFor);
     setup(div, docShell);
     info("Waiting for new markers on the docShell");
     let markers = yield onMarkers;
 
     info("Running the test check function");
     check(markers);
   }
 
@@ -135,31 +139,30 @@ function openUrl(url) {
 
     linkedBrowser.addEventListener("load", function onload() {
       linkedBrowser.removeEventListener("load", onload, true);
       resolve(tab);
     }, true);
   });
 }
 
-function waitForMarkers(docshell) {
+function waitForMarkers(docshell, searchFor) {
   return new Promise(function(resolve, reject) {
     let waitIterationCount = 0;
     let maxWaitIterationCount = 10; // Wait for 2sec maximum
+    let markers = [];
 
     let interval = setInterval(() => {
-      let markers = docshell.popProfileTimelineMarkers();
-      if (markers.length > 0) {
+      let newMarkers = docshell.popProfileTimelineMarkers();
+      markers = [...markers, ...newMarkers];
+      if (newMarkers.some(m => m.name == searchFor) ||
+          waitIterationCount > maxWaitIterationCount) {
         clearInterval(interval);
         resolve(markers);
       }
-      if (waitIterationCount > maxWaitIterationCount) {
-        clearInterval(interval);
-        resolve([]);
-      }
       waitIterationCount++;
     }, 200);
   });
 }
 
 function rectangleContains(rect, x, y, width, height) {
   return rect.x <= x && rect.y <= y && rect.width >= width &&
     rect.height >= height;
--- a/mobile/android/base/resources/layout/home_banner_content.xml
+++ b/mobile/android/base/resources/layout/home_banner_content.xml
@@ -2,18 +2,18 @@
 <!-- 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/. -->
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
      <ImageView android:id="@+id/icon"
-                android:layout_width="48dip"
-                android:layout_height="48dip"
+                android:layout_width="@dimen/home_banner_icon_width"
+                android:layout_height="@dimen/home_banner_icon_height"
                 android:layout_marginLeft="10dp"
                 android:scaleType="centerInside"/>
 
      <org.mozilla.gecko.widget.EllipsisTextView
          android:id="@+id/text"
          android:layout_width="0dip"
          android:layout_height="match_parent"
          android:layout_weight="1"
@@ -21,16 +21,16 @@
          android:paddingTop="7dp"
          android:paddingBottom="7dp"
          android:textAppearance="@style/TextAppearance.Widget.HomeBanner"
          android:layout_gravity="bottom"
          android:gravity="center_vertical"
          gecko:ellipsizeAtLine="3"/>
 
     <ImageButton android:id="@+id/close"
-                 android:layout_width="34dip"
+                 android:layout_width="@dimen/home_banner_close_width"
                  android:layout_height="match_parent"
                  android:background="@drawable/home_banner"
                  android:scaleType="center"
                  android:contentDescription="@string/close_tab"
                  android:src="@drawable/tab_close"/>
 
 </merge>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -143,16 +143,19 @@
 
     <dimen name="new_tablet_tab_highlight_stroke_width">5dp</dimen>
 
     <!-- PageActionButtons dimensions -->
     <dimen name="page_action_button_width">32dp</dimen>
 
     <!-- Banner -->
     <dimen name="home_banner_height">72dp</dimen>
+    <dimen name="home_banner_close_width">42dp</dimen>
+    <dimen name="home_banner_icon_height">48dip</dimen>
+    <dimen name="home_banner_icon_width">48dip</dimen>
 
     <!-- Icon Grid -->
     <dimen name="icongrid_columnwidth">128dp</dimen>
     <dimen name="icongrid_padding">16dp</dimen>
 
     <!-- PanelGridView dimensions -->
     <dimen name="panel_grid_view_column_width">150dp</dimen>
 
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -760,20 +760,21 @@ class GTestCommands(MachCommandBase):
 
         import mozdebug
 
         if not debugger:
             # No debugger name was provided. Look for the default ones on
             # current OS.
             debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
 
-        debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
-        if not debuggerInfo:
-            print("Could not find a suitable debugger in your PATH.")
-            return 1
+        if debugger:
+            debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
+            if not debuggerInfo:
+                print("Could not find a suitable debugger in your PATH.")
+                return 1
 
         # Parameters come from the CLI. We need to convert them before
         # their use.
         if debugger_args:
             import pymake.process
             argv, badchar = pymake.process.clinetoargv(debugger_args, os.getcwd())
             if badchar:
                 print("The --debugger_args you passed require a real shell to parse them.")
@@ -937,20 +938,21 @@ class RunProgram(MachCommandBase):
 
         if debug:
             import mozdebug
             if not debugger:
                 # No debugger name was provided. Look for the default ones on
                 # current OS.
                 debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
 
-            self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams)
-            if not self.debuggerInfo:
-                print("Could not find a suitable debugger in your PATH.")
-                return 1
+            if debugger:
+                self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams)
+                if not self.debuggerInfo:
+                    print("Could not find a suitable debugger in your PATH.")
+                    return 1
 
             # Parameters come from the CLI. We need to convert them before
             # their use.
             if debugparams:
                 import pymake.process
                 argv, badchar = pymake.process.clinetoargv(debugparams, os.getcwd())
                 if badchar:
                     print("The --debugparams you passed require a real shell to parse them.")
--- a/services/sync/modules/FxaMigrator.jsm
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -345,22 +345,45 @@ Migrator.prototype = {
     // observer notification.
   }),
 
   // Ask the FxA servers to re-send a verification mail for the currently
   // logged in user. This should only be called while we are in the
   // STATE_USER_FXA_VERIFIED state.  When the user clicks on the link in
   // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
   // to complete the migration.
-  resendVerificationMail: Task.async(function * () {
+  resendVerificationMail: Task.async(function * (win) {
     // warn if we aren't in the expected state - but go ahead anyway!
     if (this._state != this.STATE_USER_FXA_VERIFIED) {
       this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
     }
-    return fxAccounts.resendVerificationEmail();
+    let ok = true;
+    try {
+      yield fxAccounts.resendVerificationEmail();
+    } catch (ex) {
+      this.log.error("Failed to resend verification mail: ${}", ex);
+      ok = false;
+    }
+    let fxauser = yield fxAccounts.getSignedInUser();
+    let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
+
+    let heading = ok ?
+                  sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
+                  sb.GetStringFromName("verificationNotSentHeading");
+    let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
+    let description = sb.GetStringFromName(ok ? "verificationSentDescription"
+                                              : "verificationNotSentDescription");
+
+    let factory = Cc["@mozilla.org/prompter;1"]
+                    .getService(Ci.nsIPromptFactory);
+    let prompt = factory.getPrompt(win, Ci.nsIPrompt);
+    let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
+    bag.setPropertyAsBool("allowTabModal", true);
+
+    prompt.alert(title, heading + "\n\n" + description);
   }),
 
   // "forget" about the current Firefox account. This should only be called
   // while we are in the STATE_USER_FXA_VERIFIED state.  After this we will
   // see an ONLOGOUT_NOTIFICATION, which will cause the migrator to return back
   // to the STATE_USER_FXA state, from where they can choose a different account.
   forgetFxAccount: Task.async(function * () {
     // warn if we aren't in the expected state - but go ahead anyway!
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -1796,19 +1796,21 @@ class Mochitest(MochitestUtilsMixin):
 
     # get debugger info, a dict of:
     # {'path': path to the debugger (string),
     #  'interactive': whether the debugger is interactive or not (bool)
     #  'args': arguments to the debugger (list)
     # TODO: use mozrunner.local.debugger_arguments:
     # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
 
-    debuggerInfo = mozdebug.get_debugger_info(options.debugger,
-                                              options.debuggerArgs,
-                                              options.debuggerInteractive)
+    debuggerInfo = None
+    if options.debugger:
+        debuggerInfo = mozdebug.get_debugger_info(options.debugger,
+                                                  options.debuggerArgs,
+                                                  options.debuggerInteractive)
 
     if options.useTestMediaDevices:
       devices = findTestMediaDevices(self.log)
       if not devices:
         self.log.error("Could not find test media devices to use")
         return 1
       self.mediaDevices = devices
 
--- a/toolkit/components/places/tests/unit/test_bookmarks_html.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -130,25 +130,27 @@ add_task(function* test_emptytitle_expor
   // 4. empty bookmarks db
   // 5. import bookmarks.exported.html
   // 6. run the test-suite
   // 7. remove the empty-titled bookmark
   // 8. export to bookmarks.exported.html
   // 9. empty bookmarks db and continue
 
   yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  yield promiseAsyncUpdates();
 
   const NOTITLE_URL = "http://notitle.mozilla.org/";
   let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                 NetUtil.newURI(NOTITLE_URL),
                                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                 "");
   test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
 
   yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+  yield promiseAsyncUpdates();
   remove_all_bookmarks();
 
   yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
   yield promiseAsyncUpdates();
   yield testImportedBookmarks();
 
   // Cleanup.
   test_bookmarks.unfiled.pop();
@@ -172,16 +174,17 @@ add_task(function* test_import_chromefav
   // 8. export to bookmarks.exported.html
   // 9. empty bookmarks db and continue
 
   const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page");
   const CHROME_FAVICON_URI = NetUtil.newURI("chrome://global/skin/icons/information-16.png");
   const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
 
   yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  yield promiseAsyncUpdates();
   let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                                 PAGE_URI,
                                                 PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                 "Test");
 
   let deferred = Promise.defer();
   PlacesUtils.favicons.setAndFetchFaviconForPage(
                                   PAGE_URI, CHROME_FAVICON_URI, true,
@@ -196,16 +199,17 @@ add_task(function* test_import_chromefav
 
   let base64Icon = "data:image/png;base64," +
       base64EncodeString(String.fromCharCode.apply(String, data));
 
   test_bookmarks.unfiled.push(
     { title: "Test", url: PAGE_URI.spec, icon: base64Icon });
 
   yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+  yield promiseAsyncUpdates();
 
   // Change the favicon to check it's really imported again later.
   deferred = Promise.defer();
   PlacesUtils.favicons.setAndFetchFaviconForPage(
                                   PAGE_URI, CHROME_FAVICON_URI_2, true,
                                   PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
                                   deferred.resolve);
   yield deferred.promise;
@@ -231,17 +235,20 @@ add_task(function* test_import_ontop()
   // bookmarks.
   // 1. empty bookmarks db
   // 2. import the exported bookmarks file
   // 3. export to file
   // 3. import the exported bookmarks file
   // 4. run the test-suite
 
   yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+  yield promiseAsyncUpdates();
   yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+  yield promiseAsyncUpdates();
+
   yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
   yield promiseAsyncUpdates();
   yield testImportedBookmarks();
   yield promiseAsyncUpdates();
   remove_all_bookmarks();
 });
 
 function* testImportedBookmarks()
deleted file mode 100644
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5917,16 +5917,31 @@
     "kind": "boolean",
     "description": "How many times has the devtool's Scratchpad been opened via the toolbox button?"
   },
   "DEVTOOLS_RESPONSIVE_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the devtool's Responsive View been opened via the toolbox button?"
   },
+  "DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the devtool's Eyedropper tool been opened?"
+  },
+  "DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the devtool's eyedropper been opened via the devtools menu?"
+  },
+  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "How many times has the devtool's eyedropper been opened via the color picker?"
+  },
   "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "How many times has the devtool's Developer Toolbar been opened via the toolbox button?"
   },
   "DEVTOOLS_WEBIDE_OPENED_BOOLEAN": {
     "expires_in_version": "never",
     "kind": "boolean",
@@ -6042,16 +6057,31 @@
     "kind": "flag",
     "description": "How many users have opened the devtool's Scratchpad been opened via the toolbox button?"
   },
   "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the devtool's Responsive View been opened via the toolbox button?"
   },
+  "DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Eyedropper via the toolbox button?"
+  },
+  "DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Eyedropper via the devtools menu?"
+  },
+  "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": {
+    "expires_in_version": "never",
+    "kind": "flag",
+    "description": "How many users have opened the devtool's Eyedropper via the color picker?"
+  },
   "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "How many users have opened the devtool's Developer Toolbar been opened via the toolbox button?"
   },
   "DEVTOOLS_WEBIDE_OPENED_PER_USER_FLAG": {
     "expires_in_version": "never",
     "kind": "flag",
--- a/toolkit/devtools/server/tests/browser/browser_timeline.js
+++ b/toolkit/devtools/server/tests/browser/browser_timeline.js
@@ -1,55 +1,61 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that the timeline front's start/stop/isRecording methods work in a
 // simple use case, and that markers events are sent when operations occur.
+// Note that this test isn't concerned with which markers are actually recorded,
+// just that markers are recorded at all.
+// Trying to check marker types here may lead to intermittents, see bug 1066474.
 
 const {TimelineFront} = require("devtools/server/actors/timeline");
 
 add_task(function*() {
   let doc = yield addTab("data:text/html;charset=utf-8,mop");
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
-
   let form = yield connectDebuggerClient(client);
   let front = TimelineFront(client, form);
 
+  ok(front, "The TimelineFront was created");
+
   let isActive = yield front.isRecording();
-  ok(!isActive, "Not initially recording");
+  ok(!isActive, "The TimelineFront is not initially recording");
 
-  doc.body.innerHeight; // flush any pending reflow
+  info("Flush any pending reflows");
+  let forceSyncReflow = doc.body.innerHeight;
 
+  info("Start recording");
   yield front.start();
 
   isActive = yield front.isRecording();
-  ok(isActive, "Recording after start()");
+  ok(isActive, "The TimelineFront is now recording");
 
+  info("Change some style on the page to cause style/reflow/paint");
+  let onMarkers = once(front, "markers");
   doc.body.style.padding = "10px";
+  let markers = yield onMarkers;
 
-  let markers = yield once(front, "markers");
+  ok(true, "The markers event was fired");
+  ok(markers.length > 0, "Markers were returned");
+
+  info("Flush pending reflows again");
+  forceSyncReflow = doc.body.innerHeight;
+
+  info("Change some style on the page to cause style/paint");
+  onMarkers = once(front, "markers");
+  doc.body.style.backgroundColor = "red";
+  markers = yield onMarkers;
 
   ok(markers.length > 0, "markers were returned");
-  ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
-  ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
-  ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
-
-  doc.body.style.backgroundColor = "red";
-
-  markers = yield once(front, "markers");
-
-  ok(markers.length > 0, "markers were returned");
-  ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
-  ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
-  ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
 
   yield front.stop();
 
   isActive = yield front.isRecording();
   ok(!isActive, "Not recording after stop()");
 
   yield closeDebuggerClient(client);
   gBrowser.removeCurrentTab();
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -78,16 +78,21 @@ define('source-map/source-map-consumer',
     var file = util.getArg(sourceMap, 'file', null);
 
     // Once again, Sass deviates from the spec and supplies the version as a
     // string rather than a number, so we use loose equality checking here.
     if (version != this._version) {
       throw new Error('Unsupported version: ' + version);
     }
 
+    // Some source maps produce relative source paths like "./foo.js" instead of
+    // "foo.js".  Normalize these first so that future comparisons will succeed.
+    // See bugzil.la/1090768.
+    sources = sources.map(util.normalize);
+
     // Pass `true` below to allow duplicate names and sources. While source maps
     // are intended to be compressed and deduplicated, the TypeScript compiler
     // sometimes generates source maps with duplicates in them. See Github issue
     // #72 and bugzil.la/889492.
     this._names = ArraySet.fromArray(names, true);
     this._sources = ArraySet.fromArray(sources, true);
 
     this.sourceRoot = sourceRoot;
@@ -303,16 +308,43 @@ define('source-map/source-map-consumer',
         throw new TypeError('Column must be greater than or equal to 0, got '
                             + aNeedle[aColumnName]);
       }
 
       return binarySearch.search(aNeedle, aMappings, aComparator);
     };
 
   /**
+   * Compute the last column for each generated mapping. The last column is
+   * inclusive.
+   */
+  SourceMapConsumer.prototype.computeColumnSpans =
+    function SourceMapConsumer_computeColumnSpans() {
+      for (var index = 0; index < this._generatedMappings.length; ++index) {
+        var mapping = this._generatedMappings[index];
+
+        // Mappings do not contain a field for the last generated columnt. We
+        // can come up with an optimistic estimate, however, by assuming that
+        // mappings are contiguous (i.e. given two consecutive mappings, the
+        // first mapping ends where the second one starts).
+        if (index + 1 < this._generatedMappings.length) {
+          var nextMapping = this._generatedMappings[index + 1];
+
+          if (mapping.generatedLine === nextMapping.generatedLine) {
+            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+            continue;
+          }
+        }
+
+        // The last mapping for each line spans the entire line.
+        mapping.lastGeneratedColumn = Infinity;
+      }
+    };
+
+  /**
    * Returns the original source, line, and column information for the generated
    * source's line and column positions provided. The only argument is an object
    * with the following properties:
    *
    *   - line: The line number in the generated source.
    *   - column: The column number in the generated source.
    *
    * and an object is returned with the following properties:
@@ -324,33 +356,37 @@ define('source-map/source-map-consumer',
    */
   SourceMapConsumer.prototype.originalPositionFor =
     function SourceMapConsumer_originalPositionFor(aArgs) {
       var needle = {
         generatedLine: util.getArg(aArgs, 'line'),
         generatedColumn: util.getArg(aArgs, 'column')
       };
 
-      var mapping = this._findMapping(needle,
-                                      this._generatedMappings,
-                                      "generatedLine",
-                                      "generatedColumn",
-                                      util.compareByGeneratedPositions);
+      var index = this._findMapping(needle,
+                                    this._generatedMappings,
+                                    "generatedLine",
+                                    "generatedColumn",
+                                    util.compareByGeneratedPositions);
+
+      if (index >= 0) {
+        var mapping = this._generatedMappings[index];
 
-      if (mapping && mapping.generatedLine === needle.generatedLine) {
-        var source = util.getArg(mapping, 'source', null);
-        if (source != null && this.sourceRoot != null) {
-          source = util.join(this.sourceRoot, source);
+        if (mapping.generatedLine === needle.generatedLine) {
+          var source = util.getArg(mapping, 'source', null);
+          if (source != null && this.sourceRoot != null) {
+            source = util.join(this.sourceRoot, source);
+          }
+          return {
+            source: source,
+            line: util.getArg(mapping, 'originalLine', null),
+            column: util.getArg(mapping, 'originalColumn', null),
+            name: util.getArg(mapping, 'name', null)
+          };
         }
-        return {
-          source: source,
-          line: util.getArg(mapping, 'originalLine', null),
-          column: util.getArg(mapping, 'originalColumn', null),
-          name: util.getArg(mapping, 'name', null)
-        };
       }
 
       return {
         source: null,
         line: null,
         column: null,
         name: null
       };
@@ -418,35 +454,92 @@ define('source-map/source-map-consumer',
         originalLine: util.getArg(aArgs, 'line'),
         originalColumn: util.getArg(aArgs, 'column')
       };
 
       if (this.sourceRoot != null) {
         needle.source = util.relative(this.sourceRoot, needle.source);
       }
 
-      var mapping = this._findMapping(needle,
-                                      this._originalMappings,
-                                      "originalLine",
-                                      "originalColumn",
-                                      util.compareByOriginalPositions);
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions);
 
-      if (mapping) {
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
         return {
           line: util.getArg(mapping, 'generatedLine', null),
-          column: util.getArg(mapping, 'generatedColumn', null)
+          column: util.getArg(mapping, 'generatedColumn', null),
+          lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
         };
       }
 
       return {
         line: null,
-        column: null
+        column: null,
+        lastColumn: null
       };
     };
 
+  /**
+   * Returns all generated line and column information for the original source
+   * and line provided. The only argument is an object with the following
+   * properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *
+   * and an array of objects is returned, each with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  SourceMapConsumer.prototype.allGeneratedPositionsFor =
+    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+      // When there is no exact match, SourceMapConsumer.prototype._findMapping
+      // returns the index of the closest mapping less than the needle. By
+      // setting needle.originalColumn to Infinity, we thus find the last
+      // mapping for the given line, provided such a mapping exists.
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: Infinity
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var mappings = [];
+
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions);
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
+        while (mapping && mapping.originalLine === needle.originalLine) {
+          mappings.push({
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          });
+
+          mapping = this._originalMappings[--index];
+        }
+      }
+
+      return mappings.reverse();
+    };
+
   SourceMapConsumer.GENERATED_ORDER = 1;
   SourceMapConsumer.ORIGINAL_ORDER = 2;
 
   /**
    * Iterate over each mapping between an original source/line/column and a
    * generated line/column in this source map.
    *
    * @param Function aCallback
@@ -831,69 +924,68 @@ define('source-map/binary-search', ['req
    * @param aHaystack The non-empty array being searched.
    * @param aCompare Function which takes two elements and returns -1, 0, or 1.
    */
   function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) {
     // This function terminates when one of the following is true:
     //
     //   1. We find the exact element we are looking for.
     //
-    //   2. We did not find the exact element, but we can return the next
-    //      closest element that is less than that element.
+    //   2. We did not find the exact element, but we can return the index of
+    //      the next closest element that is less than that element.
     //
     //   3. We did not find the exact element, and there is no next-closest
     //      element which is less than the one we are searching for, so we
-    //      return null.
+    //      return -1.
     var mid = Math.floor((aHigh - aLow) / 2) + aLow;
     var cmp = aCompare(aNeedle, aHaystack[mid], true);
     if (cmp === 0) {
       // Found the element we are looking for.
-      return aHaystack[mid];
+      return mid;
     }
     else if (cmp > 0) {
       // aHaystack[mid] is greater than our needle.
       if (aHigh - mid > 1) {
         // The element is in the upper half.
         return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare);
       }
       // We did not find an exact match, return the next closest one
       // (termination case 2).
-      return aHaystack[mid];
+      return mid;
     }
     else {
       // aHaystack[mid] is less than our needle.
       if (mid - aLow > 1) {
         // The element is in the lower half.
         return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare);
       }
       // The exact needle element was not found in this haystack. Determine if
       // we are in termination case (2) or (3) and return the appropriate thing.
-      return aLow < 0
-        ? null
-        : aHaystack[aLow];
+      return aLow < 0 ? -1 : aLow;
     }
   }
 
   /**
    * This is an implementation of binary search which will always try and return
-   * the next lowest value checked if there is no exact hit. This is because
-   * mappings between original and generated line/col pairs are single points,
-   * and there is an implicit region between each of them, so a miss just means
-   * that you aren't on the very start of a region.
+   * the index of next lowest value checked if there is no exact hit. This is
+   * because mappings between original and generated line/col pairs are single
+   * points, and there is an implicit region between each of them, so a miss
+   * just means that you aren't on the very start of a region.
    *
    * @param aNeedle The element you are looking for.
    * @param aHaystack The array that is being searched.
    * @param aCompare A function which takes the needle and an element in the
    *     array and returns -1, 0, or 1 depending on whether the needle is less
    *     than, equal to, or greater than the element, respectively.
    */
   exports.search = function search(aNeedle, aHaystack, aCompare) {
-    return aHaystack.length > 0
-      ? recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
-      : null;
+    if (aHaystack.length === 0) {
+      return -1;
+    }
+    return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
--- a/toolkit/devtools/sourcemap/source-map.js
+++ b/toolkit/devtools/sourcemap/source-map.js
@@ -1,9 +1,9 @@
-/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 
 /**
  * Define a module along with a payload.
@@ -159,24 +159,27 @@ var require = define.globalDomain.requir
 define('source-map/source-map-generator', ['require', 'exports', 'module' ,  'source-map/base64-vlq', 'source-map/util', 'source-map/array-set'], function(require, exports, module) {
 
   var base64VLQ = require('./base64-vlq');
   var util = require('./util');
   var ArraySet = require('./array-set').ArraySet;
 
   /**
    * An instance of the SourceMapGenerator represents a source map which is
-   * being built incrementally. To create a new one, you must pass an object
-   * with the following properties:
+   * being built incrementally. You may pass an object with the following
+   * properties:
    *
    *   - file: The filename of the generated source.
-   *   - sourceRoot: An optional root for all URLs in this source map.
+   *   - sourceRoot: A root for all relative URLs in this source map.
    */
   function SourceMapGenerator(aArgs) {
-    this._file = util.getArg(aArgs, 'file');
+    if (!aArgs) {
+      aArgs = {};
+    }
+    this._file = util.getArg(aArgs, 'file', null);
     this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);
     this._sources = new ArraySet();
     this._names = new ArraySet();
     this._mappings = [];
     this._sourcesContents = null;
   }
 
   SourceMapGenerator.prototype._version = 3;
@@ -196,37 +199,37 @@ define('source-map/source-map-generator'
       aSourceMapConsumer.eachMapping(function (mapping) {
         var newMapping = {
           generated: {
             line: mapping.generatedLine,
             column: mapping.generatedColumn
           }
         };
 
-        if (mapping.source) {
+        if (mapping.source != null) {
           newMapping.source = mapping.source;
-          if (sourceRoot) {
+          if (sourceRoot != null) {
             newMapping.source = util.relative(sourceRoot, newMapping.source);
           }
 
           newMapping.original = {
             line: mapping.originalLine,
             column: mapping.originalColumn
           };
 
-          if (mapping.name) {
+          if (mapping.name != null) {
             newMapping.name = mapping.name;
           }
         }
 
         generator.addMapping(newMapping);
       });
       aSourceMapConsumer.sources.forEach(function (sourceFile) {
         var content = aSourceMapConsumer.sourceContentFor(sourceFile);
-        if (content) {
+        if (content != null) {
           generator.setSourceContent(sourceFile, content);
         }
       });
       return generator;
     };
 
   /**
    * Add a single mapping from original source line and column to the generated
@@ -242,21 +245,21 @@ define('source-map/source-map-generator'
     function SourceMapGenerator_addMapping(aArgs) {
       var generated = util.getArg(aArgs, 'generated');
       var original = util.getArg(aArgs, 'original', null);
       var source = util.getArg(aArgs, 'source', null);
       var name = util.getArg(aArgs, 'name', null);
 
       this._validateMapping(generated, original, source, name);
 
-      if (source && !this._sources.has(source)) {
+      if (source != null && !this._sources.has(source)) {
         this._sources.add(source);
       }
 
-      if (name && !this._names.has(name)) {
+      if (name != null && !this._names.has(name)) {
         this._names.add(name);
       }
 
       this._mappings.push({
         generatedLine: generated.line,
         generatedColumn: generated.column,
         originalLine: original != null && original.line,
         originalColumn: original != null && original.column,
@@ -266,28 +269,28 @@ define('source-map/source-map-generator'
     };
 
   /**
    * Set the source content for a source file.
    */
   SourceMapGenerator.prototype.setSourceContent =
     function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {
       var source = aSourceFile;
-      if (this._sourceRoot) {
+      if (this._sourceRoot != null) {
         source = util.relative(this._sourceRoot, source);
       }
 
-      if (aSourceContent !== null) {
+      if (aSourceContent != null) {
         // Add the source content to the _sourcesContents map.
         // Create a new _sourcesContents map if the property is null.
         if (!this._sourcesContents) {
           this._sourcesContents = {};
         }
         this._sourcesContents[util.toSetString(source)] = aSourceContent;
-      } else {
+      } else if (this._sourcesContents) {
         // Remove the source file from the _sourcesContents map.
         // If the _sourcesContents map is empty, set the property to null.
         delete this._sourcesContents[util.toSetString(source)];
         if (Object.keys(this._sourcesContents).length === 0) {
           this._sourcesContents = null;
         }
       }
     };
@@ -296,77 +299,93 @@ define('source-map/source-map-generator'
    * Applies the mappings of a sub-source-map for a specific source file to the
    * source map being generated. Each mapping to the supplied source file is
    * rewritten using the supplied source map. Note: The resolution for the
    * resulting mappings is the minimium of this map and the supplied map.
    *
    * @param aSourceMapConsumer The source map to be applied.
    * @param aSourceFile Optional. The filename of the source file.
    *        If omitted, SourceMapConsumer's file property will be used.
+   * @param aSourceMapPath Optional. The dirname of the path to the source map
+   *        to be applied. If relative, it is relative to the SourceMapConsumer.
+   *        This parameter is needed when the two source maps aren't in the same
+   *        directory, and the source map to be applied contains relative source
+   *        paths. If so, those relative source paths need to be rewritten
+   *        relative to the SourceMapGenerator.
    */
   SourceMapGenerator.prototype.applySourceMap =
-    function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) {
+    function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {
+      var sourceFile = aSourceFile;
       // If aSourceFile is omitted, we will use the file property of the SourceMap
-      if (!aSourceFile) {
-        aSourceFile = aSourceMapConsumer.file;
+      if (aSourceFile == null) {
+        if (aSourceMapConsumer.file == null) {
+          throw new Error(
+            'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +
+            'or the source map\'s "file" property. Both were omitted.'
+          );
+        }
+        sourceFile = aSourceMapConsumer.file;
       }
       var sourceRoot = this._sourceRoot;
-      // Make "aSourceFile" relative if an absolute Url is passed.
-      if (sourceRoot) {
-        aSourceFile = util.relative(sourceRoot, aSourceFile);
+      // Make "sourceFile" relative if an absolute Url is passed.
+      if (sourceRoot != null) {
+        sourceFile = util.relative(sourceRoot, sourceFile);
       }
       // Applying the SourceMap can add and remove items from the sources and
       // the names array.
       var newSources = new ArraySet();
       var newNames = new ArraySet();
 
-      // Find mappings for the "aSourceFile"
+      // Find mappings for the "sourceFile"
       this._mappings.forEach(function (mapping) {
-        if (mapping.source === aSourceFile && mapping.originalLine) {
+        if (mapping.source === sourceFile && mapping.originalLine != null) {
           // Check if it can be mapped by the source map, then update the mapping.
           var original = aSourceMapConsumer.originalPositionFor({
             line: mapping.originalLine,
             column: mapping.originalColumn
           });
-          if (original.source !== null) {
+          if (original.source != null) {
             // Copy mapping
-            if (sourceRoot) {
-              mapping.source = util.relative(sourceRoot, original.source);
-            } else {
-              mapping.source = original.source;
+            mapping.source = original.source;
+            if (aSourceMapPath != null) {
+              mapping.source = util.join(aSourceMapPath, mapping.source)
+            }
+            if (sourceRoot != null) {
+              mapping.source = util.relative(sourceRoot, mapping.source);
             }
             mapping.originalLine = original.line;
             mapping.originalColumn = original.column;
-            if (original.name !== null && mapping.name !== null) {
-              // Only use the identifier name if it's an identifier
-              // in both SourceMaps
+            if (original.name != null) {
               mapping.name = original.name;
             }
           }
         }
 
         var source = mapping.source;
-        if (source && !newSources.has(source)) {
+        if (source != null && !newSources.has(source)) {
           newSources.add(source);
         }
 
         var name = mapping.name;
-        if (name && !newNames.has(name)) {
+        if (name != null && !newNames.has(name)) {
           newNames.add(name);
         }
 
       }, this);
       this._sources = newSources;
       this._names = newNames;
 
       // Copy sourcesContents of applied map.
       aSourceMapConsumer.sources.forEach(function (sourceFile) {
         var content = aSourceMapConsumer.sourceContentFor(sourceFile);
-        if (content) {
-          if (sourceRoot) {
+        if (content != null) {
+          if (aSourceMapPath != null) {
+            sourceFile = util.join(aSourceMapPath, sourceFile);
+          }
+          if (sourceRoot != null) {
             sourceFile = util.relative(sourceRoot, sourceFile);
           }
           this.setSourceContent(sourceFile, content);
         }
       }, this);
     };
 
   /**
@@ -396,17 +415,17 @@ define('source-map/source-map-generator'
                && aSource) {
         // Cases 2 and 3.
         return;
       }
       else {
         throw new Error('Invalid mapping: ' + JSON.stringify({
           generated: aGenerated,
           source: aSource,
-          orginal: aOriginal,
+          original: aOriginal,
           name: aName
         }));
       }
     };
 
   /**
    * Serialize the accumulated mappings in to the stream of base 64 VLQs
    * specified by the source map format.
@@ -447,48 +466,48 @@ define('source-map/source-map-generator'
             result += ',';
           }
         }
 
         result += base64VLQ.encode(mapping.generatedColumn
                                    - previousGeneratedColumn);
         previousGeneratedColumn = mapping.generatedColumn;
 
-        if (mapping.source) {
+        if (mapping.source != null) {
           result += base64VLQ.encode(this._sources.indexOf(mapping.source)
                                      - previousSource);
           previousSource = this._sources.indexOf(mapping.source);
 
           // lines are stored 0-based in SourceMap spec version 3
           result += base64VLQ.encode(mapping.originalLine - 1
                                      - previousOriginalLine);
           previousOriginalLine = mapping.originalLine - 1;
 
           result += base64VLQ.encode(mapping.originalColumn
                                      - previousOriginalColumn);
           previousOriginalColumn = mapping.originalColumn;
 
-          if (mapping.name) {
+          if (mapping.name != null) {
             result += base64VLQ.encode(this._names.indexOf(mapping.name)
                                        - previousName);
             previousName = this._names.indexOf(mapping.name);
           }
         }
       }
 
       return result;
     };
 
   SourceMapGenerator.prototype._generateSourcesContent =
     function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {
       return aSources.map(function (source) {
         if (!this._sourcesContents) {
           return null;
         }
-        if (aSourceRoot) {
+        if (aSourceRoot != null) {
           source = util.relative(aSourceRoot, source);
         }
         var key = util.toSetString(source);
         return Object.prototype.hasOwnProperty.call(this._sourcesContents,
                                                     key)
           ? this._sourcesContents[key]
           : null;
       }, this);
@@ -496,22 +515,24 @@ define('source-map/source-map-generator'
 
   /**
    * Externalize the source map.
    */
   SourceMapGenerator.prototype.toJSON =
     function SourceMapGenerator_toJSON() {
       var map = {
         version: this._version,
-        file: this._file,
         sources: this._sources.toArray(),
         names: this._names.toArray(),
         mappings: this._serializeMappings()
       };
-      if (this._sourceRoot) {
+      if (this._file != null) {
+        map.file = this._file;
+      }
+      if (this._sourceRoot != null) {
         map.sourceRoot = this._sourceRoot;
       }
       if (this._sourcesContents) {
         map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);
       }
 
       return map;
     };
@@ -636,19 +657,19 @@ define('source-map/base64-vlq', ['requir
       encoded += base64.encode(digit);
     } while (vlq > 0);
 
     return encoded;
   };
 
   /**
    * Decodes the next base 64 VLQ value from the given string and returns the
-   * value and the rest of the string.
+   * value and the rest of the string via the out parameter.
    */
-  exports.decode = function base64VLQ_decode(aStr) {
+  exports.decode = function base64VLQ_decode(aStr, aOutParam) {
     var i = 0;
     var strLen = aStr.length;
     var result = 0;
     var shift = 0;
     var continuation, digit;
 
     do {
       if (i >= strLen) {
@@ -656,20 +677,18 @@ define('source-map/base64-vlq', ['requir
       }
       digit = base64.decode(aStr.charAt(i++));
       continuation = !!(digit & VLQ_CONTINUATION_BIT);
       digit &= VLQ_BASE_MASK;
       result = result + (digit << shift);
       shift += VLQ_BASE_SHIFT;
     } while (continuation);
 
-    return {
-      value: fromVLQSigned(result),
-      rest: aStr.slice(i)
-    };
+    aOutParam.value = fromVLQSigned(result);
+    aOutParam.rest = aStr.slice(i);
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
@@ -731,69 +750,197 @@ define('source-map/util', ['require', 'e
     } else if (arguments.length === 3) {
       return aDefaultValue;
     } else {
       throw new Error('"' + aName + '" is a required argument.');
     }
   }
   exports.getArg = getArg;
 
-  var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
-  var dataUrlRegexp = /^data:.+\,.+/;
+  var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.]*)(?::(\d+))?(\S*)$/;
+  var dataUrlRegexp = /^data:.+\,.+$/;
 
   function urlParse(aUrl) {
     var match = aUrl.match(urlRegexp);
     if (!match) {
       return null;
     }
     return {
       scheme: match[1],
-      auth: match[3],
-      host: match[4],
-      port: match[6],
-      path: match[7]
+      auth: match[2],
+      host: match[3],
+      port: match[4],
+      path: match[5]
     };
   }
   exports.urlParse = urlParse;
 
   function urlGenerate(aParsedUrl) {
-    var url = aParsedUrl.scheme + "://";
+    var url = '';
+    if (aParsedUrl.scheme) {
+      url += aParsedUrl.scheme + ':';
+    }
+    url += '//';
     if (aParsedUrl.auth) {
-      url += aParsedUrl.auth + "@"
+      url += aParsedUrl.auth + '@';
     }
     if (aParsedUrl.host) {
       url += aParsedUrl.host;
     }
     if (aParsedUrl.port) {
       url += ":" + aParsedUrl.port
     }
     if (aParsedUrl.path) {
       url += aParsedUrl.path;
     }
     return url;
   }
   exports.urlGenerate = urlGenerate;
 
+  /**
+   * Normalizes a path, or the path portion of a URL:
+   *
+   * - Replaces consequtive slashes with one slash.
+   * - Removes unnecessary '.' parts.
+   * - Removes unnecessary '<dir>/..' parts.
+   *
+   * Based on code in the Node.js 'path' core module.
+   *
+   * @param aPath The path or url to normalize.
+   */
+  function normalize(aPath) {
+    var path = aPath;
+    var url = urlParse(aPath);
+    if (url) {
+      if (!url.path) {
+        return aPath;
+      }
+      path = url.path;
+    }
+    var isAbsolute = (path.charAt(0) === '/');
+
+    var parts = path.split(/\/+/);
+    for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
+      part = parts[i];
+      if (part === '.') {
+        parts.splice(i, 1);
+      } else if (part === '..') {
+        up++;
+      } else if (up > 0) {
+        if (part === '') {
+          // The first part is blank if the path is absolute. Trying to go
+          // above the root is a no-op. Therefore we can remove all '..' parts
+          // directly after the root.
+          parts.splice(i + 1, up);
+          up = 0;
+        } else {
+          parts.splice(i, 2);
+          up--;
+        }
+      }
+    }
+    path = parts.join('/');
+
+    if (path === '') {
+      path = isAbsolute ? '/' : '.';
+    }
+
+    if (url) {
+      url.path = path;
+      return urlGenerate(url);
+    }
+    return path;
+  }
+  exports.normalize = normalize;
+
+  /**
+   * Joins two paths/URLs.
+   *
+   * @param aRoot The root path or URL.
+   * @param aPath The path or URL to be joined with the root.
+   *
+   * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a
+   *   scheme-relative URL: Then the scheme of aRoot, if any, is prepended
+   *   first.
+   * - Otherwise aPath is a path. If aRoot is a URL, then its path portion
+   *   is updated with the result and aRoot is returned. Otherwise the result
+   *   is returned.
+   *   - If aPath is absolute, the result is aPath.
+   *   - Otherwise the two paths are joined with a slash.
+   * - Joining for example 'http://' and 'www.example.com' is also supported.
+   */
   function join(aRoot, aPath) {
-    var url;
+    if (aRoot === "") {
+      aRoot = ".";
+    }
+    if (aPath === "") {
+      aPath = ".";
+    }
+    var aPathUrl = urlParse(aPath);
+    var aRootUrl = urlParse(aRoot);
+    if (aRootUrl) {
+      aRoot = aRootUrl.path || '/';
+    }
 
-    if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
+    // `join(foo, '//www.example.org')`
+    if (aPathUrl && !aPathUrl.scheme) {
+      if (aRootUrl) {
+        aPathUrl.scheme = aRootUrl.scheme;
+      }
+      return urlGenerate(aPathUrl);
+    }
+
+    if (aPathUrl || aPath.match(dataUrlRegexp)) {
       return aPath;
     }
 
-    if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
-      url.path = aPath;
-      return urlGenerate(url);
+    // `join('http://', 'www.example.com')`
+    if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {
+      aRootUrl.host = aPath;
+      return urlGenerate(aRootUrl);
     }
 
-    return aRoot.replace(/\/$/, '') + '/' + aPath;
+    var joined = aPath.charAt(0) === '/'
+      ? aPath
+      : normalize(aRoot.replace(/\/+$/, '') + '/' + aPath);
+
+    if (aRootUrl) {
+      aRootUrl.path = joined;
+      return urlGenerate(aRootUrl);
+    }
+    return joined;
   }
   exports.join = join;
 
   /**
+   * Make a path relative to a URL or another path.
+   *
+   * @param aRoot The root path or URL.
+   * @param aPath The path or URL to be made relative to aRoot.
+   */
+  function relative(aRoot, aPath) {
+    if (aRoot === "") {
+      aRoot = ".";
+    }
+
+    aRoot = aRoot.replace(/\/$/, '');
+
+    // XXX: It is possible to remove this block, and the tests still pass!
+    var url = urlParse(aRoot);
+    if (aPath.charAt(0) == "/" && url && url.path == "/") {
+      return aPath.slice(1);
+    }
+
+    return aPath.indexOf(aRoot + '/') === 0
+      ? aPath.substr(aRoot.length + 1)
+      : aPath;
+  }
+  exports.relative = relative;
+
+  /**
    * Because behavior goes wacky when you set `__proto__` on objects, we
    * have to prefix all the strings in our set with an arbitrary character.
    *
    * See https://github.com/mozilla/source-map/pull/31 and
    * https://github.com/mozilla/source-map/issues/30
    *
    * @param String aStr
    */
@@ -802,30 +949,16 @@ define('source-map/util', ['require', 'e
   }
   exports.toSetString = toSetString;
 
   function fromSetString(aStr) {
     return aStr.substr(1);
   }
   exports.fromSetString = fromSetString;
 
-  function relative(aRoot, aPath) {
-    aRoot = aRoot.replace(/\/$/, '');
-
-    var url = urlParse(aRoot);
-    if (aPath.charAt(0) == "/" && url && url.path == "/") {
-      return aPath.slice(1);
-    }
-
-    return aPath.indexOf(aRoot + '/') === 0
-      ? aPath.substr(aRoot.length + 1)
-      : aPath;
-  }
-  exports.relative = relative;
-
   function strcmp(aStr1, aStr2) {
     var s1 = aStr1 || "";
     var s2 = aStr2 || "";
     return (s1 > s2) - (s1 < s2);
   }
 
   /**
    * Comparator between two mappings where the original positions are compared.
@@ -1026,17 +1159,17 @@ define('source-map/source-map-consumer',
    * following attributes:
    *
    *   - version: Which version of the source map spec this map is following.
    *   - sources: An array of URLs to the original source files.
    *   - names: An array of identifiers which can be referrenced by individual mappings.
    *   - sourceRoot: Optional. The URL root from which all sources are relative.
    *   - sourcesContent: Optional. An array of contents of the original source files.
    *   - mappings: A string of base64 VLQs which contain the actual mappings.
-   *   - file: The generated file this source map is associated with.
+   *   - file: Optional. The generated file this source map is associated with.
    *
    * Here is an example source map, taken from the source map spec[0]:
    *
    *     {
    *       version : 3,
    *       file: "out.js",
    *       sourceRoot : "",
    *       sources: ["foo.js", "bar.js"],
@@ -1049,26 +1182,35 @@ define('source-map/source-map-consumer',
   function SourceMapConsumer(aSourceMap) {
     var sourceMap = aSourceMap;
     if (typeof aSourceMap === 'string') {
       sourceMap = JSON.parse(aSourceMap.replace(/^\)\]\}'/, ''));
     }
 
     var version = util.getArg(sourceMap, 'version');
     var sources = util.getArg(sourceMap, 'sources');
-    var names = util.getArg(sourceMap, 'names');
+    // Sass 3.3 leaves out the 'names' array, so we deviate from the spec (which
+    // requires the array) to play nice here.
+    var names = util.getArg(sourceMap, 'names', []);
     var sourceRoot = util.getArg(sourceMap, 'sourceRoot', null);
     var sourcesContent = util.getArg(sourceMap, 'sourcesContent', null);
     var mappings = util.getArg(sourceMap, 'mappings');
     var file = util.getArg(sourceMap, 'file', null);
 
-    if (version !== this._version) {
+    // Once again, Sass deviates from the spec and supplies the version as a
+    // string rather than a number, so we use loose equality checking here.
+    if (version != this._version) {
       throw new Error('Unsupported version: ' + version);
     }
 
+    // Some source maps produce relative source paths like "./foo.js" instead of
+    // "foo.js".  Normalize these first so that future comparisons will succeed.
+    // See bugzil.la/1090768.
+    sources = sources.map(util.normalize);
+
     // Pass `true` below to allow duplicate names and sources. While source maps
     // are intended to be compressed and deduplicated, the TypeScript compiler
     // sometimes generates source maps with duplicates in them. See Github issue
     // #72 and bugzil.la/889492.
     this._names = ArraySet.fromArray(names, true);
     this._sources = ArraySet.fromArray(sources, true);
 
     this.sourceRoot = sourceRoot;
@@ -1109,17 +1251,17 @@ define('source-map/source-map-consumer',
   SourceMapConsumer.prototype._version = 3;
 
   /**
    * The list of original sources.
    */
   Object.defineProperty(SourceMapConsumer.prototype, 'sources', {
     get: function () {
       return this._sources.toArray().map(function (s) {
-        return this.sourceRoot ? util.join(this.sourceRoot, s) : s;
+        return this.sourceRoot != null ? util.join(this.sourceRoot, s) : s;
       }, this);
     }
   });
 
   // `__generatedMappings` and `__originalMappings` are arrays that hold the
   // parsed mapping coordinates from the source map's "mappings" attribute. They
   // are lazily instantiated, accessed via the `_generatedMappings` and
   // `_originalMappings` getters respectively, and we only parse the mappings
@@ -1170,96 +1312,102 @@ define('source-map/source-map-consumer',
         this.__originalMappings = [];
         this._parseMappings(this._mappings, this.sourceRoot);
       }
 
       return this.__originalMappings;
     }
   });
 
+  SourceMapConsumer.prototype._nextCharIsMappingSeparator =
+    function SourceMapConsumer_nextCharIsMappingSeparator(aStr) {
+      var c = aStr.charAt(0);
+      return c === ";" || c === ",";
+    };
+
   /**
    * Parse the mappings in a string in to a data structure which we can easily
    * query (the ordered arrays in the `this.__generatedMappings` and
    * `this.__originalMappings` properties).
    */
   SourceMapConsumer.prototype._parseMappings =
     function SourceMapConsumer_parseMappings(aStr, aSourceRoot) {
       var generatedLine = 1;
       var previousGeneratedColumn = 0;
       var previousOriginalLine = 0;
       var previousOriginalColumn = 0;
       var previousSource = 0;
       var previousName = 0;
-      var mappingSeparator = /^[,;]/;
       var str = aStr;
+      var temp = {};
       var mapping;
-      var temp;
 
       while (str.length > 0) {
         if (str.charAt(0) === ';') {
           generatedLine++;
           str = str.slice(1);
           previousGeneratedColumn = 0;
         }
         else if (str.charAt(0) === ',') {
           str = str.slice(1);
         }
         else {
           mapping = {};
           mapping.generatedLine = generatedLine;
 
           // Generated column.
-          temp = base64VLQ.decode(str);
+          base64VLQ.decode(str, temp);
           mapping.generatedColumn = previousGeneratedColumn + temp.value;
           previousGeneratedColumn = mapping.generatedColumn;
           str = temp.rest;
 
-          if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) {
+          if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
             // Original source.
-            temp = base64VLQ.decode(str);
+            base64VLQ.decode(str, temp);
             mapping.source = this._sources.at(previousSource + temp.value);
             previousSource += temp.value;
             str = temp.rest;
-            if (str.length === 0 || mappingSeparator.test(str.charAt(0))) {
+            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
               throw new Error('Found a source, but no line and column');
             }
 
             // Original line.
-            temp = base64VLQ.decode(str);
+            base64VLQ.decode(str, temp);
             mapping.originalLine = previousOriginalLine + temp.value;
             previousOriginalLine = mapping.originalLine;
             // Lines are stored 0-based
             mapping.originalLine += 1;
             str = temp.rest;
-            if (str.length === 0 || mappingSeparator.test(str.charAt(0))) {
+            if (str.length === 0 || this._nextCharIsMappingSeparator(str)) {
               throw new Error('Found a source and line, but no column');
             }
 
             // Original column.
-            temp = base64VLQ.decode(str);
+            base64VLQ.decode(str, temp);
             mapping.originalColumn = previousOriginalColumn + temp.value;
             previousOriginalColumn = mapping.originalColumn;
             str = temp.rest;
 
-            if (str.length > 0 && !mappingSeparator.test(str.charAt(0))) {
+            if (str.length > 0 && !this._nextCharIsMappingSeparator(str)) {
               // Original name.
-              temp = base64VLQ.decode(str);
+              base64VLQ.decode(str, temp);
               mapping.name = this._names.at(previousName + temp.value);
               previousName += temp.value;
               str = temp.rest;
             }
           }
 
           this.__generatedMappings.push(mapping);
           if (typeof mapping.originalLine === 'number') {
             this.__originalMappings.push(mapping);
           }
         }
       }
 
+      this.__generatedMappings.sort(util.compareByGeneratedPositions);
       this.__originalMappings.sort(util.compareByOriginalPositions);
     };
 
   /**
    * Find the mapping that best matches the hypothetical "needle" mapping that
    * we are searching for in the given "haystack" of mappings.
    */
   SourceMapConsumer.prototype._findMapping =
@@ -1278,16 +1426,43 @@ define('source-map/source-map-consumer',
         throw new TypeError('Column must be greater than or equal to 0, got '
                             + aNeedle[aColumnName]);
       }
 
       return binarySearch.search(aNeedle, aMappings, aComparator);
     };
 
   /**
+   * Compute the last column for each generated mapping. The last column is
+   * inclusive.
+   */
+  SourceMapConsumer.prototype.computeColumnSpans =
+    function SourceMapConsumer_computeColumnSpans() {
+      for (var index = 0; index < this._generatedMappings.length; ++index) {
+        var mapping = this._generatedMappings[index];
+
+        // Mappings do not contain a field for the last generated columnt. We
+        // can come up with an optimistic estimate, however, by assuming that
+        // mappings are contiguous (i.e. given two consecutive mappings, the
+        // first mapping ends where the second one starts).
+        if (index + 1 < this._generatedMappings.length) {
+          var nextMapping = this._generatedMappings[index + 1];
+
+          if (mapping.generatedLine === nextMapping.generatedLine) {
+            mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
+            continue;
+          }
+        }
+
+        // The last mapping for each line spans the entire line.
+        mapping.lastGeneratedColumn = Infinity;
+      }
+    };
+
+  /**
    * Returns the original source, line, and column information for the generated
    * source's line and column positions provided. The only argument is an object
    * with the following properties:
    *
    *   - line: The line number in the generated source.
    *   - column: The column number in the generated source.
    *
    * and an object is returned with the following properties:
@@ -1299,33 +1474,37 @@ define('source-map/source-map-consumer',
    */
   SourceMapConsumer.prototype.originalPositionFor =
     function SourceMapConsumer_originalPositionFor(aArgs) {
       var needle = {
         generatedLine: util.getArg(aArgs, 'line'),
         generatedColumn: util.getArg(aArgs, 'column')
       };
 
-      var mapping = this._findMapping(needle,
-                                      this._generatedMappings,
-                                      "generatedLine",
-                                      "generatedColumn",
-                                      util.compareByGeneratedPositions);
+      var index = this._findMapping(needle,
+                                    this._generatedMappings,
+                                    "generatedLine",
+                                    "generatedColumn",
+                                    util.compareByGeneratedPositions);
+
+      if (index >= 0) {
+        var mapping = this._generatedMappings[index];
 
-      if (mapping) {
-        var source = util.getArg(mapping, 'source', null);
-        if (source && this.sourceRoot) {
-          source = util.join(this.sourceRoot, source);
+        if (mapping.generatedLine === needle.generatedLine) {
+          var source = util.getArg(mapping, 'source', null);
+          if (source != null && this.sourceRoot != null) {
+            source = util.join(this.sourceRoot, source);
+          }
+          return {
+            source: source,
+            line: util.getArg(mapping, 'originalLine', null),
+            column: util.getArg(mapping, 'originalColumn', null),
+            name: util.getArg(mapping, 'name', null)
+          };
         }
-        return {
-          source: source,
-          line: util.getArg(mapping, 'originalLine', null),
-          column: util.getArg(mapping, 'originalColumn', null),
-          name: util.getArg(mapping, 'name', null)
-        };
       }
 
       return {
         source: null,
         line: null,
         column: null,
         name: null
       };
@@ -1337,26 +1516,26 @@ define('source-map/source-map-consumer',
    * availible.
    */
   SourceMapConsumer.prototype.sourceContentFor =
     function SourceMapConsumer_sourceContentFor(aSource) {
       if (!this.sourcesContent) {
         return null;
       }
 
-      if (this.sourceRoot) {
+      if (this.sourceRoot != null) {
         aSource = util.relative(this.sourceRoot, aSource);
       }
 
       if (this._sources.has(aSource)) {
         return this.sourcesContent[this._sources.indexOf(aSource)];
       }
 
       var url;
-      if (this.sourceRoot
+      if (this.sourceRoot != null
           && (url = util.urlParse(this.sourceRoot))) {
         // XXX: file:// URIs and absolute paths lead to unexpected behavior for
         // many users. We can help them out when they expect file:// URIs to
         // behave like it would if they were running a local HTTP server. See
         // https://bugzilla.mozilla.org/show_bug.cgi?id=885597.
         var fileUriAbsPath = aSource.replace(/^file:\/\//, "");
         if (url.scheme == "file"
             && this._sources.has(fileUriAbsPath)) {
@@ -1389,39 +1568,96 @@ define('source-map/source-map-consumer',
   SourceMapConsumer.prototype.generatedPositionFor =
     function SourceMapConsumer_generatedPositionFor(aArgs) {
       var needle = {
         source: util.getArg(aArgs, 'source'),
         originalLine: util.getArg(aArgs, 'line'),
         originalColumn: util.getArg(aArgs, 'column')
       };
 
-      if (this.sourceRoot) {
+      if (this.sourceRoot != null) {
         needle.source = util.relative(this.sourceRoot, needle.source);
       }
 
-      var mapping = this._findMapping(needle,
-                                      this._originalMappings,
-                                      "originalLine",
-                                      "originalColumn",
-                                      util.compareByOriginalPositions);
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions);
 
-      if (mapping) {
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
         return {
           line: util.getArg(mapping, 'generatedLine', null),
-          column: util.getArg(mapping, 'generatedColumn', null)
+          column: util.getArg(mapping, 'generatedColumn', null),
+          lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
         };
       }
 
       return {
         line: null,
-        column: null
+        column: null,
+        lastColumn: null
       };
     };
 
+  /**
+   * Returns all generated line and column information for the original source
+   * and line provided. The only argument is an object with the following
+   * properties:
+   *
+   *   - source: The filename of the original source.
+   *   - line: The line number in the original source.
+   *
+   * and an array of objects is returned, each with the following properties:
+   *
+   *   - line: The line number in the generated source, or null.
+   *   - column: The column number in the generated source, or null.
+   */
+  SourceMapConsumer.prototype.allGeneratedPositionsFor =
+    function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
+      // When there is no exact match, SourceMapConsumer.prototype._findMapping
+      // returns the index of the closest mapping less than the needle. By
+      // setting needle.originalColumn to Infinity, we thus find the last
+      // mapping for the given line, provided such a mapping exists.
+      var needle = {
+        source: util.getArg(aArgs, 'source'),
+        originalLine: util.getArg(aArgs, 'line'),
+        originalColumn: Infinity
+      };
+
+      if (this.sourceRoot != null) {
+        needle.source = util.relative(this.sourceRoot, needle.source);
+      }
+
+      var mappings = [];
+
+      var index = this._findMapping(needle,
+                                    this._originalMappings,
+                                    "originalLine",
+                                    "originalColumn",
+                                    util.compareByOriginalPositions);
+      if (index >= 0) {
+        var mapping = this._originalMappings[index];
+
+        while (mapping && mapping.originalLine === needle.originalLine) {
+          mappings.push({
+            line: util.getArg(mapping, 'generatedLine', null),
+            column: util.getArg(mapping, 'generatedColumn', null),
+            lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
+          });
+
+          mapping = this._originalMappings[--index];
+        }
+      }
+
+      return mappings.reverse();
+    };
+
   SourceMapConsumer.GENERATED_ORDER = 1;
   SourceMapConsumer.ORIGINAL_ORDER = 2;
 
   /**
    * Iterate over each mapping between an original source/line/column and a
    * generated line/column in this source map.
    *
    * @param Function aCallback
@@ -1451,17 +1687,17 @@ define('source-map/source-map-consumer',
         break;
       default:
         throw new Error("Unknown order of iteration.");
       }
 
       var sourceRoot = this.sourceRoot;
       mappings.map(function (mapping) {
         var source = mapping.source;
-        if (source && sourceRoot) {
+        if (source != null && sourceRoot != null) {
           source = util.join(sourceRoot, source);
         }
         return {
           source: source,
           generatedLine: mapping.generatedLine,
           generatedColumn: mapping.generatedColumn,
           originalLine: mapping.originalLine,
           originalColumn: mapping.originalColumn,
@@ -1490,202 +1726,220 @@ define('source-map/binary-search', ['req
    * @param aHaystack The non-empty array being searched.
    * @param aCompare Function which takes two elements and returns -1, 0, or 1.
    */
   function recursiveSearch(aLow, aHigh, aNeedle, aHaystack, aCompare) {
     // This function terminates when one of the following is true:
     //
     //   1. We find the exact element we are looking for.
     //
-    //   2. We did not find the exact element, but we can return the next
-    //      closest element that is less than that element.
+    //   2. We did not find the exact element, but we can return the index of
+    //      the next closest element that is less than that element.
     //
     //   3. We did not find the exact element, and there is no next-closest
     //      element which is less than the one we are searching for, so we
-    //      return null.
+    //      return -1.
     var mid = Math.floor((aHigh - aLow) / 2) + aLow;
     var cmp = aCompare(aNeedle, aHaystack[mid], true);
     if (cmp === 0) {
       // Found the element we are looking for.
-      return aHaystack[mid];
+      return mid;
     }
     else if (cmp > 0) {
       // aHaystack[mid] is greater than our needle.
       if (aHigh - mid > 1) {
         // The element is in the upper half.
         return recursiveSearch(mid, aHigh, aNeedle, aHaystack, aCompare);
       }
       // We did not find an exact match, return the next closest one
       // (termination case 2).
-      return aHaystack[mid];
+      return mid;
     }
     else {
       // aHaystack[mid] is less than our needle.
       if (mid - aLow > 1) {
         // The element is in the lower half.
         return recursiveSearch(aLow, mid, aNeedle, aHaystack, aCompare);
       }
       // The exact needle element was not found in this haystack. Determine if
       // we are in termination case (2) or (3) and return the appropriate thing.
-      return aLow < 0
-        ? null
-        : aHaystack[aLow];
+      return aLow < 0 ? -1 : aLow;
     }
   }
 
   /**
    * This is an implementation of binary search which will always try and return
-   * the next lowest value checked if there is no exact hit. This is because
-   * mappings between original and generated line/col pairs are single points,
-   * and there is an implicit region between each of them, so a miss just means
-   * that you aren't on the very start of a region.
+   * the index of next lowest value checked if there is no exact hit. This is
+   * because mappings between original and generated line/col pairs are single
+   * points, and there is an implicit region between each of them, so a miss
+   * just means that you aren't on the very start of a region.
    *
    * @param aNeedle The element you are looking for.
    * @param aHaystack The array that is being searched.
    * @param aCompare A function which takes the needle and an element in the
    *     array and returns -1, 0, or 1 depending on whether the needle is less
    *     than, equal to, or greater than the element, respectively.
    */
   exports.search = function search(aNeedle, aHaystack, aCompare) {
-    return aHaystack.length > 0
-      ? recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
-      : null;
+    if (aHaystack.length === 0) {
+      return -1;
+    }
+    return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
   };
 
 });
 /* -*- Mode: js; js-indent-level: 2; -*- */
 /*
  * Copyright 2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE or:
  * http://opensource.org/licenses/BSD-3-Clause
  */
 define('source-map/source-node', ['require', 'exports', 'module' ,  'source-map/source-map-generator', 'source-map/util'], function(require, exports, module) {
 
   var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
   var util = require('./util');
 
+  // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
+  // operating systems these days (capturing the result).
+  var REGEX_NEWLINE = /(\r?\n)/;
+
+  // Matches a Windows-style newline, or any character.
+  var REGEX_CHARACTER = /\r\n|[\s\S]/g;
+
   /**
    * SourceNodes provide a way to abstract over interpolating/concatenating
    * snippets of generated JavaScript source code while maintaining the line and
    * column information associated with the original source code.
    *
    * @param aLine The original line number.
    * @param aColumn The original column number.
    * @param aSource The original source's filename.
    * @param aChunks Optional. An array of strings which are snippets of
    *        generated JS, or other SourceNodes.
    * @param aName The original identifier.
    */
   function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
     this.children = [];
     this.sourceContents = {};
-    this.line = aLine === undefined ? null : aLine;
-    this.column = aColumn === undefined ? null : aColumn;
-    this.source = aSource === undefined ? null : aSource;
-    this.name = aName === undefined ? null : aName;
+    this.line = aLine == null ? null : aLine;
+    this.column = aColumn == null ? null : aColumn;
+    this.source = aSource == null ? null : aSource;
+    this.name = aName == null ? null : aName;
     if (aChunks != null) this.add(aChunks);
   }
 
   /**
    * Creates a SourceNode from generated code and a SourceMapConsumer.
    *
    * @param aGeneratedCode The generated code
    * @param aSourceMapConsumer The SourceMap for the generated code
+   * @param aRelativePath Optional. The path that relative sources in the
+   *        SourceMapConsumer should be relative to.
    */
   SourceNode.fromStringWithSourceMap =
-    function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer) {
+    function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
       // The SourceNode we want to fill with the generated code
       // and the SourceMap
       var node = new SourceNode();
 
-      // The generated code
-      // Processed fragments are removed from this array.
-      var remainingLines = aGeneratedCode.split('\n');
+      // All even indices of this array are one line of the generated code,
+      // while all odd indices are the newlines between two adjacent lines
+      // (since `REGEX_NEWLINE` captures its match).
+      // Processed fragments are removed from this array, by calling `shiftNextLine`.
+      var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
+      var shiftNextLine = function() {
+        var lineContents = remainingLines.shift();
+        // The last line of a file might not have a newline.
+        var newLine = remainingLines.shift() || "";
+        return lineContents + newLine;
+      };
 
       // We need to remember the position of "remainingLines"
       var lastGeneratedLine = 1, lastGeneratedColumn = 0;
 
       // The generate SourceNodes we need a code range.
       // To extract it current and last mapping is used.
       // Here we store the last mapping.
       var lastMapping = null;
 
       aSourceMapConsumer.eachMapping(function (mapping) {
-        if (lastMapping === null) {
-          // We add the generated code until the first mapping
-          // to the SourceNode without any mapping.
-          // Each line is added as separate string.
-          while (lastGeneratedLine < mapping.generatedLine) {
-            node.add(remainingLines.shift() + "\n");
-            lastGeneratedLine++;
-          }
-          if (lastGeneratedColumn < mapping.generatedColumn) {
-            var nextLine = remainingLines[0];
-            node.add(nextLine.substr(0, mapping.generatedColumn));
-            remainingLines[0] = nextLine.substr(mapping.generatedColumn);
-            lastGeneratedColumn = mapping.generatedColumn;
-          }
-        } else {
+        if (lastMapping !== null) {
           // We add the code from "lastMapping" to "mapping":
           // First check if there is a new line in between.
           if (lastGeneratedLine < mapping.generatedLine) {
             var code = "";
-            // Associate full lines with "lastMapping"
-            do {
-              code += remainingLines.shift() + "\n";
-              lastGeneratedLine++;
-              lastGeneratedColumn = 0;
-            } while (lastGeneratedLine < mapping.generatedLine);
-            // When we reached the correct line, we add code until we
-            // reach the correct column too.
-            if (lastGeneratedColumn < mapping.generatedColumn) {
-              var nextLine = remainingLines[0];
-              code += nextLine.substr(0, mapping.generatedColumn);
-              remainingLines[0] = nextLine.substr(mapping.generatedColumn);
-              lastGeneratedColumn = mapping.generatedColumn;
-            }
-            // Create the SourceNode.
-            addMappingWithCode(lastMapping, code);
+            // Associate first line with "lastMapping"
+            addMappingWithCode(lastMapping, shiftNextLine());
+            lastGeneratedLine++;
+            lastGeneratedColumn = 0;
+            // The remaining code is added without mapping
           } else {
             // There is no new line in between.
             // Associate the code between "lastGeneratedColumn" and
             // "mapping.generatedColumn" with "lastMapping"
             var nextLine = remainingLines[0];
             var code = nextLine.substr(0, mapping.generatedColumn -
                                           lastGeneratedColumn);
             remainingLines[0] = nextLine.substr(mapping.generatedColumn -
                                                 lastGeneratedColumn);
             lastGeneratedColumn = mapping.generatedColumn;
             addMappingWithCode(lastMapping, code);
+            // No more remaining code, continue
+            lastMapping = mapping;
+            return;
           }
         }
+        // We add the generated code until the first mapping
+        // to the SourceNode without any mapping.
+        // Each line is added as separate string.
+        while (lastGeneratedLine < mapping.generatedLine) {
+          node.add(shiftNextLine());
+          lastGeneratedLine++;
+        }
+        if (lastGeneratedColumn < mapping.generatedColumn) {
+          var nextLine = remainingLines[0];
+          node.add(nextLine.substr(0, mapping.generatedColumn));
+          remainingLines[0] = nextLine.substr(mapping.generatedColumn);
+          lastGeneratedColumn = mapping.generatedColumn;
+        }
         lastMapping = mapping;
       }, this);
       // We have processed all mappings.
-      // Associate the remaining code in the current line with "lastMapping"
-      // and add the remaining lines without any mapping
-      addMappingWithCode(lastMapping, remainingLines.join("\n"));
+      if (remainingLines.length > 0) {
+        if (lastMapping) {
+          // Associate the remaining code in the current line with "lastMapping"
+          addMappingWithCode(lastMapping, shiftNextLine());
+        }
+        // and add the remaining lines without any mapping
+        node.add(remainingLines.join(""));
+      }
 
       // Copy sourcesContent into SourceNode
       aSourceMapConsumer.sources.forEach(function (sourceFile) {
         var content = aSourceMapConsumer.sourceContentFor(sourceFile);
-        if (content) {
+        if (content != null) {
+          if (aRelativePath != null) {
+            sourceFile = util.join(aRelativePath, sourceFile);
+          }
           node.setSourceContent(sourceFile, content);
         }
       });
 
       return node;
 
       function addMappingWithCode(mapping, code) {
         if (mapping === null || mapping.source === undefined) {
           node.add(code);
         } else {
+          var source = aRelativePath
+            ? util.join(aRelativePath, mapping.source)
+            : mapping.source;
           node.add(new SourceNode(mapping.originalLine,
                                   mapping.originalColumn,
-                                  mapping.source,
+                                  source,
                                   code,
                                   mapping.name));
         }
       }
     };
 
   /**
    * Add a chunk of generated JS to this source node.
@@ -1895,22 +2149,40 @@ define('source-map/source-node', ['requi
           generated: {
             line: generated.line,
             column: generated.column
           }
         });
         lastOriginalSource = null;
         sourceMappingActive = false;
       }
-      chunk.split('').forEach(function (ch) {
-        if (ch === '\n') {
+      chunk.match(REGEX_CHARACTER).forEach(function (ch, idx, array) {
+        if (REGEX_NEWLINE.test(ch)) {
           generated.line++;
           generated.column = 0;
+          // Mappings end at eol
+          if (idx + 1 === array.length) {
+            lastOriginalSource = null;
+            sourceMappingActive = false;
+          } else if (sourceMappingActive) {
+            map.addMapping({
+              source: original.source,
+              original: {
+                line: original.line,
+                column: original.column
+              },
+              generated: {
+                line: generated.line,
+                column: generated.column
+              },
+              name: original.name
+            });
+          }
         } else {
-          generated.column++;
+          generated.column += ch.length;
         }
       });
     });
     this.walkSourceContents(function (sourceFile, sourceContent) {
       map.setSourceContent(sourceFile, sourceContent);
     });
 
     return { code: generated.code, map: map };
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -137,16 +137,32 @@ define('test/source-map/util', ['require
       ' };',
       ' TWO.inc = function (n) {\n' +
       '   return n + 1;\n' +
       ' };'
     ],
     sourceRoot: '/the/root',
     mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
   };
+  exports.testMapRelativeSources = {
+    version: 3,
+    file: 'min.js',
+    names: ['bar', 'baz', 'n'],
+    sources: ['./one.js', './two.js'],
+    sourcesContent: [
+      ' ONE.foo = function (bar) {\n' +
+      '   return baz(bar);\n' +
+      ' };',
+      ' TWO.inc = function (n) {\n' +
+      '   return n + 1;\n' +
+      ' };'
+    ],
+    sourceRoot: '/the/root',
+    mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
+  };
   exports.emptyMap = {
     version: 3,
     file: 'min.js',
     names: [],
     sources: [],
     mappings: ''
   };
 
--- a/toolkit/devtools/sourcemap/tests/unit/test_binary_search.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_binary_search.js
@@ -23,40 +23,40 @@ define("test/source-map/test-binary-sear
   exports['test too high'] = function (assert, util) {
     var needle = 30;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.doesNotThrow(function () {
       binarySearch.search(needle, haystack, numberCompare);
     });
 
-    assert.equal(binarySearch.search(needle, haystack, numberCompare), 20);
+    assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 20);
   };
 
   exports['test too low'] = function (assert, util) {
     var needle = 1;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
     assert.doesNotThrow(function () {
       binarySearch.search(needle, haystack, numberCompare);
     });
 
-    assert.equal(binarySearch.search(needle, haystack, numberCompare), null);
+    assert.equal(binarySearch.search(needle, haystack, numberCompare), -1);
   };
 
   exports['test exact search'] = function (assert, util) {
     var needle = 4;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
-    assert.equal(binarySearch.search(needle, haystack, numberCompare), 4);
+    assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 4);
   };
 
   exports['test fuzzy search'] = function (assert, util) {
     var needle = 19;
     var haystack = [2,4,6,8,10,12,14,16,18,20];
 
-    assert.equal(binarySearch.search(needle, haystack, numberCompare), 18);
+    assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 18);
   };
 
 });
 function run_test() {
   runSourceMapTests('test/source-map/test-binary-search', do_throw);
 }
--- a/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
+++ b/toolkit/devtools/sourcemap/tests/unit/test_source_map_consumer.js
@@ -252,16 +252,35 @@ define("test/source-map/test-source-map-
     assert.throws(function () {
       map.sourceContentFor("/the/root/three.js");
     }, Error);
     assert.throws(function () {
       map.sourceContentFor("three.js");
     }, Error);
   };
 
+  exports['test that we can get the original source content with relative source paths'] = function (assert, util) {
+    var map = new SourceMapConsumer(util.testMapRelativeSources);
+    var sources = map.sources;
+
+    assert.equal(map.sourceContentFor(sources[0]), ' ONE.foo = function (bar) {\n   return baz(bar);\n };');
+    assert.equal(map.sourceContentFor(sources[1]), ' TWO.inc = function (n) {\n   return n + 1;\n };');
+    assert.equal(map.sourceContentFor("one.js"), ' ONE.foo = function (bar) {\n   return baz(bar);\n };');
+    assert.equal(map.sourceContentFor("two.js"), ' TWO.inc = function (n) {\n   return n + 1;\n };');
+    assert.throws(function () {
+      map.sourceContentFor("");
+    }, Error);
+    assert.throws(function () {
+      map.sourceContentFor("/the/root/three.js");
+    }, Error);
+    assert.throws(function () {
+      map.sourceContentFor("three.js");
+    }, Error);
+  };
+
   exports['test sourceRoot + generatedPositionFor'] = function (assert, util) {
     var map = new SourceMapGenerator({
       sourceRoot: 'foo/bar',
       file: 'baz.js'
     });
     map.addMapping({
       original: { line: 1, column: 1 },
       generated: { line: 2, column: 2 },
@@ -290,16 +309,168 @@ define("test/source-map/test-source-map-
       column: 1,
       source: 'foo/bar/bang.coffee'
     });
 
     assert.equal(pos.line, 2);
     assert.equal(pos.column, 2);
   };
 
+  exports['test allGeneratedPositionsFor'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'generated.js'
+    });
+    map.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 2, column: 2 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 2, column: 2 },
+      source: 'bar.coffee'
+    });
+    map.addMapping({
+      original: { line: 2, column: 1 },
+      generated: { line: 3, column: 2 },
+      source: 'bar.coffee'
+    });
+    map.addMapping({
+      original: { line: 2, column: 2 },
+      generated: { line: 3, column: 3 },
+      source: 'bar.coffee'
+    });
+    map.addMapping({
+      original: { line: 3, column: 1 },
+      generated: { line: 4, column: 2 },
+      source: 'bar.coffee'
+    });
+    map = new SourceMapConsumer(map.toString());
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 2,
+      source: 'bar.coffee'
+    });
+
+    assert.equal(mappings.length, 2);
+    assert.equal(mappings[0].line, 3);
+    assert.equal(mappings[0].column, 2);
+    assert.equal(mappings[1].line, 3);
+    assert.equal(mappings[1].column, 3);
+  };
+
+  exports['test allGeneratedPositionsFor for line with no mappings'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'generated.js'
+    });
+    map.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 2, column: 2 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 2, column: 2 },
+      source: 'bar.coffee'
+    });
+    map.addMapping({
+      original: { line: 3, column: 1 },
+      generated: { line: 4, column: 2 },
+      source: 'bar.coffee'
+    });
+    map = new SourceMapConsumer(map.toString());
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 2,
+      source: 'bar.coffee'
+    });
+
+    assert.equal(mappings.length, 0);
+  };
+
+  exports['test allGeneratedPositionsFor source map with no mappings'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'generated.js'
+    });
+    map = new SourceMapConsumer(map.toString());
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 2,
+      source: 'bar.coffee'
+    });
+
+    assert.equal(mappings.length, 0);
+  };
+
+  exports['test computeColumnSpans'] = function (assert, util) {
+    var map = new SourceMapGenerator({
+      file: 'generated.js'
+    });
+    map.addMapping({
+      original: { line: 1, column: 1 },
+      generated: { line: 1, column: 1 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 2, column: 1 },
+      generated: { line: 2, column: 1 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 2, column: 2 },
+      generated: { line: 2, column: 10 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 2, column: 3 },
+      generated: { line: 2, column: 20 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 3, column: 1 },
+      generated: { line: 3, column: 1 },
+      source: 'foo.coffee'
+    });
+    map.addMapping({
+      original: { line: 3, column: 2 },
+      generated: { line: 3, column: 2 },
+      source: 'foo.coffee'
+    });
+    map = new SourceMapConsumer(map.toString());
+
+    map.computeColumnSpans();
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 1,
+      source: 'foo.coffee'
+    });
+
+    assert.equal(mappings.length, 1);
+    assert.equal(mappings[0].lastColumn, Infinity);
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 2,
+      source: 'foo.coffee'
+    });
+
+    assert.equal(mappings.length, 3);
+    assert.equal(mappings[0].lastColumn, 9);
+    assert.equal(mappings[1].lastColumn, 19);
+    assert.equal(mappings[2].lastColumn, Infinity);
+
+    var mappings = map.allGeneratedPositionsFor({
+      line: 3,
+      source: 'foo.coffee'
+    });
+
+    assert.equal(mappings.length, 2);
+    assert.equal(mappings[0].lastColumn, 1);
+    assert.equal(mappings[1].lastColumn, Infinity);
+  };
+
   exports['test sourceRoot + originalPositionFor'] = function (assert, util) {
     var map = new SourceMapGenerator({
       sourceRoot: 'foo/bar',
       file: 'baz.js'
     });
     map.addMapping({
       original: { line: 1, column: 1 },
       generated: { line: 2, column: 2 },
--- a/toolkit/devtools/sourcemap/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/sourcemap/tests/unit/xpcshell.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 head = head_sourcemap.js
 tail =
 support-files = Utils.jsm
 
+[test_util.js]
 [test_source_node.js]
 [test_source_map_generator.js]
 [test_source_map_consumer.js]
 [test_dog_fooding.js]
 [test_binary_search.js]
 [test_base64_vlq.js]
 [test_base64.js]
 [test_array_set.js]
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -579,30 +579,39 @@ xul|filefield + xul|button {
   -moz-border-start: none;
 }
 
 /* List boxes */
 
 xul|richlistbox,
 xul|listbox {
   -moz-appearance: none;
+  background-color: #fff;
   border: 1px solid #c1c1c1;
+  color: #333;
 }
 
 xul|treechildren::-moz-tree-row,
 xul|listbox xul|listitem {
-  padding: 5px;
+  padding: 0.3em;
   margin: 0;
   border: none;
+  border-radius: 0;
   background-image: none;
 }
 
+xul|treechildren::-moz-tree-row(hover),
+xul|listbox xul|listitem:hover {
+  background-color: rgba(0,149,221,0.25);
+}
+
 xul|treechildren::-moz-tree-row(selected),
 xul|listbox xul|listitem[selected="true"] {
-  background-color: #f1f1f1;
+  background-color: #0095dd;
+  color: #fff;
 }
 
 /* Trees */
 
 xul|tree {
   -moz-appearance: none;
   font-size: 1em;
   border: 1px solid #c1c1c1;
@@ -647,9 +656,23 @@ xul|treecol:not([hideheader="true"]) > x
 xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection="descending"] {
   transform: scaleY(-1);
 }
 
 @media (min-resolution: 2dppx) {
   xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] {
     list-style-image: url("chrome://global/skin/in-content/sorter@2x.png");
   }
+}
+
+/* This is the only way to increase the height of a tree row unfortunately */
+xul|treechildren::-moz-tree-row {
+  min-height: 2em;
+}
+
+/* Color needs to be set on tree cell in order to be applied */
+xul|treechildren::-moz-tree-cell-text {
+  color: #333;
+}
+
+xul|treechildren::-moz-tree-cell-text(selected) {
+  color: #fff;
 }
\ No newline at end of file