Bug 1067325 - Extract view source core to support tabs. r=mconley
authorJ. Ryan Stinnett <jryans@gmail.com>
Sat, 23 May 2015 18:17:50 -0500
changeset 245408 197f69574d75ddbe475dc477c054ab05cbd1acec
parent 245407 650b90c446dc6f4def545406b7f24ad79223a462
child 245409 5caf0a502003315c08de27e77f264f22240eeff3
push id28805
push userphilringnalda@gmail.com
push dateSun, 24 May 2015 19:15:34 +0000
treeherdermozilla-central@b6623a27fa64 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1067325
milestone41.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 1067325 - Extract view source core to support tabs. r=mconley
browser/app/profile/firefox.js
browser/base/content/browser.js
toolkit/components/viewsource/ViewSourceBrowser.jsm
toolkit/components/viewsource/content/viewPartialSource.js
toolkit/components/viewsource/content/viewPartialSource.xul
toolkit/components/viewsource/content/viewSource-content.js
toolkit/components/viewsource/content/viewSource.js
toolkit/components/viewsource/content/viewSource.xul
toolkit/components/viewsource/content/viewSourceUtils.js
toolkit/components/viewsource/jar.mn
toolkit/components/viewsource/moz.build
toolkit/components/viewsource/test/browser/browser_gotoline.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1916,8 +1916,10 @@ pref("browser.reader.detectedFirstArticl
 pref("reader.parse-node-limit", 0);
 
 pref("browser.pocket.enabled", true);
 pref("browser.pocket.api", "api.getpocket.com");
 pref("browser.pocket.site", "getpocket.com");
 pref("browser.pocket.oAuthConsumerKey", "40249-e88c401e1b1f2242d9e441c4");
 pref("browser.pocket.useLocaleList", true);
 pref("browser.pocket.enabledLocales", "en-US de es-ES ja ja-JP-mac ru");
+
+pref("view_source.tab", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -981,16 +981,17 @@ var gBrowserInit = {
     DevEdition.init();
     AboutPrivateBrowsingListener.init();
 
     let mm = window.getGroupMessageManager("browsers");
     mm.loadFrameScript("chrome://browser/content/tab-content.js", true);
     mm.loadFrameScript("chrome://browser/content/content.js", true);
     mm.loadFrameScript("chrome://browser/content/content-UITour.js", true);
     mm.loadFrameScript("chrome://global/content/manifestMessages.js", true);
+    mm.loadFrameScript("chrome://global/content/viewSource-content.js", true);
 
     window.messageManager.addMessageListener("Browser:LoadURI", RedirectLoad);
 
     // initialize observers and listeners
     // and give C++ access to gBrowser
     XULBrowserWindow.init();
     window.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(nsIWebNavigation)
@@ -2317,21 +2318,21 @@ function readFromClipboard()
  * @param aArgsOrDocument
  *        Either an object or a Document. Passing a Document is deprecated,
  *        and is not supported with e10s. This function will throw if
  *        aArgsOrDocument is a CPOW.
  *
  *        If aArgsOrDocument is an object, that object can take the
  *        following properties:
  *
- *        browser:
+ *        URL (required):
+ *          A string URL for the page we'd like to view the source of.
+ *        browser (optional):
  *          The browser containing the document that we would like to view the
- *          source of.
- *        URL:
- *          A string URL for the page we'd like to view the source of.
+ *          source of. This is required if outerWindowID is passed.
  *        outerWindowID (optional):
  *          The outerWindowID of the content window containing the document that
  *          we want to view the source of. You only need to provide this if you
  *          want to attempt to retrieve the document source from the network
  *          cache.
  *        lineNumber (optional):
  *          The line number to focus on once the source is loaded.
  */
@@ -2354,17 +2355,28 @@ function BrowserViewSourceOfDocument(aAr
     let outerWindowID = requestor.getInterface(Ci.nsIDOMWindowUtils)
                                  .outerWindowID;
     let URL = browser.currentURI.spec;
     args = { browser, outerWindowID, URL };
   } else {
     args = aArgsOrDocument;
   }
 
-  top.gViewSourceUtils.viewSource(args);
+  let inTab = Services.prefs.getBoolPref("view_source.tab");
+  if (inTab) {
+    let viewSourceURL = `view-source:${args.URL}`;
+    let tab = gBrowser.loadOneTab(viewSourceURL, {
+      relatedToCurrent: true,
+      inBackground: false
+    });
+    args.viewSourceBrowser = gBrowser.getBrowserForTab(tab);
+    top.gViewSourceUtils.viewSourceInBrowser(args);
+  } else {
+    top.gViewSourceUtils.viewSource(args);
+  }
 }
 
 /**
  * Opens the View Source dialog for the source loaded in the root
  * top-level document of the browser. This is really just a
  * convenience wrapper around BrowserViewSourceOfDocument.
  *
  * @param browser
new file mode 100644
--- /dev/null
+++ b/toolkit/components/viewsource/ViewSourceBrowser.jsm
@@ -0,0 +1,161 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+
+/* 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 { utils: Cu, interfaces: Ci, classes: Cc } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+  "resource://gre/modules/Deprecated.jsm");
+
+this.EXPORTED_SYMBOLS = ["ViewSourceBrowser"];
+
+/**
+ * ViewSourceBrowser manages the view source <browser> from the chrome side.
+ * It's companion frame script, viewSource-content.js, needs to be loaded as a
+ * frame script into the browser being managed.
+ *
+ * For a view source window using viewSource.xul, the script viewSource.js in
+ * the window extends an instance of this with more window specific functions.
+ * The page script takes care of loading the companion frame script.
+ *
+ * For a view source tab (or some other non-window case), an instance of this is
+ * created by viewSourceUtils.js to wrap the <browser>.  The caller that manages
+ * the <browser> is responsible for ensuring the companion frame script has been
+ * loaded.
+ */
+this.ViewSourceBrowser = function ViewSourceBrowser(aBrowser) {
+  this._browser = aBrowser;
+  this.init();
+}
+
+ViewSourceBrowser.prototype = {
+  /**
+   * The <browser> that will be displaying the view source content.
+   */
+  get browser() {
+    return this._browser;
+  },
+
+  /**
+   * These are the messages that ViewSourceBrowser will listen for
+   * from the frame script it injects. Any message names added here
+   * will automatically have ViewSourceBrowser listen for those messages,
+   * and remove the listeners on teardown.
+   */
+  // TODO: Some messages will appear here in a later patch
+  messages: [
+  ],
+
+  /**
+   * This should be called as soon as the script loads. When this function
+   * executes, we can assume the DOM content has not yet loaded.
+   */
+  init() {
+    this.messages.forEach((msgName) => {
+      this.mm.addMessageListener(msgName, this);
+    });
+  },
+
+  /**
+   * This should be called when the window is closing. This function should
+   * clean up event and message listeners.
+   */
+  uninit() {
+    this.messages.forEach((msgName) => {
+      this.mm.removeMessageListener(msgName, this);
+    });
+  },
+
+  /**
+   * Anything added to the messages array will get handled here, and should
+   * get dispatched to a specific function for the message name.
+   */
+  receiveMessage(message) {
+    let data = message.data;
+
+    // TODO: Some messages will appear here in a later patch
+    switch(message.name) {
+    }
+  },
+
+  /**
+   * Getter for the message manager of the view source browser.
+   */
+  get mm() {
+    return this.browser.messageManager;
+  },
+
+  /**
+   * Send a message to the view source browser.
+   */
+  sendAsyncMessage(...args) {
+    this.browser.messageManager.sendAsyncMessage(...args);
+  },
+
+  /**
+   * Loads the source for a URL while applying some optional features if
+   * enabled.
+   *
+   * For the viewSource.xul window, this is called by onXULLoaded above.
+   * For view source in a specific browser, this is manually called after
+   * this object is constructed.
+   *
+   * This takes a single object argument containing:
+   *
+   *   URL (required):
+   *     A string URL for the page we'd like to view the source of.
+   *   browser:
+   *     The browser containing the document that we would like to view the
+   *     source of. This argument is optional if outerWindowID is not passed.
+   *   outerWindowID (optional):
+   *     The outerWindowID of the content window containing the document that
+   *     we want to view the source of. This is the only way of attempting to
+   *     load the source out of the network cache.
+   *   lineNumber (optional):
+   *     The line number to focus on once the source is loaded.
+   */
+  loadViewSource({ URL, browser, outerWindowID, lineNumber }) {
+    if (!URL) {
+      throw new Error("Must supply a URL when opening view source.");
+    }
+
+    if (browser) {
+      // If we're dealing with a remote browser, then the browser
+      // for view source needs to be remote as well.
+      this.updateBrowserRemoteness(browser.isRemoteBrowser);
+    } else {
+      if (outerWindowID) {
+        throw new Error("Must supply the browser if passing the outerWindowID");
+      }
+    }
+
+    this.sendAsyncMessage("ViewSource:LoadSource",
+                          { URL, outerWindowID, lineNumber });
+  },
+
+  /**
+   * Updates the "remote" attribute of the view source browser. This
+   * will remove the browser from the DOM, and then re-add it in the
+   * same place it was taken from.
+   *
+   * @param shouldBeRemote
+   *        True if the browser should be made remote. If the browsers
+   *        remoteness already matches this value, this function does
+   *        nothing.
+   */
+  updateBrowserRemoteness(shouldBeRemote) {
+    if (this.browser.isRemoteBrowser != shouldBeRemote) {
+      // In this base case, where we are handed a <browser> someone else is
+      // managing, we don't know for sure that it's safe to toggle remoteness.
+      // For view source in a window, this is overridden to actually do the
+      // flip if needed.
+      throw new Error("View source browser's remoteness mismatch");
+    }
+  },
+};
--- a/toolkit/components/viewsource/content/viewPartialSource.js
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -35,21 +35,16 @@ function onLoadViewPartialSource()
           .setAttribute("checked",
                         Services.prefs.getBoolPref("view_source.syntax_highlight"));
 
   if (window.arguments[3] == 'selection')
     viewPartialSourceForSelection(window.arguments[2]);
   else
     viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
 
-  gBrowser.droppedLinkHandler = function (event, url, name) {
-    viewSource(url)
-    event.preventDefault();
-  }
-
   window.content.focus();
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 // view-source of a selection with the special effect of remapping the selection
 // to the underlying view-source output
 function viewPartialSourceForSelection(selection)
 {
@@ -169,17 +164,17 @@ function viewPartialSourceForSelection(s
   // before drawing the selection.
   if (canDrawSelection) {
     window.document.getElementById("content").addEventListener("load", drawSelection, true);
   }
 
   // all our content is held by the data:URI and URIs are internally stored as utf-8 (see nsIURI.idl)
   var loadFlags = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE;
   var referrerPolicy = Components.interfaces.nsIHttpChannel.REFERRER_POLICY_DEFAULT;
-  ViewSourceChrome.webNav.loadURIWithOptions((isHTML ?
+  viewSourceChrome.webNav.loadURIWithOptions((isHTML ?
                                               "view-source:data:text/html;charset=utf-8," :
                                               "view-source:data:application/xml;charset=utf-8,")
                                              + encodeURIComponent(tmpNode.innerHTML),
                                              loadFlags,
                                              null, referrerPolicy,  // referrer
                                              null, null,  // postData, headers
                                              Services.io.newURI(doc.baseURI, null, null));
 }
--- a/toolkit/components/viewsource/content/viewPartialSource.xul
+++ b/toolkit/components/viewsource/content/viewPartialSource.xul
@@ -45,19 +45,19 @@
   <command id="cmd_close" oncommand="window.close();"/>
   <commandset id="editMenuCommands"/>
   <command id="cmd_find"
            oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
   <command id="cmd_findAgain"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
   <command id="cmd_findPrevious"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
-  <command id="cmd_goToLine" oncommand="ViewSourceChrome.promptAndGoToLine();" disabled="true"/>
-  <command id="cmd_highlightSyntax" oncommand="ViewSourceChrome.toggleSyntaxHighlighting();"/>
-  <command id="cmd_wrapLongLines" oncommand="ViewSourceChrome.toggleWrapping();"/>
+  <command id="cmd_goToLine" oncommand="viewSourceChrome.promptAndGoToLine();" disabled="true"/>
+  <command id="cmd_highlightSyntax" oncommand="viewSourceChrome.toggleSyntaxHighlighting();"/>
+  <command id="cmd_wrapLongLines" oncommand="viewSourceChrome.toggleWrapping();"/>
   <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
   <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
   <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
 
   <keyset id="editMenuKeys"/>
   <keyset id="viewSourceKeys">
     <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
     <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
@@ -75,21 +75,21 @@
 
   <menupopup id="viewSourceContextMenu">
     <menuitem id="cMenu_findAgain"/>
     <menuseparator/>
     <menuitem id="cMenu_copy"/>
     <menuitem id="context-copyLink"
               label="&copyLinkCmd.label;"
               accesskey="&copyLinkCmd.accesskey;"
-              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+              oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuitem id="context-copyEmail"
               label="&copyEmailCmd.label;"
               accesskey="&copyEmailCmd.accesskey;"
-              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+              oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuseparator/>
     <menuitem id="cMenu_selectAll"/>
   </menupopup>
 
   <!-- Menu -->
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
--- a/toolkit/components/viewsource/content/viewSource-content.js
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -46,16 +46,26 @@ let ViewSourceContent = {
    * and removed on pagehide. When the initial about:blank is transitioned
    * away from, a pagehide is fired without us having attached ourselves
    * first. We use this boolean to keep track of whether or not we're
    * attached, so we don't attempt to remove our listener when it's not
    * yet there (which throws).
    */
   selectionListenerAttached: false,
 
+  get isViewSource() {
+    let uri = content.document.documentURI;
+    return uri.startsWith("view-source:");
+  },
+
+  get isAboutBlank() {
+    let uri = content.document.documentURI;
+    return uri == "about:blank";
+  },
+
   /**
    * This should be called as soon as this frame script has loaded.
    */
   init() {
     this.messages.forEach((msgName) => {
       addMessageListener(msgName, this);
     });
 
@@ -88,16 +98,19 @@ let ViewSourceContent = {
     }
   },
 
   /**
    * Anything added to the messages array will get handled here, and should
    * get dispatched to a specific function for the message name.
    */
   receiveMessage(msg) {
+    if (!this.isViewSource && !this.isAboutBlank) {
+      return;
+    }
     let data = msg.data;
     let objects = msg.objects;
     switch(msg.name) {
       case "ViewSource:LoadSource":
         this.viewSource(data.URL, data.outerWindowID, data.lineNumber,
                         data.shouldWrap);
         break;
       case "ViewSource:LoadSourceDeprecated":
@@ -119,16 +132,19 @@ let ViewSourceContent = {
     }
   },
 
   /**
    * Any events should get handled here, and should get dispatched to
    * a specific function for the event type.
    */
   handleEvent(event) {
+    if (!this.isViewSource) {
+      return;
+    }
     switch(event.type) {
       case "pagehide":
         this.onPageHide(event);
         break;
       case "pageshow":
         this.onPageShow(event);
         break;
       case "click":
@@ -245,18 +261,23 @@ let ViewSourceContent = {
   loadSource(URL, pageDescriptor, lineNumber, forcedCharSet) {
     const viewSrcURL = "view-source:" + URL;
     let loadFromURL = false;
 
     if (forcedCharSet) {
       docShell.charset = forcedCharSet;
     }
 
-    if (lineNumber) {
+    if (lineNumber && lineNumber > 0) {
       let doneLoading = (event) => {
+        // Ignore possible initial load of about:blank
+        if (this.isAboutBlank ||
+            !content.document.body) {
+          return;
+        }
         this.goToLine(lineNumber);
         removeEventListener("pageshow", doneLoading);
       };
 
       addEventListener("pageshow", doneLoading);
     }
 
     if (!pageDescriptor) {
@@ -336,21 +357,22 @@ let ViewSourceContent = {
 
   /**
    * Handler for the pageshow event.
    *
    * @param event
    *        The pageshow event being handled.
    */
   onPageShow(event) {
-    content.getSelection()
-           .QueryInterface(Ci.nsISelectionPrivate)
-           .addSelectionListener(this);
-    this.selectionListenerAttached = true;
-
+    let selection = content.getSelection();
+    if (selection) {
+      selection.QueryInterface(Ci.nsISelectionPrivate)
+               .addSelectionListener(this);
+      this.selectionListenerAttached = true;
+    }
     content.focus();
     sendAsyncMessage("ViewSource:SourceLoaded");
   },
 
   /**
    * Handler for the pagehide event.
    *
    * @param event
--- a/toolkit/components/viewsource/content/viewSource.js
+++ b/toolkit/components/viewsource/content/viewSource.js
@@ -2,16 +2,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 { utils: Cu, interfaces: Ci, classes: Cc } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ViewSourceBrowser.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
   "resource://gre/modules/CharsetMenu.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
   "resource://gre/modules/Deprecated.jsm");
 
@@ -26,19 +27,35 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       return null;
     delete window[name];
     return window[name] = element;
   });
 });
 
 /**
  * ViewSourceChrome is the primary interface for interacting with
- * the view source browser. It initializes itself on script load.
+ * the view source browser from a self-contained window.  It extends
+ * ViewSourceBrowser with additional things needed inside the special window.
+ *
+ * It initializes itself on script load.
  */
-let ViewSourceChrome = {
+function ViewSourceChrome() {
+  ViewSourceBrowser.call(this);
+}
+
+ViewSourceChrome.prototype = {
+  __proto__: ViewSourceBrowser.prototype,
+
+  /**
+   * The <browser> that will be displaying the view source content.
+   */
+  get browser() {
+    return gBrowser;
+  },
+
   /**
    * Holds the value of the last line found via the "Go to line"
    * command, to pre-populate the prompt the next time it is
    * opened.
    */
   lastLineFound: null,
 
   /**
@@ -50,71 +67,64 @@ let ViewSourceChrome = {
   contextMenuData: {},
 
   /**
    * These are the messages that ViewSourceChrome will listen for
    * from the frame script it injects. Any message names added here
    * will automatically have ViewSourceChrome listen for those messages,
    * and remove the listeners on teardown.
    */
-  messages: [
+  messages: ViewSourceBrowser.prototype.messages.concat([
     "ViewSource:SourceLoaded",
     "ViewSource:SourceUnloaded",
     "ViewSource:Close",
     "ViewSource:OpenURL",
     "ViewSource:GoToLine:Success",
     "ViewSource:GoToLine:Failed",
     "ViewSource:UpdateStatus",
     "ViewSource:ContextMenuOpening",
-  ],
+  ]),
 
   /**
-   * This should be called as soon as the script loads. When this function
-   * executes, we can assume the DOM content has not yet loaded.
+   * This called via ViewSourceBrowser's constructor.  This should be called as
+   * soon as the script loads.  When this function executes, we can assume the
+   * DOM content has not yet loaded.
    */
   init() {
-    // We use the window message manager so that if we switch remoteness of the
-    // browser (which we might do if we're attempting to load the document
-    // source out of the network cache), we automatically re-load the frame
-    // script.
-    let wMM = window.messageManager;
-    wMM.loadFrameScript("chrome://global/content/viewSource-content.js", true);
-    this.messages.forEach((msgName) => {
-      wMM.addMessageListener(msgName, this);
-    });
+    this.mm.loadFrameScript("chrome://global/content/viewSource-content.js", true);
 
     this.shouldWrap = Services.prefs.getBoolPref("view_source.wrap_long_lines");
     this.shouldHighlight =
       Services.prefs.getBoolPref("view_source.syntax_highlight");
 
     addEventListener("load", this);
     addEventListener("unload", this);
     addEventListener("AppCommand", this, true);
     addEventListener("MozSwipeGesture", this, true);
+
+    ViewSourceBrowser.prototype.init.call(this);
   },
 
   /**
    * This should be called when the window is closing. This function should
    * clean up event and message listeners.
    */
   uninit() {
-    let wMM = window.messageManager;
-    this.messages.forEach((msgName) => {
-      wMM.removeMessageListener(msgName, this);
-    });
+    ViewSourceBrowser.prototype.uninit.call(this);
 
     // "load" event listener is removed in its handler, to
     // ensure we only fire it once.
     removeEventListener("unload", this);
     removeEventListener("AppCommand", this, true);
     removeEventListener("MozSwipeGesture", this, true);
     gContextMenu.removeEventListener("popupshowing", this);
     gContextMenu.removeEventListener("popuphidden", this);
-    Services.els.removeSystemEventListener(gBrowser, "dragover", this, true);
-    Services.els.removeSystemEventListener(gBrowser, "drop", this, true);
+    Services.els.removeSystemEventListener(this.browser, "dragover", this,
+                                           true);
+    Services.els.removeSystemEventListener(this.browser, "drop", this, true);
   },
 
   /**
    * Anything added to the messages array will get handled here, and should
    * get dispatched to a specific function for the message name.
    */
   receiveMessage(message) {
     let data = message.data;
@@ -138,17 +148,17 @@ let ViewSourceChrome = {
       case "ViewSource:GoToLine:Success":
         this.onGoToLineSuccess(data.lineNumber);
         break;
       case "ViewSource:UpdateStatus":
         this.updateStatus(data.label);
         break;
       case "ViewSource:ContextMenuOpening":
         this.onContextMenuOpening(data.isLink, data.isEmail, data.href);
-        if (gBrowser.isRemoteBrowser) {
+        if (this.browser.isRemoteBrowser) {
           this.openContextMenu(data.screenX, data.screenY);
         }
         break;
     }
   },
 
   /**
    * Any events should get handled here, and should get dispatched to
@@ -183,45 +193,52 @@ let ViewSourceChrome = {
     }
   },
 
   /**
    * Getter that returns whether or not the view source browser
    * has history enabled on it.
    */
   get historyEnabled() {
-    return !gBrowser.hasAttribute("disablehistory");
+    return !this.browser.hasAttribute("disablehistory");
   },
 
   /**
-   * Getter for the message manager of the view source browser.
+   * Getter for the message manager used to communicate with the view source
+   * browser.
+   *
+   * In this window version of view source, we use the window message manager
+   * for loading scripts and listening for messages so that if we switch
+   * remoteness of the browser (which we might do if we're attempting to load
+   * the document source out of the network cache), we automatically re-load
+   * the frame script.
    */
   get mm() {
-    return gBrowser.messageManager;
+    return window.messageManager;
   },
 
   /**
    * Getter for the nsIWebNavigation of the view source browser.
    */
   get webNav() {
-    return gBrowser.webNavigation;
+    return this.browser.webNavigation;
   },
 
   /**
    * Send the browser forward in its history.
    */
   goForward() {
-    gBrowser.goForward();
+    this.browser.goForward();
   },
 
   /**
    * Send the browser backward in its history.
    */
   goBack() {
-    gBrowser.goBack();
+    this.browser.goBack();
   },
 
   /**
    * This should be called once when the DOM has finished loading. Here we
    * set the state of various menu items, and add event listeners to
    * DOM nodes.
    *
    * This is also the place where we handle any arguments that have been
@@ -262,113 +279,88 @@ let ViewSourceChrome = {
     let highlightMenuItem = document.getElementById("menu_highlightSyntax");
     if (this.shouldHighlight) {
       highlightMenuItem.setAttribute("checked", "true");
     }
 
     gContextMenu.addEventListener("popupshowing", this);
     gContextMenu.addEventListener("popuphidden", this);
 
-    Services.els.addSystemEventListener(gBrowser, "dragover", this, true);
-    Services.els.addSystemEventListener(gBrowser, "drop", this, true);
+    Services.els.addSystemEventListener(this.browser, "dragover", this, true);
+    Services.els.addSystemEventListener(this.browser, "drop", this, true);
 
     if (!this.historyEnabled) {
       // Disable the BACK and FORWARD commands and hide the related menu items.
       let viewSourceNavigation = document.getElementById("viewSourceNavigation");
       if (viewSourceNavigation) {
         viewSourceNavigation.setAttribute("disabled", "true");
         viewSourceNavigation.setAttribute("hidden", "true");
       }
     }
 
-    // This will only work with non-remote browsers. See bug 1158377.
-    gBrowser.droppedLinkHandler = function (event, url, name) {
-      ViewSourceChrome.loadURL(url);
-      event.preventDefault();
-    };
-
     // We require the first argument to do any loading of source.
     // otherwise, we're done.
     if (!window.arguments[0]) {
       return;
     }
 
     if (typeof window.arguments[0] == "string") {
       // We're using the deprecated API
-      return ViewSourceChrome._loadViewSourceDeprecated();
+      return this._loadViewSourceDeprecated(window.arguments);
     }
 
     // We're using the modern API, which allows us to view the
     // source of documents from out of process browsers.
     let args = window.arguments[0];
-
-    if (!args.URL) {
-      throw new Error("Must supply a URL when opening view source.");
-    }
-
-    if (args.browser) {
-      // If we're dealing with a remote browser, then the browser
-      // for view source needs to be remote as well.
-      this.updateBrowserRemoteness(args.browser.isRemoteBrowser);
-    } else {
-      if (args.outerWindowID) {
-        throw new Error("Must supply the browser if passing the outerWindowID");
-      }
-    }
-
-    this.mm.sendAsyncMessage("ViewSource:LoadSource", {
-      URL: args.URL,
-      outerWindowID: args.outerWindowID,
-      lineNumber: args.lineNumber,
-    });
+    this.loadViewSource(args);
   },
 
   /**
    * This is the deprecated API for viewSource.xul, for old-timer consumers.
    * This API might eventually go away.
    */
-  _loadViewSourceDeprecated() {
+  _loadViewSourceDeprecated(aArguments) {
     Deprecated.warning("The arguments you're passing to viewSource.xul " +
                        "are using an out-of-date API.",
                        "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
     // Parse the 'arguments' supplied with the dialog.
     //    arg[0] - URL string.
     //    arg[1] - Charset value in the form 'charset=xxx'.
     //    arg[2] - Page descriptor used to load content from the cache.
     //    arg[3] - Line number to go to.
     //    arg[4] - Whether charset was forced by the user
 
-    if (window.arguments[3] == "selection" ||
-        window.arguments[3] == "mathml") {
+    if (aArguments[3] == "selection" ||
+        aArguments[3] == "mathml") {
       // viewPartialSource.js will take care of loading the content.
       return;
     }
 
-    if (window.arguments[2]) {
-      let pageDescriptor = window.arguments[2];
+    if (aArguments[2]) {
+      let pageDescriptor = aArguments[2];
       if (Cu.isCrossProcessWrapper(pageDescriptor)) {
         throw new Error("Cannot pass a CPOW as the page descriptor to viewSource.xul.");
       }
     }
 
-    if (gBrowser.isRemoteBrowser) {
+    if (this.browser.isRemoteBrowser) {
       throw new Error("Deprecated view source API should not use a remote browser.");
     }
 
     let forcedCharSet;
-    if (window.arguments[4] && window.arguments[1].startsWith("charset=")) {
-      forcedCharSet = window.arguments[1].split("=")[1];
+    if (aArguments[4] && aArguments[1].startsWith("charset=")) {
+      forcedCharSet = aArguments[1].split("=")[1];
     }
 
-    gBrowser.messageManager.sendAsyncMessage("ViewSource:LoadSourceDeprecated", {
-      URL: window.arguments[0],
-      lineNumber: window.arguments[3],
+    this.sendAsyncMessage("ViewSource:LoadSourceDeprecated", {
+      URL: aArguments[0],
+      lineNumber: aArguments[3],
       forcedCharSet,
     }, {
-      pageDescriptor: window.arguments[2],
+      pageDescriptor: aArguments[2],
     });
   },
 
   /**
    * Handler for the AppCommand event.
    *
    * @param event
    *        The AppCommand event being handled.
@@ -415,17 +407,17 @@ let ViewSourceChrome = {
    */
   onSourceLoaded() {
     document.getElementById("cmd_goToLine").removeAttribute("disabled");
 
     if (this.historyEnabled) {
       this.updateCommands();
     }
 
-    gBrowser.focus();
+    this.browser.focus();
   },
 
   /**
    * Called as soon as the frame script reports that some source
    * code has been unloaded from the browser.
    */
   onSourceUnloaded() {
     // Disable "go to line" while reloading due to e.g. change of charset
@@ -441,23 +433,24 @@ let ViewSourceChrome = {
    *        The click event on a character set menuitem.
    */
   onSetCharacterSet(event) {
     if (event.target.hasAttribute("charset")) {
       let charset = event.target.getAttribute("charset");
 
       // If we don't have history enabled, we have to do a reload in order to
       // show the character set change. See bug 136322.
-      this.mm.sendAsyncMessage("ViewSource:SetCharacterSet", {
+      this.sendAsyncMessage("ViewSource:SetCharacterSet", {
         charset: charset,
         doPageLoad: this.historyEnabled,
       });
 
       if (this.historyEnabled) {
-        gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+        this.browser
+            .reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
       }
     }
   },
 
   /**
    * Called from the frame script when the context menu is about to
    * open. This tells ViewSourceChrome things about the item that
    * the context menu is being opened on. This should be called before
@@ -578,17 +571,17 @@ let ViewSourceChrome = {
   /**
    * Loads the source of a URL. This will probably end up hitting the
    * network.
    *
    * @param URL
    *        A URL string to be opened in the view source browser.
    */
   loadURL(URL) {
-    this.mm.sendAsyncMessage("ViewSource:LoadSource", { URL });
+    this.sendAsyncMessage("ViewSource:LoadSource", { URL });
   },
 
   /**
    * Updates any commands that are dependant on command broadcasters.
    */
   updateCommands() {
     let backBroadcaster = document.getElementById("Browser:Back");
     let forwardBroadcaster = document.getElementById("Browser:Forward");
@@ -651,17 +644,17 @@ let ViewSourceChrome = {
 
   /**
    * Go to a particular line of the source code. This act is asynchronous.
    *
    * @param lineNumber
    *        The line number to try to go to to.
    */
   goToLine(lineNumber) {
-    this.mm.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
+    this.sendAsyncMessage("ViewSource:GoToLine", { lineNumber });
   },
 
   /**
    * Called when the frame script reports that a line was successfully gotten
    * to.
    *
    * @param lineNumber
    *        The line number that we successfully got to.
@@ -685,18 +678,18 @@ let ViewSourceChrome = {
                           gViewSourceBundle.getString("outOfRangeText"));
     this.promptAndGoToLine();
   },
 
   /**
    * Reloads the browser, bypassing the network cache.
    */
   reload() {
-    gBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
-                             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
+    this.browser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
+                                 Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE);
   },
 
   /**
    * Closes the view source window.
    */
   close() {
     window.close();
   },
@@ -705,78 +698,78 @@ let ViewSourceChrome = {
    * Called when the user clicks on the "Wrap Long Lines" menu item, and
    * flips the user preference for wrapping long lines in the view source
    * browser.
    */
   toggleWrapping() {
     this.shouldWrap = !this.shouldWrap;
     Services.prefs.setBoolPref("view_source.wrap_long_lines",
                                this.shouldWrap);
-    this.mm.sendAsyncMessage("ViewSource:ToggleWrapping");
+    this.sendAsyncMessage("ViewSource:ToggleWrapping");
   },
 
   /**
    * Called when the user clicks on the "Syntax Highlighting" menu item, and
    * flips the user preference for wrapping long lines in the view source
    * browser.
    */
   toggleSyntaxHighlighting() {
     this.shouldHighlight = !this.shouldHighlight;
     // We can't flip this value in the child, since prefs are read-only there.
     // We flip it here, and then cause a reload in the child to make the change
     // occur.
     Services.prefs.setBoolPref("view_source.syntax_highlight",
                                this.shouldHighlight);
-    this.mm.sendAsyncMessage("ViewSource:ToggleSyntaxHighlighting");
+    this.sendAsyncMessage("ViewSource:ToggleSyntaxHighlighting");
   },
 
   /**
    * Updates the "remote" attribute of the view source browser. This
    * will remove the browser from the DOM, and then re-add it in the
    * same place it was taken from.
    *
    * @param shouldBeRemote
    *        True if the browser should be made remote. If the browsers
    *        remoteness already matches this value, this function does
    *        nothing.
    */
   updateBrowserRemoteness(shouldBeRemote) {
-    if (gBrowser.isRemoteBrowser == shouldBeRemote) {
+    if (this.browser.isRemoteBrowser == shouldBeRemote) {
       return;
     }
 
-    let parentNode = gBrowser.parentNode;
-    let nextSibling = gBrowser.nextSibling;
+    let parentNode = this.browser.parentNode;
+    let nextSibling = this.browser.nextSibling;
 
-    gBrowser.remove();
+    this.browser.remove();
     if (shouldBeRemote) {
-      gBrowser.setAttribute("remote", "true");
+      this.browser.setAttribute("remote", "true");
     } else {
-      gBrowser.removeAttribute("remote");
+      this.browser.removeAttribute("remote");
     }
     // If nextSibling was null, this will put the browser at
     // the end of the list.
-    parentNode.insertBefore(gBrowser, nextSibling);
+    parentNode.insertBefore(this.browser, nextSibling);
 
     if (shouldBeRemote) {
       // We're going to send a message down to the remote browser
       // to load the source content - however, in order for the
       // contentWindowAsCPOW and contentDocumentAsCPOW values on
       // the remote browser to be set, we must set up the
       // RemoteWebProgress, which is lazily loaded. We only need
       // contentWindowAsCPOW for the printing support, and this
       // should go away once bug 1146454 is fixed, since we can
-      // then just pass the outerWindowID of the gBrowser to
+      // then just pass the outerWindowID of the this.browser to
       // PrintUtils.
-      gBrowser.webProgress;
+      this.browser.webProgress;
     }
   },
 };
 
-ViewSourceChrome.init();
+let viewSourceChrome = new ViewSourceChrome();
 
 /**
  * PrintUtils uses this to make Print Preview work.
  */
 let PrintPreviewListener = {
   getPrintPreviewBrowser() {
     let browser = document.getElementById("ppBrowser");
     if (!browser) {
@@ -815,17 +808,17 @@ let PrintPreviewListener = {
 };
 
 // viewZoomOverlay.js uses this
 function getBrowser() {
   return gBrowser;
 }
 
 this.__defineGetter__("gPageLoader", function () {
-  var webnav = ViewSourceChrome.webNav;
+  var webnav = viewSourceChrome.webNav;
   if (!webnav)
     return null;
   delete this.gPageLoader;
   this.gPageLoader = (webnav instanceof Ci.nsIWebPageDescriptor) ? webnav
                                                                  : null;
   return this.gPageLoader;
 });
 
@@ -838,98 +831,98 @@ function ViewSourceSavePage()
                gPageLoader);
 }
 
 // Below are old deprecated functions and variables left behind for
 // compatibility reasons. These will be removed soon via bug 1159293.
 
 this.__defineGetter__("gLastLineFound", function () {
   Deprecated.warning("gLastLineFound is deprecated - please use " +
-                     "ViewSourceChrome.lastLineFound instead.",
+                     "viewSourceChrome.lastLineFound instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  return ViewSourceChrome.lastLineFound;
+  return viewSourceChrome.lastLineFound;
 });
 
 function onLoadViewSource() {
   Deprecated.warning("onLoadViewSource() is deprecated - please use " +
-                     "ViewSourceChrome.onXULLoaded() instead.",
+                     "viewSourceChrome.onXULLoaded() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.onXULLoaded();
+  viewSourceChrome.onXULLoaded();
 }
 
 function isHistoryEnabled() {
   Deprecated.warning("isHistoryEnabled() is deprecated - please use " +
-                     "ViewSourceChrome.historyEnabled instead.",
+                     "viewSourceChrome.historyEnabled instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  return ViewSourceChrome.historyEnabled;
+  return viewSourceChrome.historyEnabled;
 }
 
 function ViewSourceClose() {
   Deprecated.warning("ViewSourceClose() is deprecated - please use " +
-                     "ViewSourceChrome.close() instead.",
+                     "viewSourceChrome.close() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.close();
+  viewSourceChrome.close();
 }
 
 function ViewSourceReload() {
   Deprecated.warning("ViewSourceReload() is deprecated - please use " +
-                     "ViewSourceChrome.reload() instead.",
+                     "viewSourceChrome.reload() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.reload();
+  viewSourceChrome.reload();
 }
 
 function getWebNavigation()
 {
   Deprecated.warning("getWebNavigation() is deprecated - please use " +
-                     "ViewSourceChrome.webNav instead.",
+                     "viewSourceChrome.webNav instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
   // The original implementation returned null if anything threw during
   // the getting of the webNavigation.
   try {
-    return ViewSourceChrome.webNav;
+    return viewSourceChrome.webNav;
   } catch (e) {
     return null;
   }
 }
 
 function viewSource(url) {
   Deprecated.warning("viewSource() is deprecated - please use " +
-                     "ViewSourceChrome.loadURL() instead.",
+                     "viewSourceChrome.loadURL() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.loadURL(url);
+  viewSourceChrome.loadURL(url);
 }
 
 function ViewSourceGoToLine()
 {
   Deprecated.warning("ViewSourceGoToLine() is deprecated - please use " +
-                     "ViewSourceChrome.promptAndGoToLine() instead.",
+                     "viewSourceChrome.promptAndGoToLine() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.promptAndGoToLine();
+  viewSourceChrome.promptAndGoToLine();
 }
 
 function goToLine(line)
 {
   Deprecated.warning("goToLine() is deprecated - please use " +
-                     "ViewSourceChrome.goToLine() instead.",
+                     "viewSourceChrome.goToLine() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.goToLine(line);
+  viewSourceChrome.goToLine(line);
 }
 
 function BrowserForward(aEvent) {
   Deprecated.warning("BrowserForward() is deprecated - please use " +
-                     "ViewSourceChrome.goForward() instead.",
+                     "viewSourceChrome.goForward() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.goForward();
+  viewSourceChrome.goForward();
 }
 
 function BrowserBack(aEvent) {
   Deprecated.warning("BrowserBack() is deprecated - please use " +
-                     "ViewSourceChrome.goBack() instead.",
+                     "viewSourceChrome.goBack() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.goBack();
+  viewSourceChrome.goBack();
 }
 
 function UpdateBackForwardCommands() {
   Deprecated.warning("UpdateBackForwardCommands() is deprecated - please use " +
-                     "ViewSourceChrome.updateCommands() instead.",
+                     "viewSourceChrome.updateCommands() instead.",
                      "https://developer.mozilla.org/en-US/Add-ons/Code_snippets/View_Source_for_XUL_Applications");
-  ViewSourceChrome.updateCommands();
+  viewSourceChrome.updateCommands();
 }
--- a/toolkit/components/viewsource/content/viewSource.xul
+++ b/toolkit/components/viewsource/content/viewSource.xul
@@ -49,26 +49,26 @@
   <command id="cmd_findAgain"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
   <command id="cmd_findPrevious"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
 #ifdef XP_MACOSX
   <command id="cmd_findSelection"
            oncommand="document.getElementById('FindToolbar').onFindSelectionCommand();"/>
 #endif
-  <command id="cmd_reload" oncommand="ViewSourceChrome.reload();"/>
-  <command id="cmd_goToLine" oncommand="ViewSourceChrome.promptAndGoToLine();" disabled="true"/>
-  <command id="cmd_highlightSyntax" oncommand="ViewSourceChrome.toggleSyntaxHighlighting();"/>
-  <command id="cmd_wrapLongLines" oncommand="ViewSourceChrome.toggleWrapping();"/>
+  <command id="cmd_reload" oncommand="viewSourceChrome.reload();"/>
+  <command id="cmd_goToLine" oncommand="viewSourceChrome.promptAndGoToLine();" disabled="true"/>
+  <command id="cmd_highlightSyntax" oncommand="viewSourceChrome.toggleSyntaxHighlighting();"/>
+  <command id="cmd_wrapLongLines" oncommand="viewSourceChrome.toggleWrapping();"/>
   <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
   <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
   <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
 
-  <command id="Browser:Back" oncommand="ViewSourceChrome.goBack()" observes="viewSourceNavigation"/>
-  <command id="Browser:Forward" oncommand="ViewSourceChrome.goForward()" observes="viewSourceNavigation"/>
+  <command id="Browser:Back" oncommand="viewSourceChrome.goBack()" observes="viewSourceNavigation"/>
+  <command id="Browser:Forward" oncommand="viewSourceChrome.goForward()" observes="viewSourceNavigation"/>
 
   <broadcaster id="viewSourceNavigation"/>
 
   <keyset id="editMenuKeys"/>
   <keyset id="viewSourceKeys">
     <key id="key_savePage" key="&savePageCmd.commandkey;" modifiers="accel" command="cmd_savePage"/>
     <key id="key_print" key="&printCmd.commandkey;" modifiers="accel" command="cmd_print"/>
     <key id="key_close" key="&closeCmd.commandkey;" modifiers="accel" command="cmd_close"/>
@@ -126,21 +126,21 @@
               observes="viewSourceNavigation"/>
     <menuseparator observes="viewSourceNavigation"/>
     <menuitem id="cMenu_findAgain"/>
     <menuseparator/>
     <menuitem id="cMenu_copy"/>
     <menuitem id="context-copyLink"
               label="&copyLinkCmd.label;"
               accesskey="&copyLinkCmd.accesskey;"
-              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+              oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuitem id="context-copyEmail"
               label="&copyEmailCmd.label;"
               accesskey="&copyEmailCmd.accesskey;"
-              oncommand="ViewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
+              oncommand="viewSourceChrome.onContextMenuCopyLinkOrEmail();"/>
     <menuseparator/>
     <menuitem id="cMenu_selectAll"/>
   </menupopup>
 
   <!-- Menu -->
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
@@ -201,17 +201,17 @@
                         key="key_textZoomReset"/>
             </menupopup>
           </menu>
 
           <!-- Charset Menu -->
           <menu id="charsetMenu"
                 label="&charsetMenu2.label;"
                 accesskey="&charsetMenu2.accesskey;"
-                oncommand="ViewSourceChrome.onSetCharacterSet(event);"
+                oncommand="viewSourceChrome.onSetCharacterSet(event);"
                 onpopupshowing="CharsetMenu.build(event.target);"
                 onpopupshown="CharsetMenu.update(event.target, content.document.characterSet);">
             <menupopup/>
           </menu>
           <menuseparator/>
           <menuitem id="menu_wrapLongLines" type="checkbox" command="cmd_wrapLongLines"
                     label="&menu_wrapLongLines.title;" accesskey="&menu_wrapLongLines.accesskey;"/>
           <menuitem type="checkbox" id="menu_highlightSyntax" command="cmd_highlightSyntax"
--- a/toolkit/components/viewsource/content/viewSourceUtils.js
+++ b/toolkit/components/viewsource/content/viewSourceUtils.js
@@ -7,16 +7,20 @@
 /*
  * To keep the global namespace safe, don't define global variables and
  * functions in this file.
  *
  * This file silently depends on contentAreaUtils.js for
  * getDefaultFileName, getNormalizedLeafName and getDefaultExtension
  */
 
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ViewSourceBrowser",
+  "resource://gre/modules/ViewSourceBrowser.jsm");
+
 var gViewSourceUtils = {
 
   mnsIWebBrowserPersist: Components.interfaces.nsIWebBrowserPersist,
   mnsIWebProgress: Components.interfaces.nsIWebProgress,
   mnsIWebPageDescriptor: Components.interfaces.nsIWebPageDescriptor,
 
   /**
    * Opens the view source window.
@@ -55,16 +59,44 @@ var gViewSourceUtils = {
                           .getService(Components.interfaces.nsIPrefBranch);
     if (prefs.getBoolPref("view_source.editor.external")) {
       this.openInExternalEditor(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
     } else {
       this._openInInternalViewer(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber);
     }
   },
 
+  /**
+   * Displays view source in the provided <browser>.  This allows for non-window
+   * display methods, such as a tab from Firefox.  The caller that manages
+   * the <browser> is responsible for ensuring the companion frame script,
+   * viewSource-content.js, has been loaded for the <browser>.
+   *
+   * @param aArgs
+   *        An object with the following properties:
+   *
+   *        URL (required):
+   *          A string URL for the page we'd like to view the source of.
+   *        viewSourceBrowser (required):
+   *          The browser to display the view source in.
+   *        browser (optional):
+   *          The browser containing the document that we would like to view the
+   *          source of. This is required if outerWindowID is passed.
+   *        outerWindowID (optional):
+   *          The outerWindowID of the content window containing the document that
+   *          we want to view the source of. Pass this if you want to attempt to
+   *          load the document source out of the network cache.
+   *        lineNumber (optional):
+   *          The line number to focus on once the source is loaded.
+   */
+  viewSourceInBrowser: function(aArgs) {
+    let viewSourceBrowser = new ViewSourceBrowser(aArgs.viewSourceBrowser);
+    viewSourceBrowser.loadViewSource(aArgs);
+  },
+
   // Opens the interval view source viewer
   _openInInternalViewer: function(aArgsOrURL, aPageDescriptor, aDocument, aLineNumber)
   {
     // try to open a view-source window while inheriting the charset (if any)
     var charset = null;
     var isForcedCharset = false;
     if (aDocument) {
       if (Components.utils.isCrossProcessWrapper(aDocument)) {
--- a/toolkit/components/viewsource/jar.mn
+++ b/toolkit/components/viewsource/jar.mn
@@ -4,9 +4,9 @@
 
 toolkit.jar:
   content/global/viewSource.css             (content/viewSource.css)
   content/global/viewSource.js              (content/viewSource.js)
 * content/global/viewSource.xul             (content/viewSource.xul)
   content/global/viewPartialSource.js       (content/viewPartialSource.js)
 * content/global/viewPartialSource.xul      (content/viewPartialSource.xul)
   content/global/viewSourceUtils.js         (content/viewSourceUtils.js)
-  content/global/viewSource-content.js      (content/viewSource-content.js)
\ No newline at end of file
+  content/global/viewSource-content.js      (content/viewSource-content.js)
--- a/toolkit/components/viewsource/moz.build
+++ b/toolkit/components/viewsource/moz.build
@@ -4,10 +4,14 @@
 # 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/.
 
 BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
 MOCHITEST_CHROME_MANIFESTS += ['test/chrome.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
+EXTRA_JS_MODULES += [
+    'ViewSourceBrowser.jsm',
+]
+
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'View Source')
--- a/toolkit/components/viewsource/test/browser/browser_gotoline.js
+++ b/toolkit/components/viewsource/test/browser/browser_gotoline.js
@@ -19,17 +19,17 @@ add_task(function*() {
 
 let checkViewSource = Task.async(function* (aWindow) {
   is(aWindow.gBrowser.contentDocument.body.textContent, content, "Correct content loaded");
   let selection = aWindow.gBrowser.contentWindow.getSelection();
   let statusPanel = aWindow.document.getElementById("statusbar-line-col");
   is(statusPanel.getAttribute("label"), "", "Correct status bar text");
 
   for (let i = 1; i <= 3; i++) {
-    aWindow.ViewSourceChrome.goToLine(i);
+    aWindow.viewSourceChrome.goToLine(i);
     let result = yield ContentTask.spawn(aWindow.gBrowser, i, function*(i) {
       let selection = content.getSelection();
       return (selection.toString() == "line " + i);
     });
 
     ok(result, "Correct text selected");
 
     yield ContentTaskUtils.waitForCondition(() => {