Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 15 Mar 2016 10:44:42 +0100
changeset 328622 7d9d6c06bf9ed5ebb30815c15306069d6fecf11a
parent 328621 6d8001b49fea6e73ddc1d3b7940840c21515b5cf (current diff)
parent 328573 5e14887312d4523ab59c3f6c6c94a679cf42b496 (diff)
child 328623 e2974ac0c94f60d020a7679c18f17a9b918b4ce1
push id1146
push userCallek@gmail.com
push dateMon, 25 Jul 2016 16:35:44 +0000
treeherdermozilla-release@a55778f9cd5a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone48.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 mozilla-central to mozilla-inbound
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -11,15 +11,16 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli
 
 DevToolsModules(
     'attach-thread.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
     'selection.js',
     'sidebar.js',
+    'source-location.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
     'toolbox.js',
     'ToolboxProcess.jsm',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/source-location.js
@@ -0,0 +1,137 @@
+/* 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 { Task } = require("resource://gre/modules/Task.jsm");
+const { assert } = require("devtools/shared/DevToolsUtils");
+
+/**
+ * A manager class that wraps a TabTarget and listens to source changes
+ * from source maps and resolves non-source mapped locations to the source mapped
+ * versions and back and forth, and creating smart elements with a location that
+ * auto-update when the source changes (from pretty printing, source maps loading, etc)
+ *
+ * @param {TabTarget} target
+ */
+function SourceLocationController(target) {
+  this.target = target;
+  this.locations = new Set();
+
+  this._onSourceUpdated = this._onSourceUpdated.bind(this);
+  this.reset = this.reset.bind(this);
+  this.destroy = this.destroy.bind(this);
+
+  target.on("source-updated", this._onSourceUpdated);
+  target.on("navigate", this.reset);
+  target.on("will-navigate", this.reset);
+  target.on("close", this.destroy);
+}
+
+SourceLocationController.prototype.reset = function() {
+  this.locations.clear();
+};
+
+SourceLocationController.prototype.destroy = function() {
+  this.locations.clear();
+  this.target.off("source-updated", this._onSourceUpdated);
+  this.target.off("navigate", this.reset);
+  this.target.off("will-navigate", this.reset);
+  this.target.off("close", this.destroy);
+  this.target = this.locations = null;
+};
+
+/**
+ * Add this `location` to be observed and register a callback
+ * whenever the underlying source is updated.
+ *
+ * @param {Object} location
+ *        An object with a {String} url, {Number} line, and optionally
+ *        a {Number} column.
+ * @param {Function} callback
+ */
+SourceLocationController.prototype.bindLocation = function(location, callback) {
+  assert(location.url, "Location must have a url.");
+  assert(location.line, "Location must have a line.");
+  this.locations.add({ location, callback });
+};
+
+/**
+ * Called when a new source occurs (a normal source, source maps) or an updated
+ * source (pretty print) occurs.
+ *
+ * @param {String} eventName
+ * @param {Object} sourceEvent
+ */
+SourceLocationController.prototype._onSourceUpdated = function(_, sourceEvent) {
+  let { type, source } = sourceEvent;
+  // If we get a new source, and it's not a source map, abort;
+  // we can ahve no actionable updates as this is just a new normal source.
+  // Also abort if there's no `url`, which means it's unsourcemappable anyway,
+  // like an eval script.
+  if (!source.url || type === "newSource" && !source.isSourceMapped) {
+    return;
+  }
+
+  for (let locationItem of this.locations) {
+    if (isSourceRelated(locationItem.location, source)) {
+      this._updateSource(locationItem);
+    }
+  }
+};
+
+SourceLocationController.prototype._updateSource = Task.async(function*(locationItem) {
+  let newLocation = yield resolveLocation(this.target, locationItem.location);
+  if (newLocation) {
+    let previousLocation = Object.assign({}, locationItem.location);
+    Object.assign(locationItem.location, newLocation);
+    locationItem.callback(previousLocation, newLocation);
+  }
+});
+
+/**
+ * Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
+ * the location to the latest location (so a source mapped location, or if pretty print
+ * status has been updated)
+ *
+ * @param {TabTarget} target
+ * @param {Object} location
+ * @return {Promise<Object>}
+ */
+function resolveLocation(target, location) {
+  return Task.spawn(function*() {
+    let newLocation = yield target.resolveLocation({
+      url: location.url,
+      line: location.line,
+      column: location.column || Infinity
+    });
+
+    // Source or mapping not found, so don't do anything
+    if (newLocation.error) {
+      return null;
+    }
+
+    return newLocation;
+  });
+}
+
+/**
+ * Takes a serialized SourceActor form and returns a boolean indicating
+ * if this source is related to this location, like if a location is a generated source,
+ * and the source map is loaded subsequently, the new source mapped SourceActor
+ * will be considered related to this location. Same with pretty printing new sources.
+ *
+ * @param {Object} location
+ * @param {Object} source
+ * @return {Boolean}
+ */
+function isSourceRelated(location, source) {
+         // Mapping location to subsequently loaded source map
+  return source.generatedUrl === location.url ||
+         // Mapping source map loc to source map
+         source.url === location.url
+}
+
+exports.SourceLocationController = SourceLocationController;
+exports.resolveLocation = resolveLocation;
+exports.isSourceRelated = isSourceRelated;
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -392,16 +392,17 @@ TabTarget.prototype = {
     let attachTab = () => {
       this._client.attachTab(this._form.actor, (response, tabClient) => {
         if (!tabClient) {
           this._remote.reject("Unable to attach to the tab");
           return;
         }
         this.activeTab = tabClient;
         this.threadActor = response.threadActor;
+
         attachConsole();
       });
     };
 
     let onConsoleAttached = (response, consoleClient) => {
       if (!consoleClient) {
         this._remote.reject("Unable to attach to the console");
         return;
@@ -493,26 +494,32 @@ TabTarget.prototype = {
       }
     };
     this.client.addListener("tabNavigated", this._onTabNavigated);
 
     this._onFrameUpdate = (aType, aPacket) => {
       this.emit("frame-update", aPacket);
     };
     this.client.addListener("frameUpdate", this._onFrameUpdate);
+
+    this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
+    this.client.addListener("newSource", this._onSourceUpdated);
+    this.client.addListener("updatedSource", this._onSourceUpdated);
   },
 
   /**
    * Teardown listeners for remote debugging.
    */
   _teardownRemoteListeners: function() {
     this.client.removeListener("closed", this.destroy);
     this.client.removeListener("tabNavigated", this._onTabNavigated);
     this.client.removeListener("tabDetached", this._onTabDetached);
     this.client.removeListener("frameUpdate", this._onFrameUpdate);
+    this.client.removeListener("newSource", this._onSourceUpdated);
+    this.client.removeListener("updatedSource", this._onSourceUpdated);
   },
 
   /**
    * Handle tabs events.
    */
   handleEvent: function(event) {
     switch (event.type) {
       case "TabClose":
@@ -598,16 +605,30 @@ TabTarget.prototype = {
     this._remote = null;
     this._root = null;
   },
 
   toString: function() {
     let id = this._tab ? this._tab : (this._form && this._form.actor);
     return `TabTarget:${id}`;
   },
+
+  /**
+   * @see TabActor.prototype.onResolveLocation
+   */
+  resolveLocation(loc) {
+    let deferred = promise.defer();
+
+    this.client.request(Object.assign({
+      to: this._form.actor,
+      type: "resolveLocation",
+    }, loc), deferred.resolve);
+
+    return deferred.promise;
+  },
 };
 
 /**
  * WebProgressListener for TabTarget.
  *
  * @param object aTarget
  *        The TabTarget instance to work with.
  */
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -2,16 +2,17 @@
 tags = devtools
 subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   browser_toolbox_sidebar_tool.xul
   code_math.js
+  code_ugly.js
   head.js
   shared-head.js
   shared-redux-head.js
   helper_disable_cache.js
   doc_theme.css
   doc_viewsource.html
   browser_toolbox_options_enable_serviceworkers_testing_frame_script.js
   browser_toolbox_options_enable_serviceworkers_testing.html
@@ -21,16 +22,18 @@ support-files =
 [browser_devtools_api.js]
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_ignore_toolbox_network_requests.js]
 [browser_keybindings_01.js]
 [browser_keybindings_02.js]
 [browser_keybindings_03.js]
 [browser_new_activation_workflow.js]
+[browser_source-location-01.js]
+[browser_source-location-02.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_toolbox_custom_host.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
 [browser_toolbox_hosts.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_source-location-01.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the SourceMapController updates generated sources when source maps
+ * are subsequently found. Also checks when no column is provided, and
+ * when tagging an already source mapped location initially.
+ */
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${DEBUGGER_ROOT}code_binary_search.js`;
+const COFFEE_URL = `${DEBUGGER_ROOT}code_binary_search.coffee`;
+const { SourceLocationController } = require("devtools/client/framework/source-location");
+
+add_task(function*() {
+  let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+
+  let controller = new SourceLocationController(toolbox.target);
+
+  let aggregator = [];
+
+  function onUpdate (oldLoc, newLoc) {
+    if (oldLoc.line === 6) {
+      checkLoc1(oldLoc, newLoc);
+    } else if (oldLoc.line === 8) {
+      checkLoc2(oldLoc, newLoc);
+    } else if (oldLoc.line === 2) {
+      checkLoc3(oldLoc, newLoc);
+    } else {
+      throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+    }
+    aggregator.push(newLoc);
+  }
+
+  let loc1 = { url: JS_URL, line: 6 };
+  let loc2 = { url: JS_URL, line: 8, column: 3 };
+  let loc3 = { url: COFFEE_URL, line: 2, column: 0 };
+
+  controller.bindLocation(loc1, onUpdate);
+  controller.bindLocation(loc2, onUpdate);
+  controller.bindLocation(loc3, onUpdate);
+
+  // Inject JS script
+  yield createScript(JS_URL);
+
+  yield waitUntil(() => aggregator.length === 3);
+
+  ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
+  ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
+  ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 2), "found third updated location");
+
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function checkLoc1 (oldLoc, newLoc) {
+  is(oldLoc.line, 6, "Correct line for JS:6");
+  is(oldLoc.column, null, "Correct column for JS:6");
+  is(oldLoc.url, JS_URL, "Correct url for JS:6");
+  is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
+  is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
+  is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
+}
+
+function checkLoc2 (oldLoc, newLoc) {
+  is(oldLoc.line, 8, "Correct line for JS:8:3");
+  is(oldLoc.column, 3, "Correct column for JS:8:3");
+  is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
+  is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
+  is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
+  is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
+}
+
+function checkLoc3 (oldLoc, newLoc) {
+  is(oldLoc.line, 2, "Correct line for COFFEE:2:0");
+  is(oldLoc.column, 0, "Correct column for COFFEE:2:0");
+  is(oldLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0");
+  is(newLoc.line, 2, "Correct line for COFFEE:2:0 -> COFFEE");
+  is(newLoc.column, 0, "Correct column for COFFEE:2:0 -> COFFEE");
+  is(newLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0 -> COFFEE");
+}
+
+function createScript (url) {
+  info(`Creating script: ${url}`);
+  let mm = getFrameScript();
+  let command = `
+    let script = document.createElement("script");
+    script.setAttribute("src", "${url}");
+    document.body.appendChild(script);
+    null;
+  `;
+  return evalInDebuggee(mm, command);
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_source-location-02.js
@@ -0,0 +1,107 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the SourceLocationController updates generated sources when pretty printing
+ * and un pretty printing.
+ */
+
+const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
+// Empty page
+const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
+const JS_URL = `${URL_ROOT}code_ugly.js`;
+const { SourceLocationController } = require("devtools/client/framework/source-location");
+
+add_task(function*() {
+  let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
+
+  let controller = new SourceLocationController(toolbox.target);
+
+  let checkedPretty = false;
+  let checkedUnpretty = false;
+
+  function onUpdate (oldLoc, newLoc) {
+    if (oldLoc.line === 3) {
+      checkPrettified(oldLoc, newLoc);
+      checkedPretty = true;
+    } else if (oldLoc.line === 9) {
+      checkUnprettified(oldLoc, newLoc);
+      checkedUnpretty = true;
+    } else {
+      throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
+    }
+  }
+
+  controller.bindLocation({ url: JS_URL, line: 3 }, onUpdate);
+
+  // Inject JS script
+  let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+  yield createScript(JS_URL);
+  yield sourceShown;
+
+  let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
+  sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+  ppButton.click();
+  yield sourceShown;
+  yield waitUntil(() => checkedPretty);
+
+  // TODO check unprettified change once bug 1177446 fixed
+  /*
+  sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
+  ppButton.click();
+  yield sourceShown;
+  yield waitUntil(() => checkedUnpretty);
+  */
+
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function checkPrettified (oldLoc, newLoc) {
+  is(oldLoc.line, 3, "Correct line for JS:3");
+  is(oldLoc.column, null, "Correct column for JS:3");
+  is(oldLoc.url, JS_URL, "Correct url for JS:3");
+  is(newLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+  is(newLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+  is(newLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+}
+
+function checkUnprettified (oldLoc, newLoc) {
+  is(oldLoc.line, 9, "Correct line for JS:3 -> PRETTY");
+  is(oldLoc.column, 0, "Correct column for JS:3 -> PRETTY");
+  is(oldLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
+  is(newLoc.line, 3, "Correct line for JS:3 -> UNPRETTIED");
+  is(newLoc.column, null, "Correct column for JS:3 -> UNPRETTIED");
+  is(newLoc.url, JS_URL, "Correct url for JS:3 -> UNPRETTIED");
+}
+
+function createScript (url) {
+  info(`Creating script: ${url}`);
+  let mm = getFrameScript();
+  let command = `
+    let script = document.createElement("script");
+    script.setAttribute("src", "${url}");
+    document.body.appendChild(script);
+  `;
+  return evalInDebuggee(mm, command);
+}
+
+function waitForSourceShown (debuggerPanel, url) {
+  let { panelWin } = debuggerPanel;
+  let deferred = promise.defer();
+
+  info(`Waiting for source ${url} to be shown in the debugger...`);
+  panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown (_, source) {
+    let sourceUrl = source.url || source.introductionUrl;
+
+    if (sourceUrl.includes(url)) {
+      panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
+      info(`Source shown for ${url}`);
+      deferred.resolve(source);
+    }
+  });
+
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/code_ugly.js
@@ -0,0 +1,3 @@
+function foo() { var a=1; var b=2; bar(a, b); }
+function bar(c, d) { return c - d; }
+foo();
--- a/devtools/client/framework/test/shared-head.js
+++ b/devtools/client/framework/test/shared-head.js
@@ -213,16 +213,17 @@ function waitForTick() {
  * @param {String} hostType Optional. The type of toolbox host to be used.
  * @return {Promise} Resolves with the toolbox, when it has been opened.
  */
 var openToolboxForTab = Task.async(function*(tab, toolId, hostType) {
   info("Opening the toolbox");
 
   let toolbox;
   let target = TargetFactory.forTab(tab);
+  yield target.makeRemote();
 
   // Check if the toolbox is already loaded.
   toolbox = gDevTools.getToolbox(target);
   if (toolbox) {
     if (!toolId || (toolId && toolbox.getPanel(toolId))) {
       info("Toolbox is already opened");
       return toolbox;
     }
@@ -258,8 +259,50 @@ var openNewTabAndToolbox = Task.async(fu
  * @return {Promise} Resolves when the toolbox and tab have been destroyed and
  * closed.
  */
 function closeToolboxAndTab(toolbox) {
   return toolbox.destroy().then(function() {
     gBrowser.removeCurrentTab();
   });
 }
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ *        Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ *        How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+  if (predicate()) {
+    return Promise.resolve(true);
+  }
+  return new Promise(resolve => {
+    setTimeout(function() {
+      waitUntil(predicate, interval).then(() => resolve(true));
+    }, interval);
+  });
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+let MM_INC_ID = 0;
+function evalInDebuggee (mm, script) {
+  return new Promise(function (resolve, reject) {
+    let id = MM_INC_ID++;
+    mm.sendAsyncMessage("devtools:test:eval", { script, id });
+    mm.addMessageListener("devtools:test:eval:response", handler);
+
+    function handler ({ data }) {
+      if (id !== data.id) {
+        return;
+      }
+
+      info(`Successfully evaled in debuggee: ${script}`);
+      mm.removeMessageListener("devtools:test:eval:response", handler);
+      resolve(data.value);
+    }
+  });
+}
--- a/devtools/client/shared/frame-script-utils.js
+++ b/devtools/client/shared/frame-script-utils.js
@@ -119,18 +119,17 @@ addMessageListener("devtools:test:profil
   let result = nsIProfilerModule[method](...args);
   sendAsyncMessage("devtools:test:profiler:response", {
     data: result,
     id: id
   });
 });
 
 
-// To eval in content, look at `evalInDebuggee` in the head.js of canvasdebugger
-// for an example.
+// To eval in content, look at `evalInDebuggee` in the shared-head.js.
 addMessageListener("devtools:test:eval", function ({ data }) {
   sendAsyncMessage("devtools:test:eval:response", {
     value: content.eval(data.script),
     id: data.id
   });
 });
 
 addEventListener("load", function() {
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -432,17 +432,17 @@ function ThreadActor(aParent, aGlobal)
   // A map of actorID -> actor for breakpoints created and managed by the
   // server.
   this._hiddenBreakpoints = new Map();
 
   this.global = aGlobal;
 
   this._allEventsListener = this._allEventsListener.bind(this);
   this.onNewGlobal = this.onNewGlobal.bind(this);
-  this.onNewSource = this.onNewSource.bind(this);
+  this.onSourceEvent = this.onSourceEvent.bind(this);
   this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
   this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
   this.onNewScript = this.onNewScript.bind(this);
   this.objectGrip = this.objectGrip.bind(this);
   this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
   this._onWindowReady = this._onWindowReady.bind(this);
   events.on(this._parent, "window-ready", this._onWindowReady);
   // Set a wrappedJSObject property so |this| can be sent via the observer svc
@@ -578,16 +578,18 @@ ThreadActor.prototype = {
     }
 
     // Blow away our source actor ID store because those IDs are only
     // valid for this connection. This is ok because we never keep
     // things like breakpoints across connections.
     this._sourceActorStore = null;
 
     events.off(this._parent, "window-ready", this._onWindowReady);
+    this.sources.off("newSource", this.onSourceEvent);
+    this.sources.off("updatedSource", this.onSourceEvent);
     this.clearDebuggees();
     this.conn.removeActorPool(this._threadLifetimePool);
     this._threadLifetimePool = null;
 
     if (this._prettyPrintWorker) {
       this._prettyPrintWorker.destroy();
       this._prettyPrintWorker = null;
     }
@@ -618,19 +620,18 @@ ThreadActor.prototype = {
                message: "Current state is " + this.state };
     }
 
     this._state = "attached";
     this._debuggerSourcesSeen = new Set();
 
     update(this._options, aRequest.options || {});
     this.sources.setOptions(this._options);
-    this.sources.on('newSource', (name, source) => {
-      this.onNewSource(source);
-    });
+    this.sources.on("newSource", this.onSourceEvent);
+    this.sources.on("updatedSource", this.onSourceEvent);
 
     // Initialize an event loop stack. This can't be done in the constructor,
     // because this.conn is not yet initialized by the actor pool at that time.
     this._nestedEventLoops = new EventLoopStack({
       hooks: this._parent,
       connection: this.conn,
       thread: this
     });
@@ -1889,22 +1890,39 @@ ThreadActor.prototype = {
    *        The source script that has been loaded into a debuggee compartment.
    * @param aGlobal Debugger.Object
    *        A Debugger.Object instance whose referent is the global object.
    */
   onNewScript: function (aScript, aGlobal) {
     this._addSource(aScript.source);
   },
 
-  onNewSource: function (aSource) {
+  /**
+   * A function called when there's a new or updated source from a thread actor's
+   * sources. Emits `newSource` and `updatedSource` on the tab actor.
+   *
+   * @param {String} name
+   * @param {SourceActor} source
+   */
+  onSourceEvent: function (name, source) {
     this.conn.send({
-      from: this.actorID,
-      type: "newSource",
-      source: aSource.form()
+      from: this._parent.actorID,
+      type: name,
+      source: source.form()
     });
+
+    // For compatibility and debugger still using `newSource` on the thread client,
+    // still emit this event here. Clean up in bug 1247084
+    if (name === "newSource") {
+      this.conn.send({
+        from: this.actorID,
+        type: name,
+        source: source.form()
+      });
+    }
   },
 
   /**
    * Restore any pre-existing breakpoints to the sources that we have access to.
    */
   _restoreBreakpoints: function () {
     if (this.breakpointActorMap.size === 0) {
       return;
@@ -2019,17 +2037,17 @@ ThreadActor.prototype.requestTypes = {
   "resume": ThreadActor.prototype.onResume,
   "clientEvaluate": ThreadActor.prototype.onClientEvaluate,
   "frames": ThreadActor.prototype.onFrames,
   "interrupt": ThreadActor.prototype.onInterrupt,
   "eventListeners": ThreadActor.prototype.onEventListeners,
   "releaseMany": ThreadActor.prototype.onReleaseMany,
   "sources": ThreadActor.prototype.onSources,
   "threadGrips": ThreadActor.prototype.onThreadGrips,
-  "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
+  "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties,
 };
 
 exports.ThreadActor = ThreadActor;
 
 /**
  * Creates a PauseActor.
  *
  * PauseActors exist for the lifetime of a given debuggee pause.  Used to
--- a/devtools/server/actors/source.js
+++ b/devtools/server/actors/source.js
@@ -164,20 +164,20 @@ let SourceActor = ActorClass({
         DevToolsUtils.reportException("SourceActor", error);
       });
     } else {
       this._init = null;
     }
   },
 
   get isSourceMapped() {
-    return !this.isInlineSource && (
+    return !!(!this.isInlineSource && (
       this._originalURL || this._generatedSource ||
         this.threadActor.sources.isPrettyPrinted(this.url)
-    );
+    ));
   },
 
   get isInlineSource() {
     return this._isInlineSource;
   },
 
   get threadActor() { return this._threadActor; },
   get sources() { return this._threadActor.sources; },
@@ -206,21 +206,23 @@ let SourceActor = ActorClass({
     // that doesn't have either
     let introductionUrl = null;
     if (source && source.introductionScript) {
       introductionUrl = source.introductionScript.source.url;
     }
 
     return {
       actor: this.actorID,
+      generatedUrl: this.generatedSource ? this.generatedSource.url : null,
       url: this.url ? this.url.split(" -> ").pop() : null,
       addonID: this._addonID,
       addonPath: this._addonPath,
       isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
       isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
+      isSourceMapped: this.isSourceMapped,
       introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
       introductionType: source ? source.introductionType : null
     };
   },
 
   disconnect: function () {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -241,16 +241,17 @@ TabSources.prototype = {
       }
 
       if (url in this._sourceMappedSourceActors) {
         return this._sourceMappedSourceActors[url];
       }
     }
 
     throw new Error('getSourceByURL: could not find source for ' + url);
+    return null;
   },
 
   /**
    * Returns true if the URL likely points to a minified resource, false
    * otherwise.
    *
    * @param String aURL
    *        The URL to test.
@@ -552,16 +553,17 @@ TabSources.prototype = {
       // just make a fake URL and stick the sourcemap there.
       url = "internal://sourcemap" + (this._anonSourceMapId++) + '/';
     }
     aSource.sourceMapURL = url;
 
     // Forcefully set the sourcemap cache. This will be used even if
     // sourcemaps are disabled.
     this._sourceMapCache[url] = resolve(aMap);
+    this.emit("updatedSource", this.getSourceActor(aSource));
   },
 
   /**
    * Return the non-source-mapped location of the given Debugger.Frame. If the
    * frame does not have a script, the location's properties are all null.
    *
    * @param Debugger.Frame aFrame
    *        The frame whose location we are getting.
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -4,17 +4,19 @@
  * 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";
 
 var { Ci, Cu } = require("chrome");
 var Services = require("Services");
 var promise = require("promise");
-var { ActorPool, createExtraActors, appendExtraActors } = require("devtools/server/actors/common");
+var {
+  ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
+} = require("devtools/server/actors/common");
 var { DebuggerServer } = require("devtools/server/main");
 var DevToolsUtils = require("devtools/shared/DevToolsUtils");
 var { assert } = DevToolsUtils;
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
@@ -1898,31 +1900,69 @@ TabActor.prototype = {
     if (aName in this._extraActors) {
       const actor = this._extraActors[aName];
       if (this._tabActorPool.has(actor)) {
         this._tabActorPool.removeActor(actor);
       }
       delete this._extraActors[aName];
     }
   },
+
+  /**
+   * Takes a packet containing a url, line and column and returns
+   * the updated url, line and column based on the current source mapping
+   * (source mapped files, pretty prints).
+   *
+   * @param {String} request.url
+   * @param {Number} request.line
+   * @param {Number?} request.column
+   * @return {Promise<Object>}
+   */
+  onResolveLocation: function (request) {
+    let { url, line } = request;
+    let column = request.column || 0;
+    let actor;
+
+    if (actor = this.sources.getSourceActorByURL(url)) {
+      // Get the generated source actor if this is source mapped
+      let generatedActor = actor.generatedSource ?
+                           this.sources.createNonSourceMappedActor(actor.generatedSource) :
+                           actor;
+      let generatedLocation = new GeneratedLocation(generatedActor, line, column);
+
+      return this.sources.getOriginalLocation(generatedLocation).then(loc => {
+        // If no map found, return this packet
+        if (loc.originalLine == null) {
+          return { from: this.actorID, type: "resolveLocation", error: "MAP_NOT_FOUND" };
+        }
+
+        loc = loc.toJSON();
+        return { from: this.actorID, url: loc.source.url, column: loc.column, line: loc.line };
+      });
+    }
+
+    // Fall back to this packet when source is not found
+    return promise.resolve({ from: this.actorID, type: "resolveLocation", error: "SOURCE_NOT_FOUND" });
+  },
 };
 
 /**
  * The request types this actor can handle.
  */
 TabActor.prototype.requestTypes = {
   "attach": TabActor.prototype.onAttach,
   "detach": TabActor.prototype.onDetach,
   "focus": TabActor.prototype.onFocus,
   "reload": TabActor.prototype.onReload,
   "navigateTo": TabActor.prototype.onNavigateTo,
   "reconfigure": TabActor.prototype.onReconfigure,
   "switchToFrame": TabActor.prototype.onSwitchToFrame,
   "listFrames": TabActor.prototype.onListFrames,
-  "listWorkers": TabActor.prototype.onListWorkers
+  "listWorkers": TabActor.prototype.onListWorkers,
+  "resolveLocation": TabActor.prototype.onResolveLocation
 };
 
 exports.TabActor = TabActor;
 
 /**
  * Creates a tab actor for handling requests to a single in-process
  * <xul:browser> tab, or <html:iframe>.
  * Most of the implementation comes from TabActor.
--- a/devtools/shared/client/main.js
+++ b/devtools/shared/client/main.js
@@ -173,16 +173,18 @@ const UnsolicitedNotifications = {
   "documentLoad": "documentLoad",
   "enteredFrame": "enteredFrame",
   "exitedFrame": "exitedFrame",
   "appOpen": "appOpen",
   "appClose": "appClose",
   "appInstall": "appInstall",
   "appUninstall": "appUninstall",
   "evaluationResult": "evaluationResult",
+  "newSource": "newSource",
+  "updatedSource": "updatedSource",
 };
 
 /**
  * Set of pause types that are sent by the server and not as an immediate
  * response to a client request.
  */
 const UnsolicitedPauses = {
   "resumeLimit": "resumeLimit",
@@ -242,18 +244,18 @@ const DebuggerClient = exports.DebuggerC
  *        The function to call after the response is received. It is passed the
  *        response, and the return value is considered the new response that
  *        will be passed to the callback. The |this| context is the instance of
  *        the client object we are defining a method for.
  * @return Request
  *         The `Request` object that is a Promise object and resolves once
  *         we receive the response. (See request method for more details)
  */
-DebuggerClient.requester = function (aPacketSkeleton,
-                                     { telemetry, before, after }) {
+DebuggerClient.requester = function (aPacketSkeleton, config={}) {
+  let { telemetry, before, after } = config;
   return DevToolsUtils.makeInfallible(function (...args) {
     let histogram, startTime;
     if (telemetry) {
       let transportType = this._transport.onOutputStreamReady === undefined
         ? "LOCAL_"
         : "REMOTE_";
       let histogramId = "DEVTOOLS_DEBUGGER_RDP_"
         + transportType + telemetry + "_MS";
@@ -1370,17 +1372,30 @@ TabClient.prototype = {
   listWorkers: DebuggerClient.requester({
     type: "listWorkers"
   }, {
     telemetry: "LISTWORKERS"
   }),
 
   attachWorker: function (aWorkerActor, aOnResponse) {
     this.client.attachWorker(aWorkerActor, aOnResponse);
-  }
+  },
+
+  /**
+   * Resolve a location ({ url, line, column }) to its current
+   * source mapping location.
+   *
+   * @param {String} arg[0].url
+   * @param {Number} arg[0].line
+   * @param {Number?} arg[0].column
+   */
+  resolveLocation: DebuggerClient.requester({
+    type: "resolveLocation",
+    location: args(0)
+  }),
 };
 
 eventSource(TabClient.prototype);
 
 function WorkerClient(aClient, aForm) {
   this.client = aClient;
   this._actor = aForm.from;
   this._isClosed = false;