Bug 1134778 - Consolidate usages of view sourcing in tools with a source-utils module. r=vp,jryans
authorJordan Santell <jsantell@gmail.com>
Fri, 24 Apr 2015 14:16:34 -0700
changeset 241172 c3d0143b9268645709c48948893e13dae1dd1d4c
parent 241171 fb84ee59e04a81aaa536f1bc2e174c2ac329831d
child 241173 d94e535e171c7b642d12d562b4810f90e2b84c75
push id59036
push usercbook@mozilla.com
push dateMon, 27 Apr 2015 10:37:48 +0000
treeherdermozilla-inbound@ad388474898c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersvp, jryans
bugs1134778
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1134778 - Consolidate usages of view sourcing in tools with a source-utils module. r=vp,jryans
browser/devtools/canvasdebugger/callslist.js
browser/devtools/canvasdebugger/canvasdebugger.js
browser/devtools/framework/test/browser.ini
browser/devtools/framework/test/browser_toolbox_view_source_01.js
browser/devtools/framework/test/browser_toolbox_view_source_02.js
browser/devtools/framework/test/browser_toolbox_view_source_03.js
browser/devtools/framework/test/browser_toolbox_view_source_04.js
browser/devtools/framework/test/code_math.js
browser/devtools/framework/test/doc_viewsource.html
browser/devtools/framework/test/head.js
browser/devtools/framework/toolbox.js
browser/devtools/framework/toolbox.xul
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/inspector.xul
browser/devtools/performance/test/browser.ini
browser/devtools/performance/test/browser_perf-jump-to-debugger-01.js
browser/devtools/performance/test/browser_perf-jump-to-debugger-02.js
browser/devtools/performance/test/head.js
browser/devtools/performance/views/details-js-call-tree.js
browser/devtools/performance/views/details-memory-call-tree.js
browser/devtools/performance/views/details-waterfall.js
browser/devtools/performance/views/jit-optimizations.js
browser/devtools/shared/moz.build
browser/devtools/shared/source-utils.js
browser/devtools/shared/timeline/marker-details.js
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/style-inspector.js
browser/devtools/webconsole/hudservice.js
browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
browser/devtools/webconsole/test/browser_console_error_source_click.js
browser/devtools/webconsole/webconsole.js
--- a/browser/devtools/canvasdebugger/callslist.js
+++ b/browser/devtools/canvasdebugger/callslist.js
@@ -379,17 +379,17 @@ let CallsListView = Heritage.extend(Widg
     // or jump to the corresponding file and line in the Debugger if a
     // location link was clicked.
     if (view.hasAttribute("call-stack-populated")) {
       let isExpanded = view.getAttribute("call-stack-expanded") == "true";
 
       // If clicking on the location, jump to the Debugger.
       if (e.target.classList.contains("call-item-location")) {
         let { file, line } = callItem.attachment.actor;
-        viewSourceInDebugger(file, line);
+        this._viewSourceInDebugger(file, line);
         return;
       }
       // Otherwise hide the call stack.
       else {
         view.setAttribute("call-stack-expanded", !isExpanded);
         $(".call-item-stack", view).hidden = isExpanded;
         return;
       }
@@ -451,17 +451,17 @@ let CallsListView = Heritage.extend(Widg
    * The click listener for a location link in the call stack.
    *
    * @param string file
    *        The url of the source owning the function.
    * @param number line
    *        The line of the respective function.
    */
   _onStackFileClick: function(e, { file, line }) {
-    viewSourceInDebugger(file, line);
+    this._viewSourceInDebugger(file, line);
   },
 
   /**
    * The click listener for a thumbnail in the filmstrip.
    *
    * @param number index
    *        The function index in the recorded animation frame snapshot.
    */
@@ -496,18 +496,31 @@ let CallsListView = Heritage.extend(Widg
    */
   _onStepIn: function() {
     if (this.selectedIndex == -1) {
       this._onResume();
       return;
     }
     let callItem = this.selectedItem;
     let { file, line } = callItem.attachment.actor;
-    viewSourceInDebugger(file, line);
+    this._viewSourceInDebugger(file, line);
   },
 
   /**
    * The click listener for the "step out" button in this container's toolbar.
    */
   _onStepOut: function() {
     this.selectedIndex = this.itemCount - 1;
+  },
+
+  /**
+   * Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
+   */
+  _viewSourceInDebugger: function (file, line) {
+    gToolbox.viewSourceInDebugger(file, line).then(success => {
+      if (success) {
+        window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+      } else {
+        window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+      }
+    });
   }
 });
--- a/browser/devtools/canvasdebugger/canvasdebugger.js
+++ b/browser/devtools/canvasdebugger/canvasdebugger.js
@@ -7,21 +7,23 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 
-const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const { require } = devtools;
 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const { CallWatcherFront } = require("devtools/server/actors/call-watcher");
 const { CanvasFront } = require("devtools/server/actors/canvas");
+
 const Telemetry = require("devtools/shared/telemetry");
 const telemetry = new Telemetry();
 
 const CANVAS_ACTOR_RECORDING_ATTEMPT = gDevTools.testing ? 500 : 5000;
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
@@ -349,37 +351,8 @@ function getThumbnailForCall(thumbnails,
   for (let i = thumbnails.length - 1; i >= 0; i--) {
     let thumbnail = thumbnails[i];
     if (thumbnail.index <= index) {
       return thumbnail;
     }
   }
   return CanvasFront.INVALID_SNAPSHOT_IMAGE;
 }
-
-/**
- * Opens/selects the debugger in this toolbox and jumps to the specified
- * file name and line number.
- */
-function viewSourceInDebugger(url, line) {
-  let showSource = ({ DebuggerView }) => {
-    let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url);
-    if (item) {
-      DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true }).then(() => {
-        window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
-      }, () => {
-        window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
-      });
-    }
-  }
-
-  // If the Debugger was already open, switch to it and try to show the
-  // source immediately. Otherwise, initialize it and wait for the sources
-  // to be added first.
-  let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
-  gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
-    if (debuggerAlreadyOpen) {
-      showSource(dbg);
-    } else {
-      dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
-    }
-  });
-}
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -1,19 +1,21 @@
 [DEFAULT]
 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
   head.js
   helper_disable_cache.js
   doc_theme.css
+  doc_viewsource.html
   browser_toolbox_options_enable_serviceworkers_testing.html
   serviceworker.js
 
 [browser_devtools_api.js]
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_keybindings.js]
 [browser_new_activation_workflow.js]
@@ -43,21 +45,25 @@ skip-if = e10s # Bug 1069044 - destroyIn
 [browser_toolbox_sidebar_events.js]
 [browser_toolbox_sidebar_existing_tabs.js]
 [browser_toolbox_sidebar_overflow_menu.js]
 [browser_toolbox_tabsswitch_shortcuts.js]
 [browser_toolbox_textbox_context_menu.js]
 [browser_toolbox_tool_ready.js]
 [browser_toolbox_tool_remote_reopen.js]
 [browser_toolbox_transport_events.js]
+[browser_toolbox_view_source_01.js]
+[browser_toolbox_view_source_02.js]
+[browser_toolbox_view_source_03.js]
+[browser_toolbox_view_source_04.js]
 [browser_toolbox_window_reload_target.js]
 [browser_toolbox_window_shortcuts.js]
 skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
 [browser_toolbox_window_title_changes.js]
 [browser_toolbox_zoom.js]
 [browser_toolbox_custom_host.js]
 [browser_toolbox_theme_registration.js]
 [browser_toolbox_options_enable_serviceworkers_testing.js]
-skip-if = e10s # Bug 1030318
+skip-if = true # Bug 1153407 - this test breaks subsequent tests and is not e10s compatible
 
 # We want this test to run for mochitest-dt as well, so we include it here:
 [../../../base/content/test/general/browser_parsable_css.js]
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_view_source_01.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is not
+ * yet opened.
+ */
+
+let URL = `${URL_ROOT}doc_viewsource.html`;
+let JS_URL = `${URL_ROOT}code_math.js`;
+
+function *viewSource() {
+  let toolbox = yield loadToolbox(URL);
+
+  yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+  let debuggerPanel = toolbox.getPanel("jsdebugger");
+  ok(debuggerPanel, "The debugger panel was opened.");
+  is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+  let { DebuggerView } = debuggerPanel.panelWin;
+  let Sources = DebuggerView.Sources;
+
+  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+    "The correct source is shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line + 1, 2,
+    "The correct line is highlighted in the debugger's source editor.");
+
+  yield unloadToolbox(toolbox);
+  finish();
+}
+
+function test () {
+  Task.spawn(viewSource).then(finish, (aError) => {
+    ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_view_source_02.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
+ */
+
+let URL = `${URL_ROOT}doc_viewsource.html`;
+let JS_URL = `${URL_ROOT}code_math.js`;
+
+function *viewSource() {
+  let toolbox = yield loadToolbox(URL);
+  let { panelWin: debuggerWin } = yield toolbox.selectTool("jsdebugger");
+  let debuggerEvents = debuggerWin.EVENTS;
+  let { DebuggerView } = debuggerWin;
+  let Sources = DebuggerView.Sources;
+
+  yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
+  ok("A source was shown in the debugger.");
+
+  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+    "The correct source is initially shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line, 0,
+    "The correct line is initially highlighted in the debugger's source editor.");
+
+  yield toolbox.viewSourceInDebugger(JS_URL, 2);
+
+  let debuggerPanel = toolbox.getPanel("jsdebugger");
+  ok(debuggerPanel, "The debugger panel was opened.");
+  is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
+
+  is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
+    "The correct source is shown in the debugger.");
+  is(DebuggerView.editor.getCursor().line + 1, 2,
+    "The correct line is highlighted in the debugger's source editor.");
+
+  yield unloadToolbox(toolbox);
+  finish();
+}
+
+function test () {
+  Task.spawn(viewSource).then(finish, (aError) => {
+    ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_view_source_03.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInStyleEditor works when style editor is not
+ * yet opened.
+ */
+
+let URL = `${URL_ROOT}doc_viewsource.html`;
+let CSS_URL = `${URL_ROOT}doc_theme.css`;
+
+function *viewSource() {
+  let toolbox = yield loadToolbox(URL);
+
+  let fileFound = yield toolbox.viewSourceInStyleEditor(CSS_URL, 2);
+  ok(fileFound, "viewSourceInStyleEditor should resolve to true if source found.");
+
+  let stylePanel = toolbox.getPanel("styleeditor");
+  ok(stylePanel, "The style editor panel was opened.");
+  is(toolbox.currentToolId, "styleeditor", "The style editor panel was selected.");
+
+  let { UI } = stylePanel;
+
+  is(UI.selectedEditor.styleSheet.href, CSS_URL,
+    "The correct source is shown in the style editor.");
+  is(UI.selectedEditor.sourceEditor.getCursor().line + 1, 2,
+    "The correct line is highlighted in the style editor's source editor.");
+
+  yield unloadToolbox(toolbox);
+  finish();
+}
+
+function test () {
+  Task.spawn(viewSource).then(finish, (aError) => {
+    ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_toolbox_view_source_04.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Toolbox#viewSourceInScratchpad works.
+ */
+
+let URL = `${URL_ROOT}doc_viewsource.html`;
+
+function *viewSource() {
+  let toolbox = yield loadToolbox(URL);
+  let win = yield openScratchpadWindow();
+  let { Scratchpad: scratchpad } = win;
+
+  // Brahm's Cello Sonata No.1, Op.38 now in the scratchpad
+  scratchpad.setText("E G B C B\nA B A G A B\nG E");
+  let scratchpadURL = scratchpad.uniqueName;
+
+  // Now select another tool for focus
+  yield toolbox.selectTool("webconsole");
+
+  yield toolbox.viewSourceInScratchpad(scratchpadURL, 2);
+
+  is(scratchpad.editor.getCursor().line, 2,
+    "The correct line is highlighted in scratchpad's editor.");
+
+  win.close();
+  yield unloadToolbox(toolbox);
+  finish();
+}
+
+function test () {
+  Task.spawn(viewSource).then(finish, (aError) => {
+    ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
+    finish();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/code_math.js
@@ -0,0 +1,4 @@
+function add(a, b, k) {
+  var result = a + b;
+  return k(result);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/doc_viewsource.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Toolbox test for View Source methods</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+    <link charset="UTF-8" rel="stylesheet" href="doc_theme.css" />
+    <script src="code_math.js"></script>
+  </head>
+  <body>
+  </body>
+</html>
--- a/browser/devtools/framework/test/head.js
+++ b/browser/devtools/framework/test/head.js
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 let TargetFactory = gDevTools.TargetFactory;
 
 const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { ScratchpadManager } = Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", {});
 
 const URL_ROOT = "http://example.com/browser/browser/devtools/framework/test/";
 const CHROME_URL_ROOT = "chrome://mochitests/content/browser/browser/devtools/framework/test/";
 
 let TargetFactory = devtools.TargetFactory;
 
 // All test are asynchronous
 waitForExplicitFinish();
@@ -170,8 +171,54 @@ function getChromeActors(callback)
       callback(client, response.form);
     });
   });
 
   SimpleTest.registerCleanupFunction(() => {
     DebuggerServer.destroy();
   });
 }
+
+function loadToolbox (url) {
+  let { promise: p, resolve } = promise.defer();
+  gBrowser.selectedTab = gBrowser.addTab();
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+
+  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
+    gDevTools.showToolbox(target).then(resolve);
+  }, true);
+
+  content.location = url;
+  return p;
+}
+
+function unloadToolbox (toolbox) {
+  return toolbox.destroy().then(function() {
+    gBrowser.removeCurrentTab();
+  });
+}
+
+function getSourceActor(aSources, aURL) {
+  let item = aSources.getItemForAttachment(a => a.source.url === aURL);
+  return item && item.value;
+}
+
+/**
+ * Open a Scratchpad window.
+ *
+ * @return nsIDOMWindow
+ *         The new window object that holds Scratchpad.
+ */
+function *openScratchpadWindow () {
+  let { promise: p, resolve } = promise.defer();
+  let win = ScratchpadManager.openScratchpad();
+
+  yield once(win, "load");
+
+  win.Scratchpad.addObserver({
+    onReady: function () {
+      win.Scratchpad.removeObserver(this);
+      resolve(win);
+    }
+  });
+  return p;
+}
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -16,16 +16,17 @@ const SCREENSIZE_HISTOGRAM = "DEVTOOLS_S
 
 let {Cc, Ci, Cu} = require("chrome");
 let {Promise: promise} = require("resource://gre/modules/Promise.jsm");
 let EventEmitter = require("devtools/toolkit/event-emitter");
 let Telemetry = require("devtools/shared/telemetry");
 let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils");
 let HUDService = require("devtools/webconsole/hudservice");
 let {showDoorhanger} = require("devtools/shared/doorhanger");
+let sourceUtils = require("devtools/shared/source-utils");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
@@ -1706,16 +1707,17 @@ Toolbox.prototype = {
     this.off("ready", this._showDevEditionPromo);
 
     gDevTools.off("tool-registered", this._toolRegistered);
     gDevTools.off("tool-unregistered", this._toolUnregistered);
 
     gDevTools.off("pref-changed", this._prefChanged);
 
     this._lastFocusedElement = null;
+
     if (this.webconsolePanel) {
       this._saveSplitConsoleHeight();
       this.webconsolePanel.removeEventListener("resize",
         this._saveSplitConsoleHeight);
     }
     this.closeButton.removeEventListener("command", this.destroy, true);
     this.textboxContextMenuPopup.removeEventListener("popupshowing",
       this._updateTextboxMenuItems, true);
@@ -1875,9 +1877,53 @@ Toolbox.prototype = {
    */
   _disconnectProfiler: Task.async(function*() {
     if (!this._performanceConnection) {
       return;
     }
     yield this._performanceConnection.destroy();
     this._performanceConnection = null;
   }),
+
+  /**
+   * Returns gViewSourceUtils for viewing source.
+   */
+  get gViewSourceUtils() {
+    return this.frame.contentWindow.gViewSourceUtils;
+  },
+
+  /**
+   * Opens source in style editor. Falls back to plain "view-source:".
+   * @see browser/devtools/shared/source-utils.js
+   */
+  viewSourceInStyleEditor: function (sourceURL, sourceLine) {
+    return sourceUtils.viewSourceInStyleEditor(this, sourceURL, sourceLine);
+  },
+
+  /**
+   * Opens source in debugger. Falls back to plain "view-source:".
+   * @see browser/devtools/shared/source-utils.js
+   */
+  viewSourceInDebugger: function (sourceURL, sourceLine) {
+    return sourceUtils.viewSourceInDebugger(this, sourceURL, sourceLine);
+  },
+
+  /**
+   * Opens source in scratchpad. Falls back to plain "view-source:".
+   * TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
+   * If instances are scoped one-per-browser-window, then we should be able
+   * to infer the URL from this toolbox, or use the built in scratchpad IN
+   * the toolbox.
+   *
+   * @see browser/devtools/shared/source-utils.js
+   */
+  viewSourceInScratchpad: function (sourceURL, sourceLine) {
+    return sourceUtils.viewSourceInScratchpad(sourceURL, sourceLine);
+  },
+
+  /**
+   * Opens source in plain "view-source:".
+   * @see browser/devtools/shared/source-utils.js
+   */
+  viewSource: function (sourceURL, sourceLine) {
+    return sourceUtils.viewSource(this, sourceURL, sourceLine);
+  },
 };
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -13,16 +13,19 @@
 <!ENTITY % editMenuStrings SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
 %editMenuStrings;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
+  <script type="application/javascript"
+          src="chrome://global/content/viewSourceUtils.js"/>
+
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
 
   <commandset id="editMenuCommands"/>
   <keyset id="editMenuKeys"/>
   <keyset id="toolbox-keyset">
     <key id="toolbox-options-key"
          key="&toolboxOptionsButton.key;"
          oncommand="void(0);"
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -279,23 +279,16 @@ InspectorPanel.prototype = {
   /**
    * Target setter.
    */
   set target(value) {
     this._target = value;
   },
 
   /**
-   * Expose gViewSourceUtils so that other tools can make use of them.
-   */
-  get viewSourceUtils() {
-    return this.panelWin.gViewSourceUtils;
-  },
-
-  /**
    * Indicate that a tool has modified the state of the page.  Used to
    * decide whether to show the "are you sure you want to navigate"
    * notification.
    */
   markDirty: function InspectorPanel_markDirty() {
     this.isDirty = true;
   },
 
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -13,19 +13,16 @@
    %inspectorDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script type="application/javascript;version=1.8"
           src="chrome://browser/content/devtools/theme-switching.js"/>
 
-  <script type="application/javascript"
-          src="chrome://global/content/viewSourceUtils.js"/>
-
   <commandset>
     <command id="nodeSearchCommand"
       oncommand="inspector.searchBox.focus()"/>
   </commandset>
 
   <keyset>
     <key id="nodeSearchKey"
       key="&inspectorSearchHTML.key;"
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -53,18 +53,16 @@ support-files =
 #[browser_perf-front-profiler-05.js] bug 1077464
 #[browser_perf-front-profiler-06.js]
 [browser_perf-front-01.js]
 [browser_perf-front-02.js]
 [browser_perf-jit-view-01.js]
 [browser_perf-jit-view-02.js]
 [browser_perf-jit-model-01.js]
 [browser_perf-jit-model-02.js]
-[browser_perf-jump-to-debugger-01.js]
-[browser_perf-jump-to-debugger-02.js]
 [browser_perf-options-01.js]
 [browser_perf-options-02.js]
 [browser_perf-options-invert-call-tree-01.js]
 [browser_perf-options-invert-call-tree-02.js]
 [browser_perf-options-invert-flame-graph-01.js]
 [browser_perf-options-invert-flame-graph-02.js]
 [browser_perf-options-flatten-tree-recursion-01.js]
 [browser_perf-options-flatten-tree-recursion-02.js]
deleted file mode 100644
--- a/browser/devtools/performance/test/browser_perf-jump-to-debugger-01.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the performance tool can jump to the debugger.
- */
-
-function spawnTest () {
-  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
-  let { viewSourceInDebugger } = panel.panelWin;
-
-  yield viewSourceInDebugger(SIMPLE_URL, 14);
-
-  let debuggerPanel = toolbox.getPanel("jsdebugger");
-  ok(debuggerPanel, "The debugger panel was opened.");
-
-  let { DebuggerView } = debuggerPanel.panelWin;
-  let Sources = DebuggerView.Sources;
-
-  is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
-    "The correct source is shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line + 1, 14,
-    "The correct line is highlighted in the debugger's source editor.");
-
-  yield teardown(panel);
-  finish();
-}
deleted file mode 100644
--- a/browser/devtools/performance/test/browser_perf-jump-to-debugger-02.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the performance tool can jump to the debugger, when the source was
- * already loaded in that tool.
- */
-
-function spawnTest() {
-  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "jsdebugger");
-  let debuggerWin = panel.panelWin;
-  let debuggerEvents = debuggerWin.EVENTS;
-  let { DebuggerView } = debuggerWin;
-  let Sources = DebuggerView.Sources;
-
-  yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
-  ok("A source was shown in the debugger.");
-
-  is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
-    "The correct source is initially shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line, 0,
-    "The correct line is initially highlighted in the debugger's source editor.");
-
-  yield toolbox.selectTool("performance");
-  let perfPanel = toolbox.getCurrentPanel();
-  let perfWin = perfPanel.panelWin;
-  let { viewSourceInDebugger } = perfWin;
-
-  yield viewSourceInDebugger(SIMPLE_URL, 14);
-
-  panel = toolbox.getPanel("jsdebugger");
-  ok(panel, "The debugger panel was reselected.");
-
-  is(DebuggerView.Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
-    "The correct source is still shown in the debugger.");
-  is(DebuggerView.editor.getCursor().line + 1, 14,
-    "The correct line is now highlighted in the debugger's source editor.");
-
-  yield teardown(perfPanel);
-  finish();
-}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -468,21 +468,16 @@ function dragStop(graph, x, y = 1) {
   graph._onMouseUp({ clientX: x, clientY: y });
 }
 
 function dropSelection(graph) {
   graph.dropSelection();
   graph.emit("selecting");
 }
 
-function getSourceActor(aSources, aURL) {
-  let item = aSources.getItemForAttachment(a => a.source.url === aURL);
-  return item && item.value;
-}
-
 /**
  * Fires a key event, like "VK_UP", "VK_DOWN", etc.
  */
 function fireKey (e) {
   EventUtils.synthesizeKey(e, {});
 }
 
 function reload (aTarget, aEvent = "navigate") {
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -56,19 +56,23 @@ let JsCallTreeView = Heritage.extend(Det
     this.emit(EVENTS.JS_CALL_TREE_RENDERED);
   },
 
   /**
    * Fired on the "link" event for the call tree in this container.
    */
   _onLink: function (_, treeItem) {
     let { url, line } = treeItem.frame.getInfo();
-    viewSourceInDebugger(url, line).then(
-      () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
-      () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
+    gToolbox.viewSourceInDebugger(url, line).then(success => {
+      if (success) {
+        this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+      } else {
+        this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+      }
+    });
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (profile, { startTime, endTime }, options) {
     let threadSamples = profile.threads[0].samples;
@@ -116,36 +120,8 @@ let JsCallTreeView = Heritage.extend(Det
     root.toggleCategories(options.contentOnly);
 
     // Return the CallView for tests
     return root;
   },
 
   toString: () => "[object JsCallTreeView]"
 });
-
-/**
- * Opens/selects the debugger in this toolbox and jumps to the specified
- * file name and line number.
- * @param string url
- * @param number line
- */
-let viewSourceInDebugger = Task.async(function *(url, line) {
-  // If the Debugger was already open, switch to it and try to show the
-  // source immediately. Otherwise, initialize it and wait for the sources
-  // to be added first.
-  let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
-  let { panelWin: dbg } = yield gToolbox.selectTool("jsdebugger");
-
-  if (!debuggerAlreadyOpen) {
-    yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
-  }
-
-  let { DebuggerView } = dbg;
-  let { Sources } = DebuggerView;
-
-  let item = Sources.getItemForAttachment(a => a.source.url === url);
-  if (item) {
-    return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
-  }
-
-  return Promise.reject("Couldn't find the specified source in the debugger.");
-});
--- a/browser/devtools/performance/views/details-memory-call-tree.js
+++ b/browser/devtools/performance/views/details-memory-call-tree.js
@@ -46,19 +46,23 @@ let MemoryCallTreeView = Heritage.extend
     this.emit(EVENTS.MEMORY_CALL_TREE_RENDERED);
   },
 
   /**
    * Fired on the "link" event for the call tree in this container.
    */
   _onLink: function (_, treeItem) {
     let { url, line } = treeItem.frame.getInfo();
-    viewSourceInDebugger(url, line).then(
-      () => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER),
-      () => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER));
+    gToolbox.viewSourceInDebugger(url, line).then(success => {
+      if (success) {
+        this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
+      } else {
+        this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
+      }
+    });
   },
 
   /**
    * Called when the recording is stopped and prepares data to
    * populate the call tree.
    */
   _prepareCallTree: function (allocations, { startTime, endTime }, options) {
     let samples = RecordingUtils.getSamplesFromAllocations(allocations);
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -24,35 +24,38 @@ let WaterfallView = Heritage.extend(Deta
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
     this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
     this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
 
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
+    this._onViewSource = this._onViewSource.bind(this);
 
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
     this.details.on("resize", this._onResize);
+    this.details.on("view-source", this._onViewSource);
 
     let blueprint = PerformanceController.getTimelineBlueprint();
     this.waterfall.setBlueprint(blueprint);
     this.waterfall.recalculateBounds();
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     DetailsSubview.destroy.call(this);
 
     this.waterfall.off("selected", this._onMarkerSelected);
     this.waterfall.off("unselected", this._onMarkerSelected);
     this.details.off("resize", this._onResize);
+    this.details.off("view-source", this._onViewSource);
   },
 
   /**
    * Method for handling all the set up for rendering a new waterfall.
    *
    * @param object interval [optional]
    *        The { startTime, endTime }, in milliseconds.
    */
@@ -92,10 +95,17 @@ let WaterfallView = Heritage.extend(Deta
   /**
    * Called whenever an observed pref is changed.
    */
   _onObservedPrefChange: function(_, prefName) {
     let blueprint = PerformanceController.getTimelineBlueprint();
     this.waterfall.setBlueprint(blueprint);
   },
 
+  /**
+   * Called when MarkerDetails view emits an event to view source.
+   */
+  _onViewSource: function (_, file, line) {
+    gToolbox.viewSourceInDebugger(file, line);
+  },
+
   toString: () => "[object WaterfallView]"
 });
--- a/browser/devtools/performance/views/jit-optimizations.js
+++ b/browser/devtools/performance/views/jit-optimizations.js
@@ -345,17 +345,17 @@ let JITOptimizationsView = {
     let node = el || document.createElement("span");
     node.className = "opt-url";
     let fileName;
 
     if (this._isLinkableURL(url)) {
       fileName = url.slice(url.lastIndexOf("/") + 1);
       node.classList.add("debugger-link");
       node.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + url);
-      node.addEventListener("click", () => viewSourceInDebugger(url, line));
+      node.addEventListener("click", () => gToolbox.viewSourceInDebugger(url, line));
     }
     fileName = fileName || url || "";
     node.textContent = fileName ? `@${fileName}` : "";
     return node;
   },
 
   /**
    * Updates the headers with the current frame's data.
--- a/browser/devtools/shared/moz.build
+++ b/browser/devtools/shared/moz.build
@@ -49,16 +49,17 @@ EXTRA_JS_MODULES.devtools.shared += [
     'autocomplete-popup.js',
     'devices.js',
     'doorhanger.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'observable-object.js',
     'options-view.js',
+    'source-utils.js',
     'telemetry.js',
     'theme-switching.js',
     'theme.js',
     'undo.js',
 ]
 
 EXTRA_JS_MODULES.devtools.shared.widgets += [
     'widgets/CubicBezierPresets.js',
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/source-utils.js
@@ -0,0 +1,128 @@
+/* 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";
+
+loader.lazyRequireGetter(this, "Services");
+loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
+loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
+
+/**
+ * Tries to open a Stylesheet file in the Style Editor. If the file is not found,
+ * it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the StyleEditor, as the built-in Firefox
+ * View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInStyleEditor = Task.async(function *(toolbox, sourceURL, sourceLine) {
+  let panel = yield toolbox.loadTool("styleeditor");
+
+  try {
+    let selected = panel.UI.once("editor-selected");
+    yield panel.selectStyleSheet(sourceURL, sourceLine);
+    yield toolbox.selectTool("styleeditor");
+    yield selected;
+    return true;
+  } catch (e) {
+    exports.viewSource(toolbox, sourceURL, sourceLine);
+    return false;
+  }
+});
+
+/**
+ * Tries to open a JavaScript file in the Debugger. If the file is not found,
+ * it is opened in source view instead.
+ * Returns a promise resolving to a boolean indicating whether or not
+ * the source was able to be displayed in the Debugger, as the built-in Firefox
+ * View Source is the fallback.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise<boolean>}
+ */
+exports.viewSourceInDebugger = Task.async(function *(toolbox, sourceURL, sourceLine) {
+  // If the Debugger was already open, switch to it and try to show the
+  // source immediately. Otherwise, initialize it and wait for the sources
+  // to be added first.
+  let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
+  let { panelWin: dbg } = yield toolbox.loadTool("jsdebugger");
+
+  if (!debuggerAlreadyOpen) {
+    yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
+  }
+
+  let { DebuggerView } = dbg;
+  let { Sources } = DebuggerView;
+
+  let item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
+  if (item) {
+    yield toolbox.selectTool("jsdebugger");
+    yield DebuggerView.setEditorLocation(item.attachment.source.actor, sourceLine, { noDebug: true });
+    return true;
+  }
+
+  // If not found, still attempt to open in View Source
+  exports.viewSource(toolbox, sourceURL, sourceLine);
+  return false;
+});
+
+/**
+ * Tries to open a JavaScript file in the corresponding Scratchpad.
+ *
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSourceInScratchpad = Task.async(function *(sourceURL, sourceLine) {
+  // Check for matching top level scratchpad window.
+  let wins = Services.wm.getEnumerator("devtools:scratchpad");
+
+  while (wins.hasMoreElements()) {
+    let win = wins.getNext();
+
+    if (!win.closed && win.Scratchpad.uniqueName === sourceURL) {
+      win.focus();
+      win.Scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+      return;
+    }
+  }
+
+  // For scratchpads within toolbox
+  for (let [, toolbox] of gDevTools) {
+    let scratchpadPanel = toolbox.getPanel("scratchpad");
+    if (scratchpadPanel) {
+      let { scratchpad } = scratchpadPanel;
+      if (scratchpad.uniqueName === sourceURL) {
+        toolbox.selectTool("scratchpad");
+        toolbox.raise();
+        scratchpad.editor.focus();
+        scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
+        return;
+      }
+    }
+  }
+});
+
+/**
+ * Open a link in Firefox's View Source.
+ *
+ * @param {Toolbox} toolbox
+ * @param {string} sourceURL
+ * @param {number} sourceLine
+ *
+ * @return {Promise}
+ */
+exports.viewSource = Task.async(function *(toolbox, sourceURL, sourceLine) {
+  let utils = toolbox.gViewSourceUtils;
+  utils.viewSource(sourceURL, null, toolbox.doc, sourceLine || 0);
+});
--- a/browser/devtools/shared/timeline/marker-details.js
+++ b/browser/devtools/shared/timeline/marker-details.js
@@ -217,17 +217,17 @@ MarkerDetails.prototype = {
         let text = WebConsoleUtils.abbreviateSourceURL(url) + ":" + line;
         let label = this._document.createElement("label");
         label.setAttribute("value", text);
         aNode.appendChild(label);
         hbox.appendChild(aNode);
 
         aNode.addEventListener("click", (event) => {
           event.preventDefault();
-          viewSourceInDebugger(toolbox, url, line);
+          this.emit("view-source", url, line);
         });
       }
 
       if (!displayName && !url) {
         let label = this._document.createElement("label");
         label.setAttribute("value", L10N.getStr("timeline.markerDetail.unknownFrame"));
         hbox.appendChild(label);
       }
@@ -299,39 +299,9 @@ MarkerDetails.prototype = {
     if ("causeName" in marker) {
       let cause = this.buildNameValueLabel("timeline.markerDetail.causeName", marker.causeName);
       this._parent.appendChild(cause);
     }
   },
 
 };
 
-/**
- * Opens/selects the debugger in this toolbox and jumps to the specified
- * file name and line number.
- * @param object toolbox
- *        The toolbox.
- * @param string url
- * @param number line
- */
-let viewSourceInDebugger = Task.async(function *(toolbox, url, line) {
-  // If the Debugger was already open, switch to it and try to show the
-  // source immediately. Otherwise, initialize it and wait for the sources
-  // to be added first.
-  let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
-  let { panelWin: dbg } = yield toolbox.selectTool("jsdebugger");
-
-  if (!debuggerAlreadyOpen) {
-    yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
-  }
-
-  let { DebuggerView } = dbg;
-  let { Sources } = DebuggerView;
-
-  let item = Sources.getItemForAttachment(a => a.source.url === url);
-  if (item) {
-    return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
-  }
-
-  return Promise.reject("Couldn't find the specified source in the debugger.");
-});
-
 exports.MarkerDetails = MarkerDetails;
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -1556,18 +1556,18 @@ SelectorView.prototype = {
     if (!sheet || sheet.isSystem) {
       let contentDoc = null;
       if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
         let rawNode = this.tree.viewedElement.rawNode();
         if (rawNode) {
           contentDoc = rawNode.ownerDocument;
         }
       }
-      let viewSourceUtils = inspector.viewSourceUtils;
-      viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line);
+      let toolbox = gDevTools.getToolbox(inspector.target);
+      toolbox.viewSource(rule.href, rule.line);
       return;
     }
 
     let location = promise.resolve(rule.location);
     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
       location = rule.getOriginalLocation();
     }
     location.then(({source, href, line, column}) => {
--- a/browser/devtools/styleinspector/style-inspector.js
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -105,19 +105,19 @@ RuleViewTool.prototype = {
 
   onLinkClicked: function(e, rule) {
     let sheet = rule.parentStyleSheet;
 
     // Chrome stylesheets are not listed in the style editor, so show
     // these sheets in the view source window instead.
     if (!sheet || sheet.isSystem) {
       let contentDoc = this.inspector.selection.document;
-      let viewSourceUtils = this.inspector.viewSourceUtils;
       let href = rule.nodeHref || rule.href;
-      viewSourceUtils.viewSource(href, null, contentDoc, rule.line || 0);
+      let toolbox = gDevTools.getToolbox(this.inspector.target);
+      toolbox.viewSource(href, rule.line);
       return;
     }
 
     let location = promise.resolve(rule.location);
     if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
       location = rule.getOriginalLocation();
     }
     location.then(({ source, href, line, column }) => {
--- a/browser/devtools/webconsole/hudservice.js
+++ b/browser/devtools/webconsole/hudservice.js
@@ -15,16 +15,17 @@ loader.lazyGetter(this, "Telemetry", () 
 loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame);
 loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
 loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
 loader.lazyGetter(this, "showDoorhanger", () => require("devtools/shared/doorhanger").showDoorhanger);
+loader.lazyRequireGetter(this, "sourceUtils", "devtools/shared/source-utils");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
 
 const BROWSER_CONSOLE_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no";
 
 // The preference prefix for all of the Browser Console filters.
 const BROWSER_CONSOLE_FILTER_PREFS_PREFIX = "devtools.browserconsole.filter.";
@@ -427,135 +428,73 @@ WebConsole.prototype = {
   /**
    * Open a link in Firefox's view source.
    *
    * @param string aSourceURL
    *        The URL of the file.
    * @param integer aSourceLine
    *        The line number which should be highlighted.
    */
-  viewSource: function WC_viewSource(aSourceURL, aSourceLine)
-  {
-    this.gViewSourceUtils.viewSource(aSourceURL, null,
-                                     this.iframeWindow.document, aSourceLine);
+  viewSource: function WC_viewSource(aSourceURL, aSourceLine) {
+    this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0);
   },
 
   /**
    * Tries to open a Stylesheet file related to the web page for the web console
    * instance in the Style Editor. If the file is not found, it is opened in
    * source view instead.
    *
+   * Manually handle the case where toolbox does not exist (Browser Console).
+   *
    * @param string aSourceURL
    *        The URL of the file.
    * @param integer aSourceLine
    *        The line number which you want to place the caret.
-   * TODO: This function breaks the client-server boundaries.
-   *       To be fixed in bug 793259.
    */
-  viewSourceInStyleEditor:
-  function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
-  {
+  viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) {
     let toolbox = gDevTools.getToolbox(this.target);
     if (!toolbox) {
       this.viewSource(aSourceURL, aSourceLine);
       return;
     }
-
-    gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
-      try {
-        toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
-      } catch(e) {
-        // Open view source if style editor fails.
-        this.viewSource(aSourceURL, aSourceLine);
-      }
-    });
+    toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine);
   },
 
   /**
    * Tries to open a JavaScript file related to the web page for the web console
    * instance in the Script Debugger. If the file is not found, it is opened in
    * source view instead.
    *
+   * Manually handle the case where toolbox does not exist (Browser Console).
+   *
    * @param string aSourceURL
    *        The URL of the file.
    * @param integer aSourceLine
    *        The line number which you want to place the caret.
    */
-  viewSourceInDebugger:
-  function WC_viewSourceInDebugger(aSourceURL, aSourceLine)
-  {
+  viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) {
     let toolbox = gDevTools.getToolbox(this.target);
     if (!toolbox) {
       this.viewSource(aSourceURL, aSourceLine);
       return;
     }
-
-    let showSource = ({ DebuggerView }) => {
-      let item = DebuggerView.Sources.getItemForAttachment(
-        a => a.source.url === aSourceURL
-      );
-      if (item) {
-        DebuggerView.setEditorLocation(item.attachment.source.actor, aSourceLine,
-                                       { noDebug: true }).then(() => {
-          this.ui.emit("source-in-debugger-opened");
-        });
-        return;
-      }
-      toolbox.selectTool("webconsole")
-             .then(() => this.viewSource(aSourceURL, aSourceLine));
-    }
-
-    // If the Debugger was already open, switch to it and try to show the
-    // source immediately. Otherwise, initialize it and wait for the sources
-    // to be added first.
-    let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
-    toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
-      if (debuggerAlreadyOpen) {
-        showSource(dbg);
-      } else {
-        dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
-      }
-    });
+    toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => {
+      this.ui.emit("source-in-debugger-opened");
+    })
   },
 
-
   /**
    * Tries to open a JavaScript file related to the web page for the web console
    * instance in the corresponding Scratchpad.
    *
    * @param string aSourceURL
    *        The URL of the file which corresponds to a Scratchpad id.
    */
-  viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL)
-  {
-    // Check for matching top level Scratchpad window.
-    let wins = Services.wm.getEnumerator("devtools:scratchpad");
-
-    while (wins.hasMoreElements()) {
-      let win = wins.getNext();
-
-      if (!win.closed && win.Scratchpad.uniqueName === aSourceURL) {
-        win.focus();
-        return;
-      }
-    }
-
-    // Check for matching Scratchpad toolbox tab.
-    for (let [, toolbox] of gDevTools) {
-      let scratchpadPanel = toolbox.getPanel("scratchpad");
-      if (scratchpadPanel) {
-        let { scratchpad } = scratchpadPanel;
-        if (scratchpad.uniqueName === aSourceURL) {
-          toolbox.selectTool("scratchpad");
-          toolbox.raise();
-          scratchpad.editor.focus();
-          return;
-        }
-      }
-    }
+  viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL, aSourceLine) {
+    sourceUtils.viewSourceInScratchpad(aSourceURL, aSourceLine);
   },
 
   /**
    * Retrieve information about the JavaScript debugger's stackframes list. This
    * is used to allow the Web Console to evaluate code in the selected
    * stackframe.
    *
    * @return object|null
--- a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -62,29 +62,29 @@ function test()
 
     let title = locationNode.getAttribute("title");
     info("location node title: " + title);
     isnot(title.indexOf(" -> "), -1, "error comes from a subscript");
 
     let viewSource = browserconsole.viewSource;
     let URL = null;
     let clickPromise = promise.defer();
-    browserconsole.viewSource = (aURL) => {
-      info("browserconsole.viewSource() was invoked: " + aURL);
+    browserconsole.viewSourceInDebugger = (aURL) => {
+      info("browserconsole.viewSourceInDebugger() was invoked: " + aURL);
       URL = aURL;
       clickPromise.resolve(null);
     };
 
     msg.scrollIntoView();
     EventUtils.synthesizeMouse(locationNode, 2, 2, {},
                                browserconsole.iframeWindow);
 
     info("wait for click on locationNode");
     yield clickPromise;
 
     info("view-source url: " + URL);
     ok(URL, "we have some source URL after the click");
     isnot(URL.indexOf("toolbox.js"), -1, "we have the expected view source URL");
     is(URL.indexOf("->"), -1, "no -> in the URL given to view-source");
 
-    browserconsole.viewSource = viewSource;
+    browserconsole.viewSourceInDebugger = viewSource;
   }
 }
--- a/browser/devtools/webconsole/test/browser_console_error_source_click.js
+++ b/browser/devtools/webconsole/test/browser_console_error_source_click.js
@@ -47,27 +47,27 @@ function test()
       ],
     }).then(onMessageFound);
   }
 
   function onMessageFound(results)
   {
     let viewSource = hud.viewSource;
     let viewSourceCalled = false;
-    hud.viewSource = () => viewSourceCalled = true;
+    hud.viewSourceInDebugger = () => viewSourceCalled = true;
 
     for (let result of results) {
       viewSourceCalled = false;
 
       let msg = [...results[0].matched][0];
       ok(msg, "message element found for: " + result.text);
       let locationNode = msg.querySelector(".message-location");
       ok(locationNode, "message location element found");
 
       EventUtils.synthesizeMouse(locationNode, 2, 2, {}, hud.iframeWindow);
 
       ok(viewSourceCalled, "view source opened");
     }
 
-    hud.viewSource = viewSource;
+    hud.viewSourceInDebugger = viewSource;
     finishTest();
   }
 }
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -2715,17 +2715,17 @@ WebConsoleFrame.prototype = {
     }
     locationNode.setAttribute("title", url);
     locationNode.className = "message-location theme-link devtools-monospace";
 
     // Make the location clickable.
     let onClick = () => {
       let target = locationNode.target;
       if (target == "scratchpad" || isScratchpad) {
-        this.owner.viewSourceInScratchpad(url);
+        this.owner.viewSourceInScratchpad(url, line);
         return;
       }
 
       let category = locationNode.parentNode.category;
       if (target == "styleeditor" || category == CATEGORY_CSS) {
         this.owner.viewSourceInStyleEditor(fullURL, line);
       }
       else if (target == "jsdebugger" ||