Merge f-t to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 24 May 2015 12:15:26 -0700
changeset 245427 b6623a27fa647437a630dfc548d684b2a2de22c1
parent 245413 5250f0664044ddf2b8e7a00a08d5fbf7530bf992 (current diff)
parent 245426 b15bc4bf63dbf92600ef4e18f9bbc17ea23b0c0a (diff)
child 245428 5f7e75cf18915c3a695497a3dd304f84388c11d1
child 245520 7637b24cd7a5fa2819e594b643746079cea8e9f8
child 245523 13b457c4d6c9d66b5e918c04f5b819babb6d7ffd
push id60174
push userphilringnalda@gmail.com
push dateSun, 24 May 2015 22:27:17 +0000
treeherdermozilla-inbound@5f7e75cf1891 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
b6623a27fa64 / 41.0a1 / 20150525030205 / files
nightly linux64
b6623a27fa64 / 41.0a1 / 20150525030205 / files
nightly mac
b6623a27fa64 / 41.0a1 / 20150525030205 / files
nightly win32
b6623a27fa64 / 41.0a1 / 20150525030205 / files
nightly win64
b6623a27fa64 / 41.0a1 / 20150525030205 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge f-t to m-c, a=merge
browser/app/profile/firefox.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -193,17 +193,17 @@ pref("app.update.url", "https://aus4.moz
 
 // app.update.interval is in branding section
 // app.update.promptWaitTime is in branding section
 
 // Show the Update Checking/Ready UI when the user was idle for x seconds
 pref("app.update.idletime", 60);
 
 // Whether or not we show a dialog box informing the user that the update was
-// successfully applied. This is off in Firefox by default since we show a 
+// successfully applied. This is off in Firefox by default since we show a
 // upgrade start page instead! Other apps may wish to show this UI, and supply
 // a whatsNewURL field in their brand.properties that contains a link to a page
 // which tells users what's new in this new update.
 pref("app.update.showInstalledUI", false);
 
 // 0 = suppress prompting for incompatibilities if there are updates available
 //     to newer versions of installed addons that resolve them.
 // 1 = suppress prompting for incompatibilities only if there are VersionInfo
@@ -220,20 +220,20 @@ pref("app.update.service.enabled", true)
 // e.g.
 //  extensions.{GUID}.update.enabled
 //  extensions.{GUID}.update.url
 //  .. etc ..
 //
 pref("extensions.update.enabled", true);
 pref("extensions.update.url", "https://versioncheck.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
 pref("extensions.update.background.url", "https://versioncheck-bg.addons.mozilla.org/update/VersionCheck.php?reqVersion=%REQ_VERSION%&id=%ITEM_ID%&version=%ITEM_VERSION%&maxAppVersion=%ITEM_MAXAPPVERSION%&status=%ITEM_STATUS%&appID=%APP_ID%&appVersion=%APP_VERSION%&appOS=%APP_OS%&appABI=%APP_ABI%&locale=%APP_LOCALE%&currentAppVersion=%CURRENT_APP_VERSION%&updateType=%UPDATE_TYPE%&compatMode=%COMPATIBILITY_MODE%");
-pref("extensions.update.interval", 86400);  // Check for updates to Extensions and 
+pref("extensions.update.interval", 86400);  // Check for updates to Extensions and
                                             // Themes every day
 // Non-symmetric (not shared by extensions) extension-specific [update] preferences
-pref("extensions.dss.enabled", false);          // Dynamic Skin Switching                                               
+pref("extensions.dss.enabled", false);          // Dynamic Skin Switching
 pref("extensions.dss.switchPending", false);    // Non-dynamic switch pending after next
                                                 // restart.
 
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.name", "chrome://browser/locale/browser.properties");
 pref("extensions.{972ce4c6-7e08-4474-a285-3208198ce6fd}.description", "chrome://browser/locale/browser.properties");
 
 pref("lightweightThemes.update.enabled", true);
 pref("lightweightThemes.getMoreURL", "https://addons.mozilla.org/%LOCALE%/firefox/themes");
@@ -354,17 +354,17 @@ pref("browser.urlbar.formatting.enabled"
 pref("browser.urlbar.trimURLs", true);
 
 pref("browser.altClickSave", false);
 
 // Enable logging downloads operations to the Error Console.
 pref("browser.download.debug", false);
 
 // Number of milliseconds to wait for the http headers (and thus
-// the Content-Disposition filename) before giving up and falling back to 
+// the Content-Disposition filename) before giving up and falling back to
 // picking a filename without that info in hand so that the user sees some
 // feedback from their action.
 pref("browser.download.saveLinkAsFilenameTimeout", 4000);
 
 pref("browser.download.useDownloadDir", true);
 pref("browser.download.folderList", 1);
 pref("browser.download.manager.addToRecentDocs", true);
 pref("browser.download.manager.resumeOnWakeDelay", 10000);
@@ -385,17 +385,17 @@ pref("browser.search.searchEnginesURL", 
 
 // Tell the search service to load search plugins from the locale JAR
 pref("browser.search.loadFromJars", true);
 pref("browser.search.jarURIs", "chrome://browser/locale/searchplugins/");
 
 // pointer to the default engine name
 pref("browser.search.defaultenginename",      "chrome://browser-region/locale/region.properties");
 
-// Ordering of Search Engines in the Engine list. 
+// Ordering of Search Engines in the Engine list.
 pref("browser.search.order.1",                "chrome://browser-region/locale/region.properties");
 pref("browser.search.order.2",                "chrome://browser-region/locale/region.properties");
 pref("browser.search.order.3",                "chrome://browser-region/locale/region.properties");
 
 // Market-specific search defaults (US market only)
 pref("browser.search.geoSpecificDefaults", true);
 pref("browser.search.defaultenginename.US",      "data:text/plain,browser.search.defaultenginename.US=Yahoo");
 pref("browser.search.order.US.1",                "data:text/plain,browser.search.order.US.1=Yahoo");
@@ -457,44 +457,44 @@ pref("browser.tabs.loadBookmarksInBackgr
 pref("browser.tabs.tabClipWidth", 140);
 pref("browser.tabs.animate", true);
 #ifdef UNIX_BUT_NOT_MAC
 pref("browser.tabs.drawInTitlebar", false);
 #else
 pref("browser.tabs.drawInTitlebar", true);
 #endif
 
-// When tabs opened by links in other tabs via a combination of 
+// When tabs opened by links in other tabs via a combination of
 // browser.link.open_newwindow being set to 3 and target="_blank" etc are
 // closed:
 // true   return to the tab that opened this tab (its owner)
 // false  return to the adjacent tab (old default)
 pref("browser.tabs.selectOwnerOnClose", true);
 
 pref("browser.ctrlTab.previews", false);
 
 // By default, do not export HTML at shutdown.
 // If true, at shutdown the bookmarks in your menu and toolbar will
 // be exported as HTML to the bookmarks.html file.
 pref("browser.bookmarks.autoExportHTML",          false);
 
-// The maximum number of daily bookmark backups to 
+// The maximum number of daily bookmark backups to
 // keep in {PROFILEDIR}/bookmarkbackups. Special values:
 // -1: unlimited
 //  0: no backups created (and deletes all existing backups)
 pref("browser.bookmarks.max_backups",             15);
 
 // Scripts & Windows prefs
 pref("dom.disable_open_during_load",              true);
 pref("javascript.options.showInConsole",          true);
 #ifdef DEBUG
 pref("general.warnOnAboutConfig",                 false);
 #endif
 
-// This is the pref to control the location bar, change this to true to 
+// This is the pref to control the location bar, change this to true to
 // force this - this makes the origin of popup windows more obvious to avoid
 // spoofing. We would rather not do it by default because it affects UE for web
 // applications, but without it there isn't a really good way to prevent chrome
 // spoofing, see bug 337344
 pref("dom.disable_window_open_feature.location",  true);
 // prevent JS from setting status messages
 pref("dom.disable_window_status_change",          true);
 // allow JS to move and resize existing windows
@@ -912,17 +912,17 @@ pref("browser.contentHandlers.types.5.ty
 
 pref("browser.feeds.handler", "ask");
 pref("browser.videoFeeds.handler", "ask");
 pref("browser.audioFeeds.handler", "ask");
 
 // At startup, if the handler service notices that the version number in the
 // region.properties file is newer than the version number in the handler
 // service datastore, it will add any new handlers it finds in the prefs (as
-// seeded by this file) to its datastore.  
+// seeded by this file) to its datastore.
 pref("gecko.handlerService.defaultHandlersVersion", "chrome://browser-region/locale/region.properties");
 
 // The default set of web-based protocol handlers shown in the application
 // selection dialog for webcal: ; I've arbitrarily picked 4 default handlers
 // per protocol, but if some locale wants more than that (or defaults for some
 // protocol not currently listed here), we should go ahead and add those.
 
 // webcal
@@ -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
@@ -5555,17 +5567,17 @@ function middleMousePaste(event) {
     return;
 
   // Strip embedded newlines and surrounding whitespace, to match the URL
   // bar's behavior (stripsurroundingwhitespace)
   clipboard = clipboard.replace(/\s*\n\s*/g, "");
 
   clipboard = stripUnsafeProtocolOnPaste(clipboard);
 
-  // if it's not the current tab, we don't need to do anything because the 
+  // if it's not the current tab, we don't need to do anything because the
   // browser doesn't exist.
   let where = whereToOpenLink(event, true, false);
   let lastLocationChange;
   if (where == "current") {
     lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
   }
 
   getShortcutOrURIAndPostData(clipboard).then(data => {
@@ -6273,20 +6285,20 @@ function warnAboutClosingWindow() {
   let otherPBWindowExists = false;
   let nonPopupPresent = false;
   for (let win of browserWindows()) {
     if (!win.closed && win != window) {
       if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win))
         otherPBWindowExists = true;
       if (win.toolbar.visible)
         nonPopupPresent = true;
-      // If the current window is not in private browsing mode we don't need to 
-      // look for other pb windows, we can leave the loop when finding the 
-      // first non-popup window. If however the current window is in private 
-      // browsing mode then we need at least one other pb and one non-popup 
+      // If the current window is not in private browsing mode we don't need to
+      // look for other pb windows, we can leave the loop when finding the
+      // first non-popup window. If however the current window is in private
+      // browsing mode then we need at least one other pb and one non-popup
       // window to break out early.
       if ((!isPBWindow || otherPBWindowExists) && nonPopupPresent)
         break;
     }
   }
 
   if (isPBWindow && !otherPBWindowExists) {
     let exitingCanceled = Cc["@mozilla.org/supports-PRBool;1"].
@@ -7094,17 +7106,17 @@ var gIdentityHandler = {
 
     let dt = event.dataTransfer;
     dt.setData("text/x-moz-url", urlString);
     dt.setData("text/uri-list", value);
     dt.setData("text/plain", value);
     dt.setData("text/html", htmlString);
     dt.setDragImage(gProxyFavIcon, 16, 16);
   },
- 
+
   handleEvent: function (event) {
     switch (event.type) {
       case "blur":
         // Focus hasn't moved yet, need to wait until after the blur event.
         setTimeout(() => {
           if (document.activeElement &&
               document.activeElement.compareDocumentPosition(this._identityPopup) &
                 Node.DOCUMENT_POSITION_CONTAINS)
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1000,21 +1000,38 @@ nsContextMenu.prototype = {
     var reference = null;
     if (aContext == "selection")
       reference = focusedWindow.getSelection();
     else if (aContext == "mathml")
       reference = this.target;
     else
       throw "not reached";
 
-    // unused (and play nice for fragments generated via XSLT too)
-    var docUrl = null;
-    window.openDialog("chrome://global/content/viewPartialSource.xul",
-                      "_blank", "scrollbars,resizable,chrome,dialog=no",
-                      docUrl, docCharset, reference, aContext);
+    let inTab = Services.prefs.getBoolPref("view_source.tab");
+    if (inTab) {
+      let tab = gBrowser.loadOneTab("about:blank", {
+        relatedToCurrent: true,
+        inBackground: false
+      });
+      let viewSourceBrowser = gBrowser.getBrowserForTab(tab);
+      if (aContext == "selection") {
+        top.gViewSourceUtils
+           .viewSourceFromSelectionInBrowser(reference, viewSourceBrowser);
+      } else {
+        top.gViewSourceUtils
+           .viewSourceFromFragmentInBrowser(reference, aContext,
+                                            viewSourceBrowser);
+      }
+    } else {
+      // unused (and play nice for fragments generated via XSLT too)
+      var docUrl = null;
+      window.openDialog("chrome://global/content/viewPartialSource.xul",
+                        "_blank", "scrollbars,resizable,chrome,dialog=no",
+                        docUrl, docCharset, reference, aContext);
+    }
   },
 
   // Open new "view source" window with the frame's URL.
   viewFrameSource: function() {
     BrowserViewSourceOfDocument({
       browser: this.browser,
       URL: gContextMenuContentData.docLocation,
       outerWindowID: gContextMenuContentData.frameOuterWindowID,
@@ -1148,17 +1165,17 @@ nsContextMenu.prototype = {
     // image changed since the context menu was initiated.
     if (this.disableSetDesktopBackground())
       return;
 
     var doc = this.target.ownerDocument;
     urlSecurityCheck(this.target.currentURI.spec, this.principal);
 
     // Confirm since it's annoying if you hit this accidentally.
-    const kDesktopBackgroundURL = 
+    const kDesktopBackgroundURL =
                   "chrome://browser/content/setDesktopBackground.xul";
 #ifdef XP_MACOSX
     // On Mac, the Set Desktop Background window is not modal.
     // Don't open more than one Set Desktop Background window.
     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
                        .getService(Components.interfaces.nsIWindowMediator);
     var dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
     if (dbWin) {
@@ -1191,17 +1208,17 @@ nsContextMenu.prototype = {
     const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
 
     // an object to proxy the data through to
     // nsIExternalHelperAppService.doContent, which will wait for the
     // appropriate MIME-type headers and then prompt the user with a
     // file picker
     function saveAsListener() {}
     saveAsListener.prototype = {
-      extListener: null, 
+      extListener: null,
 
       onStartRequest: function saveLinkAs_onStartRequest(aRequest, aContext) {
 
         // if the timer fired, the error status will have been caused by that,
         // and we'll be restarting in onStopRequest, so no reason to notify
         // the user
         if (aRequest.status == NS_ERROR_SAVE_LINK_AS_TIMEOUT)
           return;
@@ -1224,27 +1241,27 @@ nsContextMenu.prototype = {
             const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
                        getService(Ci.nsIWindowMediator);
             let window = wm.getOuterWindowWithId(windowID);
             promptSvc.alert(window, title, msg);
           } catch (ex) {}
           return;
         }
 
-        let extHelperAppSvc = 
+        let extHelperAppSvc =
           Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
           getService(Ci.nsIExternalHelperAppService);
         let channel = aRequest.QueryInterface(Ci.nsIChannel);
         this.extListener =
-          extHelperAppSvc.doContent(channel.contentType, aRequest, 
+          extHelperAppSvc.doContent(channel.contentType, aRequest,
                                     null, true, window);
         this.extListener.onStartRequest(aRequest, aContext);
-      }, 
+      },
 
-      onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext, 
+      onStopRequest: function saveLinkAs_onStopRequest(aRequest, aContext,
                                                        aStatusCode) {
         if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
           // do it the old fashioned way, which will pick the best filename
           // it can without waiting.
           saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
                   doc);
         }
         if (this.extListener)
@@ -1267,22 +1284,22 @@ nsContextMenu.prototype = {
           // because the save-as-timer would expire and cancel the channel
           // before we get credentials from user.  Both authentication dialog
           // and save as dialog would appear on the screen as we fall back to
           // the old fashioned way after the timeout.
           timer.cancel();
           channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
         }
         throw Cr.NS_ERROR_NO_INTERFACE;
-      } 
+      }
     }
 
-    // if it we don't have the headers after a short time, the user 
+    // if it we don't have the headers after a short time, the user
     // won't have received any feedback from their click.  that's bad.  so
-    // we give up waiting for the filename. 
+    // we give up waiting for the filename.
     function timerCallback() {}
     timerCallback.prototype = {
       notify: function sLA_timer_notify(aTimer) {
         channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
         return;
       }
     }
 
@@ -1314,18 +1331,18 @@ nsContextMenu.prototype = {
     channel.loadFlags |= flags;
 
     if (channel instanceof Ci.nsIHttpChannel) {
       channel.referrer = docURI;
       if (channel instanceof Ci.nsIHttpChannelInternal)
         channel.forceAllowThirdPartyCookie = true;
     }
 
-    // fallback to the old way if we don't see the headers quickly 
-    var timeToWait = 
+    // fallback to the old way if we don't see the headers quickly
+    var timeToWait =
       gPrefService.getIntPref("browser.download.saveLinkAsFilenameTimeout");
     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     timer.initWithCallback(new timerCallback(), timeToWait,
                            timer.TYPE_ONE_SHOT);
 
     // kick off the channel with our proxy object as the listener
     channel.asyncOpen(new saveAsListener(), null);
   },
@@ -1494,17 +1511,17 @@ nsContextMenu.prototype = {
     }
 
     // Voila!
     return node;
   },
 
   // Generate fully qualified URL for clicked-on link.
   getLinkURL: function() {
-    var href = this.link.href;  
+    var href = this.link.href;
     if (href)
       return href;
 
     href = this.link.getAttributeNS("http://www.w3.org/1999/xlink",
                                     "href");
 
     if (!href || !href.match(/\S/)) {
       // Without this we try to save as the current doc,
--- a/browser/devtools/framework/test/browser_devtools_api.js
+++ b/browser/devtools/framework/test/browser_devtools_api.js
@@ -24,17 +24,17 @@ let EventEmitter = tempScope.EventEmitte
 function test() {
   addTab("about:blank").then(runTests1);
 }
 
 // Test scenario 1: the tool definition build method returns a promise.
 function runTests1(aTab) {
   let toolDefinition = {
     id: toolId1,
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
     label: "someLabel",
     build: function(iframeWindow, toolbox) {
       let panel = new DevToolPanel(iframeWindow, toolbox);
       return panel.open();
     },
   };
@@ -85,17 +85,17 @@ function runTests1(aTab) {
     toolbox.once("select", runTests2);
   });
 }
 
 // Test scenario 2: the tool definition build method returns panel instance.
 function runTests2() {
   let toolDefinition = {
     id: toolId2,
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
     label: "someLabel",
     build: function(iframeWindow, toolbox) {
       return new DevToolPanel(iframeWindow, toolbox);
     },
   };
 
@@ -224,20 +224,26 @@ DevToolPanel.prototype = {
       this._isReady = true;
       this.emit("ready");
       deferred.resolve(this);
     });
 
     return deferred.promise;
   },
 
-  get target() this._toolbox.target,
+  get target() {
+    return this._toolbox.target;
+  },
 
-  get toolbox() this._toolbox,
+  get toolbox() {
+    return this._toolbox;
+  },
 
-  get isReady() this._isReady,
+  get isReady() {
+    return this._isReady;
+  },
 
   _isReady: false,
 
   destroy: function DTI_destroy() {
     return promise.defer(null);
   },
 };
--- a/browser/devtools/framework/test/browser_devtools_api_destroy.js
+++ b/browser/devtools/framework/test/browser_devtools_api_destroy.js
@@ -8,17 +8,17 @@ const Cu = Components.utils;
 function test() {
   addTab("about:blank").then(runTests);
 }
 
 function runTests(aTab) {
   let toolDefinition = {
     id: "testTool",
     visibilityswitch: "devtools.testTool.enabled",
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     url: "about:blank",
     label: "someLabel",
     build: function(iframeWindow, toolbox) {
       let deferred = promise.defer();
       executeSoon(() => {
         deferred.resolve({
           target: toolbox.target,
           toolbox: toolbox,
--- a/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js
+++ b/browser/devtools/framework/test/browser_toolbox_dynamic_registration.js
@@ -21,17 +21,17 @@ function testRegister(aToolbox)
 {
   toolbox = aToolbox
   gDevTools.once("tool-registered", toolRegistered);
 
   gDevTools.registerTool({
     id: "test-tool",
     label: "Test Tool",
     inMenu: true,
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     build: function() {}
   });
 }
 
 function toolRegistered(event, toolId)
 {
   is(toolId, "test-tool", "tool-registered event handler sent tool id");
 
--- a/browser/devtools/framework/test/browser_toolbox_options.js
+++ b/browser/devtools/framework/test/browser_toolbox_options.js
@@ -18,17 +18,17 @@ add_task(function*() {
   yield testOptions();
   yield testToggleTools();
   yield cleanup();
 });
 
 function registerNewTool() {
   let toolDefinition = {
     id: "test-tool",
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     visibilityswitch: "devtools.test-tool.enabled",
     url: "about:blank",
     label: "someLabel"
   };
 
   ok(gDevTools, "gDevTools exists");
   ok(!gDevTools.getToolDefinitionMap().has("test-tool"),
     "The tool is not registered");
--- a/browser/devtools/framework/test/browser_toolbox_sidebar.js
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar.js
@@ -22,25 +22,25 @@ function test() {
   let registeredTabs = {};
   let readyTabs = {};
 
   let toolDefinition = {
     id: "fakeTool4242",
     visibilityswitch: "devtools.fakeTool4242.enabled",
     url: toolURL,
     label: "FAKE TOOL!!!",
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     build: function(iframeWindow, toolbox) {
       let deferred = promise.defer();
       executeSoon(() => {
         deferred.resolve({
           target: toolbox.target,
           toolbox: toolbox,
           isReady: true,
-          destroy: function(){},
+          destroy: function() {},
           panelDoc: iframeWindow.document,
         });
       });
       return deferred.promise;
     },
   };
 
   gDevTools.registerTool(toolDefinition);
--- a/browser/devtools/framework/test/browser_toolbox_sidebar_events.js
+++ b/browser/devtools/framework/test/browser_toolbox_sidebar_events.js
@@ -17,25 +17,25 @@ function test() {
 
   let collectedEvents = [];
 
   let toolDefinition = {
     id: "testTool1072208",
     visibilityswitch: "devtools.testTool1072208.enabled",
     url: toolURL,
     label: "Test tool",
-    isTargetSupported: function() true,
+    isTargetSupported: () => true,
     build: function(iframeWindow, toolbox) {
       let deferred = promise.defer();
       executeSoon(() => {
         deferred.resolve({
           target: toolbox.target,
           toolbox: toolbox,
           isReady: true,
-          destroy: function(){},
+          destroy: function() {},
           panelDoc: iframeWindow.document,
         });
       });
       return deferred.promise;
     },
   };
 
   gDevTools.registerTool(toolDefinition);
--- a/browser/devtools/framework/test/browser_toolbox_tool_ready.js
+++ b/browser/devtools/framework/test/browser_toolbox_tool_ready.js
@@ -1,20 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ///////////////////
 //
 // Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
 
 function performChecks(target) {
-  return Task.spawn(function() {
+  return Task.spawn(function*() {
     let toolIds = gDevTools.getToolDefinitionArray()
                            .filter(def => def.isTargetSupported(target))
                            .map(def => def.id);
 
     let toolbox;
     for (let index = 0; index < toolIds.length; index++) {
       let toolId = toolIds[index];
 
@@ -27,17 +27,17 @@ function performChecks(target) {
       ok(panel.isReady, toolId + " panel should be ready");
     }
 
     yield toolbox.destroy();
   });
 }
 
 function test() {
-  Task.spawn(function() {
+  Task.spawn(function*() {
     toggleAllTools(true);
     let tab = yield addTab("about:blank");
     let target = TargetFactory.forTab(tab);
     yield target.makeRemote();
     yield performChecks(target);
     gBrowser.removeCurrentTab();
     toggleAllTools(false);
     finish();
--- a/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js
+++ b/browser/devtools/framework/test/browser_toolbox_tool_remote_reopen.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 ///////////////////
 //
 // Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Shader Editor is still waiting for a WebGL context to be created.");
 
 const { DebuggerServer } =
   Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
 const { DebuggerClient } =
   Cu.import("resource://gre/modules/devtools/dbg-client.jsm", {});
 
@@ -35,17 +35,17 @@ const { DebuggerClient } =
  *
  * In WebIDE, we do not destroy the DebuggerClient on toolbox close because it
  * is still used for other purposes like managing apps, etc. that aren't part of
  * a toolbox.  Thus, the same client gets reused across multiple toolboxes,
  * which leads to the tools failing if they don't destroy their fronts.
  */
 
 function runTools(target) {
-  return Task.spawn(function() {
+  return Task.spawn(function*() {
     let toolIds = gDevTools.getToolDefinitionArray()
                            .filter(def => def.isTargetSupported(target))
                            .map(def => def.id);
 
     let toolbox;
     for (let index = 0; index < toolIds.length; index++) {
       let toolId = toolIds[index];
 
@@ -91,17 +91,17 @@ function getTarget(client) {
     });
     deferred.resolve(target);
   });
 
   return deferred.promise;
 }
 
 function test() {
-  Task.spawn(function() {
+  Task.spawn(function*() {
     toggleAllTools(true);
     yield addTab("about:blank");
 
     let client = yield getClient();
     let target = yield getTarget(client);
     yield runTools(target);
 
     // Actor fronts should be destroyed now that the toolbox has closed, but
--- a/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
+++ b/browser/devtools/framework/test/browser_toolbox_window_title_changes.js
@@ -18,30 +18,30 @@ function test() {
   const LABEL_2 = "Debugger";
 
   let toolbox;
 
   addTab(URL_1).then(function () {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     gDevTools.showToolbox(target, null, Toolbox.HostType.BOTTOM)
       .then(function (aToolbox) { toolbox = aToolbox; })
-      .then(function () toolbox.selectTool(TOOL_ID_1))
+      .then(() => toolbox.selectTool(TOOL_ID_1))
 
     // undock toolbox and check title
-      .then(function () toolbox.switchHost(Toolbox.HostType.WINDOW))
+      .then(() => toolbox.switchHost(Toolbox.HostType.WINDOW))
       .then(checkTitle.bind(null, LABEL_1, URL_1, "toolbox undocked"))
 
     // switch to different tool and check title
-      .then(function () toolbox.selectTool(TOOL_ID_2))
+      .then(() => toolbox.selectTool(TOOL_ID_2))
       .then(checkTitle.bind(null, LABEL_2, URL_1, "tool changed"))
 
     // navigate to different url and check title
       .then(function () {
         let deferred = promise.defer();
-        target.once("navigate", function () deferred.resolve());
+        target.once("navigate", () => deferred.resolve());
         gBrowser.loadURI(URL_2);
         return deferred.promise;
       })
       .then(checkTitle.bind(null, LABEL_2, URL_2, "url changed"))
 
     // destroy toolbox, create new one hosted in a window (with a
     // different tool id), and check title
       .then(function () {
@@ -50,22 +50,22 @@ function test() {
         executeSoon(function() {
           toolbox.destroy()
             .then(function () {
               // After destroying the toolbox, a fresh target is required.
               target = TargetFactory.forTab(gBrowser.selectedTab);
               return gDevTools.showToolbox(target, null, Toolbox.HostType.WINDOW);
             })
             .then(function (aToolbox) { toolbox = aToolbox; })
-            .then(function () toolbox.selectTool(TOOL_ID_1))
+            .then(() => toolbox.selectTool(TOOL_ID_1))
             .then(checkTitle.bind(null, LABEL_1, URL_2,
                                   "toolbox destroyed and recreated"))
 
             // clean up
-            .then(function () toolbox.destroy())
+            .then(() => toolbox.destroy())
             .then(function () {
               toolbox = null;
               gBrowser.removeCurrentTab();
               Services.prefs.clearUserPref("devtools.toolbox.host");
               Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
               Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
               finish();
             });
--- a/browser/devtools/performance/modules/logic/marker-utils.js
+++ b/browser/devtools/performance/modules/logic/marker-utils.js
@@ -176,9 +176,99 @@ const DOM = exports.DOM = {
     labelName.className = "plain marker-details-labelname";
     labelValue.className = "plain marker-details-labelvalue";
     labelName.setAttribute("value", field);
     labelValue.setAttribute("value", value);
     hbox.appendChild(labelName);
     hbox.appendChild(labelValue);
     return hbox;
   },
+
+  /**
+   * Builds a stack trace in an element.
+   *
+   * @param {Document} doc
+   * @param object params
+   *        An options object with the following members:
+   *        string type - String identifier for type of stack ("stack", "startStack" or "endStack")
+   *        number frameIndex - The index of the topmost stack frame.
+   *        array frames - Array of stack frames.
+   */
+  buildStackTrace: function(doc, { type, frameIndex, frames }) {
+    let container = doc.createElement("vbox");
+    let labelName = doc.createElement("label");
+    labelName.className = "plain marker-details-labelname";
+    labelName.setAttribute("value", L10N.getStr(`timeline.markerDetail.${type}`));
+    container.appendChild(labelName);
+
+    let wasAsyncParent = false;
+    while (frameIndex > 0) {
+      let frame = frames[frameIndex];
+      let url = frame.source;
+      let displayName = frame.functionDisplayName;
+      let line = frame.line;
+
+      // If the previous frame had an async parent, then the async
+      // cause is in this frame and should be displayed.
+      if (wasAsyncParent) {
+        let asyncBox = doc.createElement("hbox");
+        let asyncLabel = doc.createElement("label");
+        asyncLabel.className = "devtools-monospace";
+        asyncLabel.setAttribute("value", L10N.getFormatStr("timeline.markerDetail.asyncStack",
+                                                           frame.asyncCause));
+        asyncBox.appendChild(asyncLabel);
+        container.appendChild(asyncBox);
+        wasAsyncParent = false;
+      }
+
+      let hbox = doc.createElement("hbox");
+
+      if (displayName) {
+        let functionLabel = doc.createElement("label");
+        functionLabel.className = "devtools-monospace";
+        functionLabel.setAttribute("value", displayName);
+        hbox.appendChild(functionLabel);
+      }
+
+      if (url) {
+        let aNode = doc.createElement("a");
+        aNode.className = "waterfall-marker-location devtools-source-link";
+        aNode.href = url;
+        aNode.draggable = false;
+        aNode.setAttribute("title", url);
+
+        let urlNode = doc.createElement("label");
+        urlNode.className = "filename";
+        urlNode.setAttribute("value", WebConsoleUtils.Utils.abbreviateSourceURL(url));
+        let lineNode = doc.createElement("label");
+        lineNode.className = "line-number";
+        lineNode.setAttribute("value", `:${line}`);
+
+        aNode.appendChild(urlNode);
+        aNode.appendChild(lineNode);
+        hbox.appendChild(aNode);
+
+        // Clicking here will bubble up to the parent,
+        // which handles the view source.
+        aNode.setAttribute("data-action", JSON.stringify({
+          url, line, action: "view-source"
+        }));
+      }
+
+      if (!displayName && !url) {
+        let label = doc.createElement("label");
+        label.setAttribute("value", L10N.getStr("timeline.markerDetail.unknownFrame"));
+        hbox.appendChild(label);
+      }
+
+      container.appendChild(hbox);
+
+      if (frame.asyncParent) {
+        frameIndex = frame.asyncParent;
+        wasAsyncParent = true;
+      } else {
+        frameIndex = frame.parent;
+      }
+    }
+
+    return container;
+  }
 };
--- a/browser/devtools/performance/modules/widgets/marker-details.js
+++ b/browser/devtools/performance/modules/widgets/marker-details.js
@@ -24,154 +24,102 @@ loader.lazyRequireGetter(this, "MarkerUt
  *
  * @param nsIDOMNode parent
  *        The parent node holding the view.
  * @param nsIDOMNode splitter
  *        The splitter node that the resize event is bound to.
  */
 function MarkerDetails(parent, splitter) {
   EventEmitter.decorate(this);
+  this._onClick = this._onClick.bind(this);
   this._document = parent.ownerDocument;
   this._parent = parent;
   this._splitter = splitter;
   this._splitter.addEventListener("mouseup", () => this.emit("resize"));
+  this._parent.addEventListener("click", this._onClick);
 }
 
 MarkerDetails.prototype = {
   /**
    * Removes any node references from this view.
    */
   destroy: function() {
     this.empty();
+    this._parent.removeEventListener("click", this._onClick);
     this._parent = null;
     this._splitter = null;
   },
 
   /**
    * Clears the view.
    */
   empty: function() {
     this._parent.innerHTML = "";
   },
 
   /**
    * Populates view with marker's details.
    *
    * @param object params
    *        An options object holding:
-   *        toolbox - The toolbox.
    *        marker - The marker to display.
    *        frames - Array of stack frame information; see stack.js.
    */
-  render: function({toolbox: toolbox, marker: marker, frames: frames}) {
+  render: function({ marker, frames }) {
     this.empty();
 
-    // UI for any marker
-
-    let title = MarkerUtils.DOM.buildTitle(this._document, marker);
-    let duration = MarkerUtils.DOM.buildDuration(this._document, marker);
-    let fields = MarkerUtils.DOM.buildFields(this._document, marker);
+    let elements = [];
+    elements.push(MarkerUtils.DOM.buildTitle(this._document, marker));
+    elements.push(MarkerUtils.DOM.buildDuration(this._document, marker));
+    MarkerUtils.DOM.buildFields(this._document, marker).forEach(field => elements.push(field));
 
-    this._parent.appendChild(title);
-    this._parent.appendChild(duration);
-    fields.forEach(field => this._parent.appendChild(field));
-
+    // Build a stack element -- and use the "startStack" label if
+    // we have both a star and endStack.
     if (marker.stack) {
-      let property = "timeline.markerDetail.stack";
-      if (marker.endStack) {
-        property = "timeline.markerDetail.startStack";
-      }
-      this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: property,
-                             frameIndex: marker.stack, frames: frames});
+      let type = marker.endStack ? "startStack" : "stack";
+      elements.push(MarkerUtils.DOM.buildStackTrace(this._document, {
+        frameIndex: marker.stack, frames, type
+      }));
     }
 
-    if (marker.endStack) {
-      this.renderStackTrace({toolbox: toolbox, parent: this._parent, property: "timeline.markerDetail.endStack",
-                             frameIndex: marker.endStack, frames: frames});
-    }
+    elements.forEach(el => this._parent.appendChild(el));
   },
 
   /**
-   * Render a stack trace.
-   *
-   * @param object params
-   *        An options object with the following members:
-   *        object toolbox - The toolbox.
-   *        nsIDOMNode parent - The parent node holding the view.
-   *        string property - String identifier for label's name.
-   *        integer frameIndex - The index of the topmost stack frame.
-   *        array frames - Array of stack frames.
+   * Handles click in the marker details view. Based on the target,
+   * can handle different actions -- only supporting view source links
+   * for the moment.
    */
-  renderStackTrace: function({toolbox: toolbox, parent: parent,
-                              property: property, frameIndex: frameIndex,
-                              frames: frames}) {
-    let labelName = this._document.createElement("label");
-    labelName.className = "plain marker-details-labelname";
-    labelName.setAttribute("value", L10N.getStr(property));
-    parent.appendChild(labelName);
-
-    let wasAsyncParent = false;
-    while (frameIndex > 0) {
-      let frame = frames[frameIndex];
-      let url = frame.source;
-      let displayName = frame.functionDisplayName;
-      let line = frame.line;
-
-      // If the previous frame had an async parent, then the async
-      // cause is in this frame and should be displayed.
-      if (wasAsyncParent) {
-        let asyncBox = this._document.createElement("hbox");
-        let asyncLabel = this._document.createElement("label");
-        asyncLabel.className = "devtools-monospace";
-        asyncLabel.setAttribute("value", L10N.getFormatStr("timeline.markerDetail.asyncStack",
-                                                           frame.asyncCause));
-        asyncBox.appendChild(asyncLabel);
-        parent.appendChild(asyncBox);
-        wasAsyncParent = false;
-      }
-
-      let hbox = this._document.createElement("hbox");
+  _onClick: function (e) {
+    let data = findActionFromEvent(e.target);
+    if (!data) {
+      return;
+    }
 
-      if (displayName) {
-        let functionLabel = this._document.createElement("label");
-        functionLabel.className = "devtools-monospace";
-        functionLabel.setAttribute("value", displayName);
-        hbox.appendChild(functionLabel);
-      }
-
-      if (url) {
-        let aNode = this._document.createElement("a");
-        aNode.className = "waterfall-marker-location theme-link devtools-monospace";
-        aNode.href = url;
-        aNode.draggable = false;
-        aNode.setAttribute("title", url);
-
-        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();
-          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);
-      }
-
-      parent.appendChild(hbox);
-
-      if (frame.asyncParent) {
-        frameIndex = frame.asyncParent;
-        wasAsyncParent = true;
-      } else {
-        frameIndex = frame.parent;
-      }
+    if (data.action === "view-source") {
+      this.emit("view-source", data.url, data.line);
     }
   },
 };
 
 exports.MarkerDetails = MarkerDetails;
+
+/**
+ * Take an element from an event `target`, and asend through
+ * the DOM, looking for an element with a `data-action` attribute. Return
+ * the parsed `data-action` value found, or null if none found before
+ * reaching the parent `container`.
+ *
+ * @param {Element} target
+ * @param {Element} container
+ * @return {?object}
+ */
+function findActionFromEvent (target, container) {
+  let el = target;
+  let action;
+  while (el !== container) {
+    if (action = el.getAttribute("data-action")) {
+      return JSON.parse(action);
+    }
+    el = el.parentNode;
+  }
+  return null;
+}
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -1,13 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
+const MARKER_DETAILS_WIDTH = 300;
+
 /**
  * Waterfall view containing the timeline markers, controlled by DetailsView.
  */
 let WaterfallView = Heritage.extend(DetailsSubview, {
 
   observedPrefs: [
     "hidden-markers"
   ],
@@ -19,16 +21,19 @@ let WaterfallView = Heritage.extend(Deta
   rangeChangeDebounceTime: 75, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
+    // TODO bug 1167093 save the previously set width, and ensure minimum width
+    $("#waterfall-details").setAttribute("width", MARKER_DETAILS_WIDTH);
+
     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);
--- a/browser/devtools/shared/source-utils.js
+++ b/browser/devtools/shared/source-utils.js
@@ -118,11 +118,20 @@ exports.viewSourceInScratchpad = Task.as
  *
  * @param {Toolbox} toolbox
  * @param {string} sourceURL
  * @param {number} sourceLine
  *
  * @return {Promise}
  */
 exports.viewSource = Task.async(function *(toolbox, sourceURL, sourceLine) {
+  // Attempt to access view source via a browser first, which may display it in
+  // a tab, if enabled.
+  let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+  if (browserWin) {
+    return browserWin.BrowserViewSourceOfDocument({
+      URL: sourceURL,
+      lineNumber: sourceLine
+    });
+  }
   let utils = toolbox.gViewSourceUtils;
   utils.viewSource(sourceURL, null, toolbox.doc, sourceLine || 0);
 });
--- a/browser/devtools/shared/test/browser_inplace-editor-01.js
+++ b/browser/devtools/shared/test/browser_inplace-editor-01.js
@@ -106,17 +106,17 @@ function testAdvanceCharsFunction(doc) {
         firstTime = false;
         return false;
       }
 
       // Just to make sure we check it somehow.
       return aText.length > 0;
     },
     start: function(editor) {
-      for each (let ch in ":Test:") {
+      for (let ch of ":Test:") {
         EventUtils.sendChar(ch);
       }
     },
     done: onDone(":Test", true, def)
   }, doc);
 
   return def.promise;
 }
--- a/browser/devtools/shared/test/head.js
+++ b/browser/devtools/shared/test/head.js
@@ -39,17 +39,17 @@ function addTab(aURL, aCallback)
   browser.addEventListener("load", onTabLoad, true);
 }
 
 function promiseTab(aURL) {
   return new Promise(resolve =>
     addTab(aURL, resolve));
 }
 
-registerCleanupFunction(function tearDown() {
+registerCleanupFunction(function* tearDown() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   yield gDevTools.closeToolbox(target);
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 
   console = undefined;
@@ -121,17 +121,19 @@ function waitForValue(aOptions)
     let successful = "value" in aOptions ?
                       lastValue == aOptions.value :
                       lastValue;
     if (successful) {
       ok(true, aOptions.name);
       successFn(aOptions, lastValue);
     }
     else {
-      setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
+      setTimeout(() => {
+        wait(validatorFn, successFn, failureFn);
+      }, 100);
     }
   }
 
   wait(aOptions.validator, aOptions.success, aOptions.failure);
 }
 
 function oneTimeObserve(name, callback) {
   var func = function() {
--- a/browser/devtools/styleinspector/test/browser_computedview_style-editor-link.js
+++ b/browser/devtools/styleinspector/test/browser_computedview_style-editor-link.js
@@ -2,17 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ///////////////////
 //
 // Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
 
 // Test the links from the computed view to the style editor
 
 const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
   [".highlight {",
    "color: blue",
@@ -65,26 +65,26 @@ add_task(function*() {
   yield testExternalStyleSheet(view, toolbox);
 });
 
 function* testInlineStyle(view, inspector) {
   info("Testing inline style");
 
   yield expandComputedViewPropertyByIndex(view, 0);
 
-  let onWindow = waitForWindow();
+  let onTab = waitForTab();
   info("Clicking on the first rule-link in the computed-view");
   clickLinkByIndex(view, 0);
 
-  let win = yield onWindow;
+  let tab = yield onTab;
 
-  let windowType = win.document.documentElement.getAttribute("windowtype");
-  is(windowType, "navigator:view-source", "View source window is open");
-  info("Closing window");
-  win.close();
+  let tabURI = tab.linkedBrowser.documentURI.spec;
+  ok(tabURI.startsWith("view-source:"), "View source tab is open");
+  info("Closing tab");
+  gBrowser.removeTab(tab);
 }
 
 function* testFirstInlineStyleSheet(view, toolbox) {
   info("Testing inline stylesheet");
 
   info("Listening for toolbox switch to the styleeditor");
   let onSwitch = waitForStyleEditor(toolbox);
 
--- a/browser/devtools/styleinspector/test/browser_ruleview_style-editor-link.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_style-editor-link.js
@@ -2,17 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ///////////////////
 //
 // Whitelisting this test.
-// As part of bug 1077403, the leaking uncaught rejection should be fixed. 
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
 //
 thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
 
 // Test the links from the rule-view to the styleeditor
 
 const STYLESHEET_URL = "data:text/css,"+encodeURIComponent(
   ["#first {",
    "color: blue",
@@ -65,26 +65,26 @@ add_task(function*() {
   yield testFirstInlineStyleSheet(view, toolbox);
   yield testSecondInlineStyleSheet(view, toolbox);
   yield testExternalStyleSheet(view, toolbox);
 });
 
 function* testInlineStyle(view, inspector) {
   info("Testing inline style");
 
-  let onWindow = waitForWindow();
+  let onTab = waitForTab();
   info("Clicking on the first link in the rule-view");
   clickLinkByIndex(view, 0);
 
-  let win = yield onWindow;
+  let tab = yield onTab;
 
-  let windowType = win.document.documentElement.getAttribute("windowtype");
-  is(windowType, "navigator:view-source", "View source window is open");
-  info("Closing window");
-  win.close();
+  let tabURI = tab.linkedBrowser.documentURI.spec;
+  ok(tabURI.startsWith("view-source:"), "View source tab is open");
+  info("Closing tab");
+  gBrowser.removeTab(tab);
 }
 
 function* testFirstInlineStyleSheet(view, toolbox) {
   info("Testing inline stylesheet");
 
   info("Listening for toolbox switch to the styleeditor");
   let onSwitch = waitForStyleEditor(toolbox);
 
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -487,16 +487,31 @@ function waitForWindow() {
       def.resolve(win);
     });
   });
 
   return def.promise;
 }
 
 /**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ * @return a promise that resolves to the tab object
+ */
+let waitForTab = Task.async(function*() {
+  info("Waiting for a tab to open");
+  yield once(gBrowser.tabContainer, "TabOpen");
+  let tab = gBrowser.selectedTab;
+  let browser = tab.linkedBrowser;
+  yield once(browser, "load", true);
+  info("The tab load completed");
+  return tab;
+});
+
+/**
  * @see SimpleTest.waitForClipboard
  * @param {Function} setup Function to execute before checking for the
  * clipboard content
  * @param {String|Boolean} expected An expected string or validator function
  * @return a promise that resolves when the expected string has been found or
  * the validator function has returned true, rejects otherwise.
  */
 function waitForClipboard(setup, expected) {
--- a/browser/devtools/webconsole/hudservice.js
+++ b/browser/devtools/webconsole/hudservice.js
@@ -429,16 +429,25 @@ 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) {
+    // Attempt to access view source via a browser first, which may display it in
+    // a tab, if enabled.
+    let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+    if (browserWin) {
+      return browserWin.BrowserViewSourceOfDocument({
+        URL: aSourceURL,
+        lineNumber: 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.
    *
--- a/browser/devtools/webconsole/test/browser_webconsole_view_source.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_view_source.js
@@ -5,78 +5,58 @@
 // standard View Source window.
 
 const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
 
 let getItemForAttachment;
 let Sources;
 let getItemInvoked = false;
 
-function test() {
-  loadTab(TEST_URI).then(() => {
-    openConsole(null).then(testViewSource);
-  });
-}
-
-function testViewSource(hud) {
+add_task(function*() {
+  yield loadTab(TEST_URI);
+  let hud = yield openConsole(null);
   info("console opened");
 
   let button = content.document.querySelector("button");
   ok(button, "we have the button on the page");
 
   expectUncaughtException();
   EventUtils.sendMouseEvent({ type: "click" }, button, content);
 
-  openDebugger().then(({panelWin: { DebuggerView }}) => {
-    info("debugger opened");
-    Sources = DebuggerView.Sources;
-    openConsole().then((hud) => {
-      info("console opened again");
+  let { panelWin: { DebuggerView } } = yield openDebugger();
+  info("debugger opened");
+  Sources = DebuggerView.Sources;
+  hud = yield openConsole();
+  info("console opened again");
 
-      waitForMessages({
-        webconsole: hud,
-        messages: [{
-          text: "fooBazBaz is not defined",
-          category: CATEGORY_JS,
-          severity: SEVERITY_ERROR,
-        }],
-      }).then(onMessage);
-    });
+  let [result] = yield waitForMessages({
+    webconsole: hud,
+    messages: [{
+      text: "fooBazBaz is not defined",
+      category: CATEGORY_JS,
+      severity: SEVERITY_ERROR,
+    }],
   });
 
-  function onMessage([result]) {
-    let msg = [...result.matched][0];
-    ok(msg, "error message");
-    let locationNode = msg.querySelector(".message-location");
-    ok(locationNode, "location node");
-
-    Services.ww.registerNotification(observer);
+  let msg = [...result.matched][0];
+  ok(msg, "error message");
+  let locationNode = msg.querySelector(".message-location");
+  ok(locationNode, "location node");
 
-    getItemForAttachment = Sources.getItemForAttachment;
-    Sources.getItemForAttachment = () => {
-      getItemInvoked = true;
-      return false;
-    };
-
-    EventUtils.sendMouseEvent({ type: "click" }, locationNode);
-  }
-}
+  let onTabOpen = waitForTab();
 
-let observer = {
-  observe: function(aSubject, aTopic, aData) {
-    if (aTopic != "domwindowopened") {
-      return;
-    }
+  getItemForAttachment = Sources.getItemForAttachment;
+  Sources.getItemForAttachment = () => {
+    getItemInvoked = true;
+    return false;
+  };
 
-    ok(true, "the view source window was opened in response to clicking " +
-       "the location node");
+  EventUtils.sendMouseEvent({ type: "click" }, locationNode);
 
-    aSubject.close();
-    ok(getItemInvoked, "custom getItemForAttachment() was invoked");
-    Sources.getItemForAttachment = getItemForAttachment;
-    Sources = getItemForAttachment = null;
-    finishTest();
-  }
-};
+  let tab = yield onTabOpen;
+  ok(true, "the view source tab was opened in response to clicking " +
+           "the location node");
+  gBrowser.removeTab(tab);
 
-registerCleanupFunction(function() {
-  Services.ww.unregisterNotification(observer);
+  ok(getItemInvoked, "custom getItemForAttachment() was invoked");
+  Sources.getItemForAttachment = getItemForAttachment;
+  Sources = getItemForAttachment = null;
 });
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -251,16 +251,31 @@ function waitForContextMenu(aPopup, aBut
   info("wait for the context menu to open");
   let eventDetails = { type: "contextmenu", button: 2};
   EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails,
                              aButton.ownerDocument.defaultView);
   return deferred.promise;
 }
 
 /**
+ * Listen for a new tab to open and return a promise that resolves when one
+ * does and completes the load event.
+ * @return a promise that resolves to the tab object
+ */
+let waitForTab = Task.async(function*() {
+  info("Waiting for a tab to open");
+  yield once(gBrowser.tabContainer, "TabOpen");
+  let tab = gBrowser.selectedTab;
+  let browser = tab.linkedBrowser;
+  yield once(browser, "load", true);
+  info("The tab load completed");
+  return tab;
+});
+
+/**
  * Dump the output of all open Web Consoles - used only for debugging purposes.
  */
 function dumpConsoles()
 {
   if (gPendingOutputTest) {
     console.log("dumpConsoles start");
     for (let [, hud] of HUDService.consoles) {
       if (!hud.outputNode) {
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1131,20 +1131,20 @@ toolbarbutton[constrain-size="true"][cui
 #identity-popup-button-container {
   background: linear-gradient(to bottom, rgba(0,0,0,0.04) 60%, transparent);
   padding: 10px;
   margin-top: 5px;
 }
 
 %include ../shared/notification-icons.inc.css
 
-.popup-notification-description[popupid="addon-progress"],
-.popup-notification-description[popupid="addon-install-confirmation"] {
-  width: 27em;
-  max-width: 27em;
+.popup-notification-body[popupid="addon-progress"],
+.popup-notification-body[popupid="addon-install-confirmation"] {
+  width: 28em;
+  max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
 /* Notification icon box */
 #notification-popup-box {
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3682,20 +3682,20 @@ notification[value="loop-sharing-notific
   list-style-image: url(chrome://browser/skin/webRTC-sharingScreen-menubar.png);
 }
 @media (min-resolution: 2dppx) {
   notification[value="loop-sharing-notification"] .messageImage {
     list-style-image: url(chrome://browser/skin/webRTC-sharingScreen-menubar@2x.png);
   }
 }
 
-.popup-notification-description[popupid="addon-progress"],
-.popup-notification-description[popupid="addon-install-confirmation"] {
-  width: 27em;
-  max-width: 27em;
+.popup-notification-body[popupid="addon-progress"],
+.popup-notification-body[popupid="addon-install-confirmation"] {
+  width: 28em;
+  max-width: 28em;
 }
 
 #addon-progress-notification-progresstext,
 #addon-progress-notification-progressmeter {
   /* override default label margin to match description margin */
   -moz-margin-start: 0;
   -moz-margin-end: 0;
 }
--- a/browser/themes/shared/devtools/common.css
+++ b/browser/themes/shared/devtools/common.css
@@ -1,26 +1,27 @@
 %if 0
 /* 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/. */
 %endif
 
 :root {
   font: message-box;
+%ifdef XP_MACOSX
+  --monospace-font-family: Menlo, monospace;
+%elifdef XP_WIN
+  --monospace-font-family: Consolas, monospace;
+%else
+  --monospace-font-family: monospace;
+%endif
 }
 
 .devtools-monospace {
-%ifdef XP_MACOSX
-  font-family: Menlo, monospace;
-%elifdef XP_WIN
-  font-family: Consolas, monospace;
-%else
-  font-family: monospace;
-%endif
+  font-family: var(--monospace-font-family);
 %if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
   font-size: 80%;
 %endif
 }
 
 /* Splitters */
 .devtools-horizontal-splitter {
   -moz-appearance: none;
@@ -242,8 +243,39 @@
 .devtools-eyedropper-panel {
   pointer-events: none;
   -moz-appearance: none;
   width: 156px;
   height: 120px;
   background-color: transparent;
   border: none;
 }
+
+/* links to source code, like displaying `myfile.js:45` */
+
+.devtools-source-link {
+  font-family: var(--monospace-font-family);
+  color: var(--theme-highlight-blue);
+  cursor: pointer;
+  white-space: nowrap;
+  display: flex;
+  text-decoration: none;
+  font-size: 11px;
+  width: 12em; /* probably should be changed for each tool */
+}
+
+.devtools-source-link:hover {
+  text-decoration: underline;
+}
+
+.devtools-source-link > .filename {
+  text-overflow: ellipsis;
+  text-align: end;
+  overflow: hidden;
+  margin: 2px 0px;
+  cursor: pointer;
+}
+
+.devtools-source-link > .line-number {
+  flex: none;
+  margin: 2px 0px;
+  cursor: pointer;
+}
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -452,25 +452,16 @@
 }
 
 .waterfall-marker-container.selected > .waterfall-sidebar,
 .waterfall-marker-container.selected > .waterfall-marker-item {
   background-color: var(--theme-selection-background);
   color: var(--theme-selection-color);
 }
 
-.waterfall-marker-location {
-   color: -moz-nativehyperlinktext;
-}
-
-.waterfall-marker-location:hover,
-.waterfall-marker-location:focus {
-   text-decoration: underline;
-}
-
 #waterfall-details {
   -moz-padding-start: 8px;
   -moz-padding-end: 8px;
   padding-top: 2vh;
   overflow: auto;
   min-width: 50px;
 }
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2148,20 +2148,20 @@ toolbarbutton.bookmark-item[dragover="tr
 #identity-popup-button-container {
   background: linear-gradient(to bottom, rgba(0,0,0,0.04) 60%, transparent);
   padding: 10px;
   margin-top: 5px;
 }
 
 %include ../shared/notification-icons.inc.css
 
-.popup-notification-description[popupid="addon-progress"],
-.popup-notification-description[popupid="addon-install-confirmation"] {
-  width: 27em;
-  max-width: 27em;
+.popup-notification-body[popupid="addon-progress"],
+.popup-notification-body[popupid="addon-install-confirmation"] {
+  width: 28em;
+  max-width: 28em;
 }
 
 .addon-install-confirmation-name {
   font-weight: bold;
 }
 
 /* Notification icon box */
 #notification-popup-box {
--- a/layout/style/viewsource.css
+++ b/layout/style/viewsource.css
@@ -35,62 +35,62 @@ span[id]:before {
   display: inline-block;
   width: 5ch;
   margin: 0 0 0 -5ch;
   text-align: right;
   color: #ccc;
   font-weight: normal;
   font-style: normal;
 }
-.start-tag {
+.highlight .start-tag {
  color: purple;
  font-weight: bold;
 }
-.end-tag {
+.highlight .end-tag {
  color: purple;
  font-weight: bold;
 }
-.comment {
+.highlight .comment {
  color: green;
  font-style: italic;
 }
-.cdata {
+.highlight .cdata {
  color: #CC0066;
 }
-.doctype {
+.highlight .doctype {
  color: steelblue;
  font-style: italic;
 }
-.pi {
+.highlight .pi {
  color: orchid;
  font-style: italic;
 }
-.entity {
- color:#FF4500;
+.highlight .entity {
+ color: #FF4500;
  font-weight: normal;
 }
-.text {
+.highlight .text {
   font-weight: normal;
 }
-.attribute-name {
+.highlight .attribute-name {
  color: black;
  font-weight: bold;
 }
-.attribute-value {
+.highlight .attribute-value {
  color: blue;
  font-weight: normal;
 }
-.markupdeclaration {
+.highlight .markupdeclaration {
  color: steelblue;
  font-style: italic;
 }
 span:not(.error), a:not(.error) {
  unicode-bidi: embed;
 }
 span[id] {
  unicode-bidi: -moz-isolate;
 }
-.error, 
-.error > :-moz-any(.start-tag, .end-tag, .comment, .cdata, .doctype, .pi,
-                   .entity, .attribute-name, .attribute-value) {
+.highlight .error,
+.highlight .error > :-moz-any(.start-tag, .end-tag, .comment, .cdata, .doctype,
+                              .pi, .entity, .attribute-name, .attribute-value) {
   color: red;
   font-weight: bold;
 }
--- a/parser/html/nsHtml5Highlighter.cpp
+++ b/parser/html/nsHtml5Highlighter.cpp
@@ -48,18 +48,16 @@ char16_t nsHtml5Highlighter::sPi[] =
 nsHtml5Highlighter::nsHtml5Highlighter(nsAHtml5TreeOpSink* aOpSink)
  : mState(NS_HTML5TOKENIZER_DATA)
  , mCStart(INT32_MAX)
  , mPos(0)
  , mLineNumber(1)
  , mInlinesOpen(0)
  , mInCharacters(false)
  , mBuffer(nullptr)
- , mSyntaxHighlight(Preferences::GetBool("view_source.syntax_highlight",
-                                         true))
  , mOpSink(aOpSink)
  , mCurrentRun(nullptr)
  , mAmpersand(nullptr)
  , mSlash(nullptr)
  , mHandles(new nsIContent*[NS_HTML5_HIGHLIGHTER_HANDLE_ARRAY_LENGTH])
  , mHandlesUsed(0)
  , mSeenBase(false)
 {
@@ -708,19 +706,16 @@ nsHtml5Highlighter::AppendCharacters(con
                                  aLength,
                                  CurrentNode());
 }
 
 
 void
 nsHtml5Highlighter::AddClass(const char16_t* aClass)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   mOpQueue.AppendElement()->InitAddClass(CurrentNode(), aClass);
 }
 
 void
 nsHtml5Highlighter::AddViewSourceHref(const nsString& aValue)
 {
   char16_t* bufferCopy = new char16_t[aValue.Length() + 1];
   memcpy(bufferCopy, aValue.get(), aValue.Length() * sizeof(char16_t));
@@ -746,78 +741,60 @@ nsHtml5Highlighter::AddBase(const nsStri
   mOpQueue.AppendElement()->Init(eTreeOpAddViewSourceBase,
                                  bufferCopy,
                                  aValue.Length());
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentNode(const char* aMsgId)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(CurrentNode(), aMsgId);
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   NS_PRECONDITION(mCurrentRun, "Adding error to run without one!");
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(mCurrentRun, aMsgId);
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId,
                                          nsIAtom* aName)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   NS_PRECONDITION(mCurrentRun, "Adding error to run without one!");
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(mCurrentRun, aMsgId, aName);
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentRun(const char* aMsgId,
                                          nsIAtom* aName,
                                          nsIAtom* aOther)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   NS_PRECONDITION(mCurrentRun, "Adding error to run without one!");
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(mCurrentRun, aMsgId, aName, aOther);
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentAmpersand(const char* aMsgId)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   NS_PRECONDITION(mAmpersand, "Adding error to ampersand without one!");
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(mAmpersand, aMsgId);
 }
 
 void
 nsHtml5Highlighter::AddErrorToCurrentSlash(const char* aMsgId)
 {
-  if (!mSyntaxHighlight) {
-    return;
-  }
   NS_PRECONDITION(mSlash, "Adding error to slash without one!");
   nsHtml5TreeOperation* treeOp = mOpQueue.AppendElement();
   NS_ASSERTION(treeOp, "Tree op allocation failed.");
   treeOp->Init(mSlash, aMsgId);
 }
--- a/parser/html/nsHtml5Highlighter.h
+++ b/parser/html/nsHtml5Highlighter.h
@@ -309,21 +309,16 @@ class nsHtml5Highlighter
     bool mInCharacters;
 
     /**
      * The current buffer being tokenized.
      */
     nsHtml5UTF16Buffer* mBuffer;
 
     /**
-     * Whether to highlight syntax visibly initially.
-     */
-    bool mSyntaxHighlight;
-
-    /**
      * The outgoing tree op queue.
      */
     nsTArray<nsHtml5TreeOperation> mOpQueue;
 
     /**
      * The tree op stage for the tree op executor.
      */
     nsAHtml5TreeOpSink* mOpSink;
--- a/parser/html/nsHtml5ViewSourceUtils.cpp
+++ b/parser/html/nsHtml5ViewSourceUtils.cpp
@@ -10,18 +10,24 @@
 // static
 nsHtml5HtmlAttributes*
 nsHtml5ViewSourceUtils::NewBodyAttributes()
 {
   nsHtml5HtmlAttributes* bodyAttrs = new nsHtml5HtmlAttributes(0);
   nsString* id = new nsString(NS_LITERAL_STRING("viewsource"));
   bodyAttrs->addAttribute(nsHtml5AttributeName::ATTR_ID, id);
 
+  nsString* klass = new nsString();
   if (mozilla::Preferences::GetBool("view_source.wrap_long_lines", true)) {
-    nsString* klass = new nsString(NS_LITERAL_STRING("wrap"));
+    klass->Append(NS_LITERAL_STRING("wrap "));
+  }
+  if (mozilla::Preferences::GetBool("view_source.syntax_highlight", true)) {
+    klass->Append(NS_LITERAL_STRING("highlight"));
+  }
+  if (!klass->IsEmpty()) {
     bodyAttrs->addAttribute(nsHtml5AttributeName::ATTR_CLASS, klass);
   }
 
   int32_t tabSize = mozilla::Preferences::GetInt("view_source.tab_size", 4);
   if (tabSize > 0) {
     nsString* style = new nsString(NS_LITERAL_STRING("-moz-tab-size: "));
     style->AppendInt(tabSize);
     bodyAttrs->addAttribute(nsHtml5AttributeName::ATTR_STYLE, style);
--- a/parser/htmlparser/tests/reftest/bug482921-1-ref.html
+++ b/parser/htmlparser/tests/reftest/bug482921-1-ref.html
@@ -1,9 +1,9 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap highlight"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">html</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">head</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">title</span>&gt;</span><span>Title</span><span>&lt;/<span class="end-tag">title</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">script</span>&gt;</span>
 <span id></span>var lt = "&lt;";
 <span id></span>&lt;!--
 <span id></span>var s = "&lt;script&gt;foo&lt;/script&gt;";
 <span id></span>--&gt;
--- a/parser/htmlparser/tests/reftest/bug482921-2-ref.html
+++ b/parser/htmlparser/tests/reftest/bug482921-2-ref.html
@@ -1,9 +1,9 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap"><pre id><span class="pi">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap highlight"><pre id><span class="pi">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
 <span id></span><span class="pi">&lt;?foo bar?&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">html</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">head</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">title</span>&gt;</span><span>Title</span><span>&lt;/<span class="end-tag">title</span>&gt;</span>
 <span id></span><span>&lt;<span class="start-tag">script</span>&gt;</span>
 <span id></span>var s = "<span>&lt;<span class="start-tag">script</span>&gt;</span><span>foo</span><span>&lt;/<span class="end-tag">script</span>&gt;</span>";
 <span id></span><span class="comment">&lt;!--
 <span id></span>var s = "&lt;script&gt;foo&lt;/script&gt;";
--- a/parser/htmlparser/tests/reftest/bug535530-2-ref.html
+++ b/parser/htmlparser/tests/reftest/bug535530-2-ref.html
@@ -1,9 +1,9 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap highlight"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span>
 <span id></span>XX<span class="error">&amp;</span>XX
 <span id></span>XX<span class="error">&amp;</span>nXX
 <span id></span>XX<span class="error">&amp;</span>noXX
 <span id></span>XX<span class="error entity">&amp;not</span>XX
 <span id></span>XX<span class="error entity">&amp;noti</span>XX
 <span id></span>XX<span class="error entity">&amp;notin</span>XX
 <span id></span>XX<span class="error">&amp;</span>;XX
 <span id></span>XX<span class="error">&amp;</span>n;XX
--- a/parser/htmlparser/tests/reftest/bug704667-1-ref.html
+++ b/parser/htmlparser/tests/reftest/bug704667-1-ref.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap"><pre id><span class="error comment">&lt;!--&gt;</span> <span class="error comment">&lt;!X&gt;</span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap highlight"><pre id><span class="error comment">&lt;!--&gt;</span> <span class="error comment">&lt;!X&gt;</span>
 <span id></span>
 </pre>
 <!-- View source CSS matches the <pre id> and <span id> elements and produces line numbers. -->
--- a/parser/htmlparser/tests/reftest/bug731234-1-ref.html
+++ b/parser/htmlparser/tests/reftest/bug731234-1-ref.html
@@ -1,9 +1,9 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span><span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="wrap highlight"><pre id><span class="doctype">&lt;!DOCTYPE html&gt;</span><span>
 <span id></span></span><span>&lt;<span class="start-tag">body</span>&gt;</span><span>
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span>&lt;/<span class="end-tag">script</span>&gt;</span><span>X
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span>&lt;/<span class="end-tag">script</span> &gt;</span><span>X
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span>&lt;/<span class="end-tag">script</span>
 <span id></span>&gt;</span><span>X
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span title="End tag had attributes." class="error">&lt;/<span class="end-tag">script</span> <span class="attribute-name">foo</span>&gt;</span><span>X
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span title="End tag had attributes." class="error">&lt;/<span class="end-tag">script</span> <span class="attribute-name">foo</span>=<a class="attribute-value">bar</a>&gt;</span><span>X
 <span id></span></span><span>&lt;<span class="start-tag">script</span>&gt;</span><span></span><span title="End tag had attributes." class="error">&lt;/<span class="end-tag">script</span> <span class="attribute-name">foo</span>="<a class="attribute-value">bar</a>"&gt;</span><span>X
--- a/parser/htmlparser/tests/reftest/bug910588-1-ref.html
+++ b/parser/htmlparser/tests/reftest/bug910588-1-ref.html
@@ -1,2 +1,2 @@
-<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" style="-moz-tab-size: 4"><pre id="line1"><span></span><span class="doctype">&lt;!DOCTYPE html&gt;</span><span></span><span>&lt;<span class="start-tag">table</span>&gt;</span><span></span><span title="Start tag “input” seen in “table”." class="error">&lt;<span class="start-tag">input</span> <span class="attribute-name">type</span>=<a class="attribute-value">hidden</a>&gt;</span><span></span><span>&lt;/<span class="end-tag">table</span>&gt;</span><span>
+<!DOCTYPE html><html><head><title></title><link rel="stylesheet" type="text/css" href="resource://gre-resources/viewsource.css"></head><body id="viewsource" class="highlight" style="-moz-tab-size: 4"><pre id="line1"><span></span><span class="doctype">&lt;!DOCTYPE html&gt;</span><span></span><span>&lt;<span class="start-tag">table</span>&gt;</span><span></span><span title="Start tag “input” seen in “table”." class="error">&lt;<span class="start-tag">input</span> <span class="attribute-name">type</span>=<a class="attribute-value">hidden</a>&gt;</span><span></span><span>&lt;/<span class="end-tag">table</span>&gt;</span><span>
 <span id="line2"></span></span></pre></body></html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/viewsource/ViewSourceBrowser.jsm
@@ -0,0 +1,627 @@
+// -*- 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");
+
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
+const VIEW_SOURCE_CSS = "resource://gre-resources/viewsource.css";
+const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
+
+// These are markers used to delimit the selection during processing. They
+// are removed from the final rendering.
+// We use noncharacter Unicode codepoints to minimize the risk of clashing
+// with anything that might legitimately be present in the document.
+// U+FDD0..FDEF <noncharacters>
+const MARK_SELECTION_START = "\uFDD0";
+const MARK_SELECTION_END = "\uFDEF";
+
+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;
+  },
+
+  /**
+   * 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,
+
+  /**
+   * 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.
+   */
+  messages: [
+    "ViewSource:PromptAndGoToLine",
+    "ViewSource:GoToLine:Success",
+    "ViewSource:GoToLine:Failed",
+  ],
+
+  /**
+   * 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;
+
+    switch(message.name) {
+      case "ViewSource:PromptAndGoToLine":
+        this.promptAndGoToLine();
+        break;
+      case "ViewSource:GoToLine:Success":
+        this.onGoToLineSuccess(data.lineNumber);
+        break;
+      case "ViewSource:GoToLine:Failed":
+        this.onGoToLineFailed();
+        break;
+    }
+  },
+
+  /**
+   * 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);
+  },
+
+  /**
+   * Getter for the nsIWebNavigation of the view source browser.
+   */
+  get webNav() {
+    return this.browser.webNavigation;
+  },
+
+  /**
+   * Getter for whether long lines should be wrapped.
+   */
+  get wrapLongLines() {
+    return Services.prefs.getBoolPref("view_source.wrap_long_lines");
+  },
+
+  /**
+   * A getter for the view source string bundle.
+   */
+  get bundle() {
+    if (this._bundle) {
+      return this._bundle;
+    }
+    return this._bundle = Services.strings.createBundle(BUNDLE_URL);
+  },
+
+  /**
+   * 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");
+    }
+  },
+
+  /**
+   * Load the view source browser from a selection in some document.
+   *
+   * @param selection
+   *        A Selection object for the content of interest.
+   */
+  loadViewSourceFromSelection(selection) {
+    var range = selection.getRangeAt(0);
+    var ancestorContainer = range.commonAncestorContainer;
+    var doc = ancestorContainer.ownerDocument;
+
+    var startContainer = range.startContainer;
+    var endContainer = range.endContainer;
+    var startOffset = range.startOffset;
+    var endOffset = range.endOffset;
+
+    // let the ancestor be an element
+    var Node = doc.defaultView.Node;
+    if (ancestorContainer.nodeType == Node.TEXT_NODE ||
+        ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
+      ancestorContainer = ancestorContainer.parentNode;
+
+    // for selectAll, let's use the entire document, including <html>...</html>
+    // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
+    try {
+      if (ancestorContainer == doc.body)
+        ancestorContainer = doc.documentElement;
+    } catch (e) { }
+
+    // each path is a "child sequence" (a.k.a. "tumbler") that
+    // descends from the ancestor down to the boundary point
+    var startPath = this._getPath(ancestorContainer, startContainer);
+    var endPath = this._getPath(ancestorContainer, endContainer);
+
+    // clone the fragment of interest and reset everything to be relative to it
+    // note: it is with the clone that we operate/munge from now on.  Also note
+    // that we clone into a data document to prevent images in the fragment from
+    // loading and the like.  The use of importNode here, as opposed to adoptNode,
+    // is _very_ important.
+    // XXXbz wish there were a less hacky way to create an untrusted document here
+    var isHTML = (doc.createElement("div").tagName == "DIV");
+    var dataDoc = isHTML ?
+      ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
+      ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
+    ancestorContainer = dataDoc.importNode(ancestorContainer, true);
+    startContainer = ancestorContainer;
+    endContainer = ancestorContainer;
+
+    // Only bother with the selection if it can be remapped. Don't mess with
+    // leaf elements (such as <isindex>) that secretly use anynomous content
+    // for their display appearance.
+    var canDrawSelection = ancestorContainer.hasChildNodes();
+    var tmpNode;
+    if (canDrawSelection) {
+      var i;
+      for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
+        startContainer = startContainer.childNodes.item(startPath[i]);
+      }
+      for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
+        endContainer = endContainer.childNodes.item(endPath[i]);
+      }
+
+      // add special markers to record the extent of the selection
+      // note: |startOffset| and |endOffset| are interpreted either as
+      // offsets in the text data or as child indices (see the Range spec)
+      // (here, munging the end point first to keep the start point safe...)
+      if (endContainer.nodeType == Node.TEXT_NODE ||
+          endContainer.nodeType == Node.CDATA_SECTION_NODE) {
+        // do some extra tweaks to try to avoid the view-source output to look like
+        // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
+        // To get a neat output, the idea here is to remap the end point from:
+        // 1. ...<tag>]...   to   ...]<tag>...
+        // 2. ...]</tag>...  to   ...</tag>]...
+        if ((endOffset > 0 && endOffset < endContainer.data.length) ||
+            !endContainer.parentNode || !endContainer.parentNode.parentNode)
+          endContainer.insertData(endOffset, MARK_SELECTION_END);
+        else {
+          tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
+          endContainer = endContainer.parentNode;
+          if (endOffset === 0)
+            endContainer.parentNode.insertBefore(tmpNode, endContainer);
+          else
+            endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
+        }
+      }
+      else {
+        tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
+        endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
+      }
+
+      if (startContainer.nodeType == Node.TEXT_NODE ||
+          startContainer.nodeType == Node.CDATA_SECTION_NODE) {
+        // do some extra tweaks to try to avoid the view-source output to look like
+        // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
+        // To get a neat output, the idea here is to remap the start point from:
+        // 1. ...<tag>[...   to   ...[<tag>...
+        // 2. ...[</tag>...  to   ...</tag>[...
+        if ((startOffset > 0 && startOffset < startContainer.data.length) ||
+            !startContainer.parentNode || !startContainer.parentNode.parentNode ||
+            startContainer != startContainer.parentNode.lastChild)
+          startContainer.insertData(startOffset, MARK_SELECTION_START);
+        else {
+          tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
+          startContainer = startContainer.parentNode;
+          if (startOffset === 0)
+            startContainer.parentNode.insertBefore(tmpNode, startContainer);
+          else
+            startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
+        }
+      }
+      else {
+        tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
+        startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
+      }
+    }
+
+    // now extract and display the syntax highlighted source
+    tmpNode = dataDoc.createElementNS(NS_XHTML, "div");
+    tmpNode.appendChild(ancestorContainer);
+
+    // Tell content to draw a selection after the load below
+    if (canDrawSelection) {
+      this.sendAsyncMessage("ViewSource:ScheduleDrawSelection");
+    }
+
+    // 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;
+    this.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 helper to get a path like FIXptr, but with an array instead of the
+   * "tumbler" notation.
+   * See FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
+   */
+  _getPath(ancestor, node) {
+    var n = node;
+    var p = n.parentNode;
+    if (n == ancestor || !p)
+      return null;
+    var path = new Array();
+    if (!path)
+      return null;
+    do {
+      for (var i = 0; i < p.childNodes.length; i++) {
+        if (p.childNodes.item(i) == n) {
+          path.push(i);
+          break;
+        }
+      }
+      n = p;
+      p = n.parentNode;
+    } while (n != ancestor && p);
+    return path;
+  },
+
+  /**
+   * Load the view source browser from a fragment of some document, as in
+   * markups such as MathML where reformatting the output is helpful.
+   *
+   * @param aNode
+   *        Some element within the fragment of interest.
+   * @param aContext
+   *        A string denoting the type of fragment.  Currently, "mathml" is the
+   *        only accepted value.
+   */
+  loadViewSourceFromFragment(node, context) {
+    var Node = node.ownerDocument.defaultView.Node;
+    this._lineCount = 0;
+    this._startTargetLine = 0;
+    this._endTargetLine = 0;
+    this._targetNode = node;
+    if (this._targetNode && this._targetNode.nodeType == Node.TEXT_NODE)
+      this._targetNode = this._targetNode.parentNode;
+
+    // walk up the tree to the top-level element (e.g., <math>, <svg>)
+    var topTag;
+    if (context == "mathml")
+      topTag = "math";
+    else
+      throw "not reached";
+    var topNode = this._targetNode;
+    while (topNode && topNode.localName != topTag)
+      topNode = topNode.parentNode;
+    if (!topNode)
+      return;
+
+    // serialize
+    var title = this.bundle.GetStringFromName("viewMathMLSourceTitle");
+    var wrapClass = this.wrapLongLines ? ' class="wrap"' : '';
+    var source =
+      '<!DOCTYPE html>'
+    + '<html>'
+    + '<head><title>' + title + '</title>'
+    + '<link rel="stylesheet" type="text/css" href="' + VIEW_SOURCE_CSS + '">'
+    + '<style type="text/css">'
+    + '#target { border: dashed 1px; background-color: lightyellow; }'
+    + '</style>'
+    + '</head>'
+    + '<body id="viewsource"' + wrapClass
+    +        ' onload="document.title=\''+title+'\'; document.getElementById(\'target\').scrollIntoView(true)">'
+    + '<pre>'
+    + this._getOuterMarkup(topNode, 0)
+    + '</pre></body></html>'
+    ; // end
+
+    // display
+    this.browser.loadURI("data:text/html;charset=utf-8," +
+                         encodeURIComponent(source));
+  },
+
+  _getInnerMarkup(node, indent) {
+    var str = '';
+    for (var i = 0; i < node.childNodes.length; i++) {
+      str += this._getOuterMarkup(node.childNodes.item(i), indent);
+    }
+    return str;
+  },
+
+  _getOuterMarkup(node, indent) {
+    var Node = node.ownerDocument.defaultView.Node;
+    var newline = "";
+    var padding = "";
+    var str = "";
+    if (node == this._targetNode) {
+      this._startTargetLine = this._lineCount;
+      str += '</pre><pre id="target">';
+    }
+
+    switch (node.nodeType) {
+    case Node.ELEMENT_NODE: // Element
+      // to avoid the wide gap problem, '\n' is not emitted on the first
+      // line and the lines before & after the <pre id="target">...</pre>
+      if (this._lineCount > 0 &&
+          this._lineCount != this._startTargetLine &&
+          this._lineCount != this._endTargetLine) {
+        newline = "\n";
+      }
+      this._lineCount++;
+      for (var k = 0; k < indent; k++) {
+        padding += " ";
+      }
+      str += newline + padding
+          +  '&lt;<span class="start-tag">' + node.nodeName + '</span>';
+      for (var i = 0; i < node.attributes.length; i++) {
+        var attr = node.attributes.item(i);
+        if (attr.nodeName.match(/^[-_]moz/)) {
+          continue;
+        }
+        str += ' <span class="attribute-name">'
+            +  attr.nodeName
+            +  '</span>=<span class="attribute-value">"'
+            +  this._unicodeToEntity(attr.nodeValue)
+            +  '"</span>';
+      }
+      if (!node.hasChildNodes()) {
+        str += "/&gt;";
+      }
+      else {
+        str += "&gt;";
+        var oldLine = this._lineCount;
+        str += this._getInnerMarkup(node, indent + 2);
+        if (oldLine == this._lineCount) {
+          newline = "";
+          padding = "";
+        }
+        else {
+          newline = (this._lineCount == this._endTargetLine) ? "" : "\n";
+          this._lineCount++;
+        }
+        str += newline + padding
+            +  '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
+      }
+      break;
+    case Node.TEXT_NODE: // Text
+      var tmp = node.nodeValue;
+      tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
+      tmp = tmp.replace(/^ +/, "");
+      tmp = tmp.replace(/ +$/, "");
+      if (tmp.length != 0) {
+        str += '<span class="text">' + this._unicodeToEntity(tmp) + '</span>';
+      }
+      break;
+    default:
+      break;
+    }
+
+    if (node == this._targetNode) {
+      this._endTargetLine = this._lineCount;
+      str += '</pre><pre>';
+    }
+    return str;
+  },
+
+  _unicodeToEntity(text) {
+    const charTable = {
+      '&': '&amp;<span class="entity">amp;</span>',
+      '<': '&amp;<span class="entity">lt;</span>',
+      '>': '&amp;<span class="entity">gt;</span>',
+      '"': '&amp;<span class="entity">quot;</span>'
+    };
+
+    function charTableLookup(letter) {
+      return charTable[letter];
+    }
+
+    function convertEntity(letter) {
+      try {
+        var unichar = this._entityConverter
+                          .ConvertToEntity(letter, entityVersion);
+        var entity = unichar.substring(1); // extract '&'
+        return '&amp;<span class="entity">' + entity + '</span>';
+      } catch (ex) {
+        return letter;
+      }
+    }
+
+    if (!this._entityConverter) {
+      try {
+        this._entityConverter = Cc["@mozilla.org/intl/entityconverter;1"]
+                                  .createInstance(Ci.nsIEntityConverter);
+      } catch(e) { }
+    }
+
+    const entityVersion = Ci.nsIEntityConverter.entityW3C;
+
+    var str = text;
+
+    // replace chars in our charTable
+    str = str.replace(/[<>&"]/g, charTableLookup);
+
+    // replace chars > 0x7f via nsIEntityConverter
+    str = str.replace(/[^\0-\u007f]/g, convertEntity);
+
+    return str;
+  },
+
+  /**
+   * Opens the "Go to line" prompt for a user to hop to a particular line
+   * of the source code they're viewing. This will keep prompting until the
+   * user either cancels out of the prompt, or enters a valid line number.
+   */
+  promptAndGoToLine() {
+    let input = { value: this.lastLineFound };
+    let window = Services.wm.getMostRecentWindow(null);
+
+    let ok = Services.prompt.prompt(
+        window,
+        this.bundle.GetStringFromName("goToLineTitle"),
+        this.bundle.GetStringFromName("goToLineText"),
+        input,
+        null,
+        {value:0});
+
+    if (!ok)
+      return;
+
+    let line = parseInt(input.value, 10);
+
+    if (!(line > 0)) {
+      Services.prompt.alert(window,
+                            this.bundle.GetStringFromName("invalidInputTitle"),
+                            this.bundle.GetStringFromName("invalidInputText"));
+      this.promptAndGoToLine();
+    } else {
+      this.goToLine(line);
+    }
+  },
+
+  /**
+   * 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.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.
+   */
+  onGoToLineSuccess(lineNumber) {
+    // We'll pre-populate the "Go to line" prompt with this value the next
+    // time it comes up.
+    this.lastLineFound = lineNumber;
+  },
+
+  /**
+   * Called when the frame script reports that we failed to go to a particular
+   * line. This informs the user that their selection was likely out of range,
+   * and then reprompts the user to try again.
+   */
+  onGoToLineFailed() {
+    let window = Services.wm.getMostRecentWindow(null);
+    Services.prompt.alert(window,
+                          this.bundle.GetStringFromName("outOfRangeTitle"),
+                          this.bundle.GetStringFromName("outOfRangeText"));
+    this.promptAndGoToLine();
+  },
+};
--- a/toolkit/components/viewsource/content/viewPartialSource.js
+++ b/toolkit/components/viewsource/content/viewPartialSource.js
@@ -1,484 +1,25 @@
 // -*- 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
-var gDebug = 0;
-var gLineCount = 0;
-var gStartTargetLine = 0;
-var gEndTargetLine = 0;
-var gTargetNode = null;
-
-var gEntityConverter = null;
-var gWrapLongLines = false;
-const gViewSourceCSS = 'resource://gre-resources/viewsource.css';
-const NS_XHTML = 'http://www.w3.org/1999/xhtml';
-
-// These are markers used to delimit the selection during processing. They
-// are removed from the final rendering.
-// We use noncharacter Unicode codepoints to minimize the risk of clashing
-// with anything that might legitimately be present in the document.
-// U+FDD0..FDEF <noncharacters>
-const MARK_SELECTION_START = '\uFDD0';
-const MARK_SELECTION_END = '\uFDEF';
-
-function onLoadViewPartialSource()
-{
+function onLoadViewPartialSource() {
   // check the view_source.wrap_long_lines pref
   // and set the menuitem's checked attribute accordingly
-  gWrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
-  document.getElementById("menu_wrapLongLines").setAttribute("checked", gWrapLongLines);
+  let wrapLongLines = Services.prefs.getBoolPref("view_source.wrap_long_lines");
+  document.getElementById("menu_wrapLongLines")
+          .setAttribute("checked", wrapLongLines);
   document.getElementById("menu_highlightSyntax")
           .setAttribute("checked",
                         Services.prefs.getBoolPref("view_source.syntax_highlight"));
 
   if (window.arguments[3] == 'selection')
-    viewPartialSourceForSelection(window.arguments[2]);
+    viewSourceChrome.loadViewSourceFromSelection(window.arguments[2]);
   else
-    viewPartialSourceForFragment(window.arguments[2], window.arguments[3]);
-
-  gBrowser.droppedLinkHandler = function (event, url, name) {
-    viewSource(url)
-    event.preventDefault();
-  }
+    viewSourceChrome.loadViewSourceFromFragment(window.arguments[2], window.arguments[3]);
 
   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)
-{
-  var range = selection.getRangeAt(0);
-  var ancestorContainer = range.commonAncestorContainer;
-  var doc = ancestorContainer.ownerDocument;
-
-  var startContainer = range.startContainer;
-  var endContainer = range.endContainer;
-  var startOffset = range.startOffset;
-  var endOffset = range.endOffset;
-
-  // let the ancestor be an element
-  if (ancestorContainer.nodeType == Node.TEXT_NODE ||
-      ancestorContainer.nodeType == Node.CDATA_SECTION_NODE)
-    ancestorContainer = ancestorContainer.parentNode;
-
-  // for selectAll, let's use the entire document, including <html>...</html>
-  // @see nsDocumentViewer::SelectAll() for how selectAll is implemented
-  try {
-    if (ancestorContainer == doc.body)
-      ancestorContainer = doc.documentElement;
-  } catch (e) { }
-
-  // each path is a "child sequence" (a.k.a. "tumbler") that
-  // descends from the ancestor down to the boundary point
-  var startPath = getPath(ancestorContainer, startContainer);
-  var endPath = getPath(ancestorContainer, endContainer);
-
-  // clone the fragment of interest and reset everything to be relative to it
-  // note: it is with the clone that we operate/munge from now on.  Also note
-  // that we clone into a data document to prevent images in the fragment from
-  // loading and the like.  The use of importNode here, as opposed to adoptNode,
-  // is _very_ important.
-  // XXXbz wish there were a less hacky way to create an untrusted document here
-  var isHTML = (doc.createElement("div").tagName == "DIV");
-  var dataDoc = isHTML ? 
-    ancestorContainer.ownerDocument.implementation.createHTMLDocument("") :
-    ancestorContainer.ownerDocument.implementation.createDocument("", "", null);
-  ancestorContainer = dataDoc.importNode(ancestorContainer, true);
-  startContainer = ancestorContainer;
-  endContainer = ancestorContainer;
-
-  // Only bother with the selection if it can be remapped. Don't mess with
-  // leaf elements (such as <isindex>) that secretly use anynomous content
-  // for their display appearance.
-  var canDrawSelection = ancestorContainer.hasChildNodes();
-  if (canDrawSelection) {
-    var i;
-    for (i = startPath ? startPath.length-1 : -1; i >= 0; i--) {
-      startContainer = startContainer.childNodes.item(startPath[i]);
-    }
-    for (i = endPath ? endPath.length-1 : -1; i >= 0; i--) {
-      endContainer = endContainer.childNodes.item(endPath[i]);
-    }
-
-    // add special markers to record the extent of the selection
-    // note: |startOffset| and |endOffset| are interpreted either as
-    // offsets in the text data or as child indices (see the Range spec)
-    // (here, munging the end point first to keep the start point safe...)
-    var tmpNode;
-    if (endContainer.nodeType == Node.TEXT_NODE ||
-        endContainer.nodeType == Node.CDATA_SECTION_NODE) {
-      // do some extra tweaks to try to avoid the view-source output to look like
-      // ...<tag>]... or ...]</tag>... (where ']' marks the end of the selection).
-      // To get a neat output, the idea here is to remap the end point from:
-      // 1. ...<tag>]...   to   ...]<tag>...
-      // 2. ...]</tag>...  to   ...</tag>]...
-      if ((endOffset > 0 && endOffset < endContainer.data.length) ||
-          !endContainer.parentNode || !endContainer.parentNode.parentNode)
-        endContainer.insertData(endOffset, MARK_SELECTION_END);
-      else {
-        tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
-        endContainer = endContainer.parentNode;
-        if (endOffset == 0)
-          endContainer.parentNode.insertBefore(tmpNode, endContainer);
-        else
-          endContainer.parentNode.insertBefore(tmpNode, endContainer.nextSibling);
-      }
-    }
-    else {
-      tmpNode = dataDoc.createTextNode(MARK_SELECTION_END);
-      endContainer.insertBefore(tmpNode, endContainer.childNodes.item(endOffset));
-    }
-
-    if (startContainer.nodeType == Node.TEXT_NODE ||
-        startContainer.nodeType == Node.CDATA_SECTION_NODE) {
-      // do some extra tweaks to try to avoid the view-source output to look like
-      // ...<tag>[... or ...[</tag>... (where '[' marks the start of the selection).
-      // To get a neat output, the idea here is to remap the start point from:
-      // 1. ...<tag>[...   to   ...[<tag>...
-      // 2. ...[</tag>...  to   ...</tag>[...
-      if ((startOffset > 0 && startOffset < startContainer.data.length) ||
-          !startContainer.parentNode || !startContainer.parentNode.parentNode ||
-          startContainer != startContainer.parentNode.lastChild)
-        startContainer.insertData(startOffset, MARK_SELECTION_START);
-      else {
-        tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
-        startContainer = startContainer.parentNode;
-        if (startOffset == 0)
-          startContainer.parentNode.insertBefore(tmpNode, startContainer);
-        else
-          startContainer.parentNode.insertBefore(tmpNode, startContainer.nextSibling);
-      }
-    }
-    else {
-      tmpNode = dataDoc.createTextNode(MARK_SELECTION_START);
-      startContainer.insertBefore(tmpNode, startContainer.childNodes.item(startOffset));
-    }
-  }
-
-  // now extract and display the syntax highlighted source
-  tmpNode = dataDoc.createElementNS(NS_XHTML, 'div');
-  tmpNode.appendChild(ancestorContainer);
-
-  // the load is aynchronous and so we will wait until the view-source DOM is done
-  // 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 ?
-                                              "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));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// helper to get a path like FIXptr, but with an array instead of the "tumbler" notation
-// see FIXptr: http://lists.w3.org/Archives/Public/www-xml-linking-comments/2001AprJun/att-0074/01-NOTE-FIXptr-20010425.htm
-function getPath(ancestor, node)
-{
-  var n = node;
-  var p = n.parentNode;
-  if (n == ancestor || !p)
-    return null;
-  var path = new Array();
-  if (!path)
-    return null;
-  do {
-    for (var i = 0; i < p.childNodes.length; i++) {
-      if (p.childNodes.item(i) == n) {
-        path.push(i);
-        break;
-      }
-    }
-    n = p;
-    p = n.parentNode;
-  } while (n != ancestor && p);
-  return path;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// using special markers left in the serialized source, this helper makes the
-// underlying markup of the selected fragment to automatically appear as selected
-// on the inflated view-source DOM
-function drawSelection()
-{
-  gBrowser.contentDocument.title =
-    gViewSourceBundle.getString("viewSelectionSourceTitle");
-
-  // find the special selection markers that we added earlier, and
-  // draw the selection between the two...
-  var findService = null;
-  try {
-    // get the find service which stores the global find state
-    findService = Components.classes["@mozilla.org/find/find_service;1"]
-                            .getService(Components.interfaces.nsIFindService);
-  } catch(e) { }
-  if (!findService)
-    return;
-
-  // cache the current global find state
-  var matchCase     = findService.matchCase;
-  var entireWord    = findService.entireWord;
-  var wrapFind      = findService.wrapFind;
-  var findBackwards = findService.findBackwards;
-  var searchString  = findService.searchString;
-  var replaceString = findService.replaceString;
-
-  // setup our find instance
-  var findInst = gBrowser.webBrowserFind;
-  findInst.matchCase = true;
-  findInst.entireWord = false;
-  findInst.wrapFind = true;
-  findInst.findBackwards = false;
-
-  // ...lookup the start mark
-  findInst.searchString = MARK_SELECTION_START;
-  var startLength = MARK_SELECTION_START.length;
-  findInst.findNext();
-
-  var selection = content.getSelection();
-  if (!selection.rangeCount)
-    return;
-
-  var range = selection.getRangeAt(0);
-
-  var startContainer = range.startContainer;
-  var startOffset = range.startOffset;
-
-  // ...lookup the end mark
-  findInst.searchString = MARK_SELECTION_END;
-  var endLength = MARK_SELECTION_END.length;
-  findInst.findNext();
-
-  var endContainer = selection.anchorNode;
-  var endOffset = selection.anchorOffset;
-
-  // reset the selection that find has left
-  selection.removeAllRanges();
-
-  // delete the special markers now...
-  endContainer.deleteData(endOffset, endLength);
-  startContainer.deleteData(startOffset, startLength);
-  if (startContainer == endContainer)
-    endOffset -= startLength; // has shrunk if on same text node...
-  range.setEnd(endContainer, endOffset);
-
-  // show the selection and scroll it into view
-  selection.addRange(range);
-  // the default behavior of the selection is to scroll at the end of
-  // the selection, whereas in this situation, it is more user-friendly
-  // to scroll at the beginning. So we override the default behavior here
-  try {
-    getSelectionController().scrollSelectionIntoView(
-                               Ci.nsISelectionController.SELECTION_NORMAL,
-                               Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
-                               true);
-  }
-  catch(e) { }
-
-  // restore the current find state
-  findService.matchCase     = matchCase;
-  findService.entireWord    = entireWord;
-  findService.wrapFind      = wrapFind;
-  findService.findBackwards = findBackwards;
-  findService.searchString  = searchString;
-  findService.replaceString = replaceString;
-
-  findInst.matchCase     = matchCase;
-  findInst.entireWord    = entireWord;
-  findInst.wrapFind      = wrapFind;
-  findInst.findBackwards = findBackwards;
-  findInst.searchString  = searchString;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-// special handler for markups such as MathML where reformatting the output is
-// helpful
-function viewPartialSourceForFragment(node, context)
-{
-  gTargetNode = node;
-  if (gTargetNode && gTargetNode.nodeType == Node.TEXT_NODE)
-    gTargetNode = gTargetNode.parentNode;
-
-  // walk up the tree to the top-level element (e.g., <math>, <svg>)
-  var topTag;
-  if (context == 'mathml')
-    topTag = 'math';
-  else
-    throw 'not reached';
-  var topNode = gTargetNode;
-  while (topNode && topNode.localName != topTag)
-    topNode = topNode.parentNode;
-  if (!topNode)
-    return;
-
-  // serialize
-  var title = gViewSourceBundle.getString("viewMathMLSourceTitle");
-  var wrapClass = gWrapLongLines ? ' class="wrap"' : '';
-  var source =
-    '<!DOCTYPE html>'
-  + '<html>'
-  + '<head><title>' + title + '</title>'
-  + '<link rel="stylesheet" type="text/css" href="' + gViewSourceCSS + '">'
-  + '<style type="text/css">'
-  + '#target { border: dashed 1px; background-color: lightyellow; }'
-  + '</style>'
-  + '</head>'
-  + '<body id="viewsource"' + wrapClass
-  +        ' onload="document.title=\''+title+'\';document.getElementById(\'target\').scrollIntoView(true)">'
-  + '<pre>'
-  + getOuterMarkup(topNode, 0)
-  + '</pre></body></html>'
-  ; // end
-
-  // display
-  gBrowser.loadURI("data:text/html;charset=utf-8," + encodeURIComponent(source));
-}
-
-////////////////////////////////////////////////////////////////////////////////
-function getInnerMarkup(node, indent) {
-  var str = '';
-  for (var i = 0; i < node.childNodes.length; i++) {
-    str += getOuterMarkup(node.childNodes.item(i), indent);
-  }
-  return str;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-function getOuterMarkup(node, indent) {
-  var newline = '';
-  var padding = '';
-  var str = '';
-  if (node == gTargetNode) {
-    gStartTargetLine = gLineCount;
-    str += '</pre><pre id="target">';
-  }
-
-  switch (node.nodeType) {
-  case Node.ELEMENT_NODE: // Element
-    // to avoid the wide gap problem, '\n' is not emitted on the first
-    // line and the lines before & after the <pre id="target">...</pre>
-    if (gLineCount > 0 &&
-        gLineCount != gStartTargetLine &&
-        gLineCount != gEndTargetLine) {
-      newline = '\n';
-    }
-    gLineCount++;
-    if (gDebug) {
-      newline += gLineCount;
-    }
-    for (var k = 0; k < indent; k++) {
-      padding += ' ';
-    }
-    str += newline + padding
-        +  '&lt;<span class="start-tag">' + node.nodeName + '</span>';
-    for (var i = 0; i < node.attributes.length; i++) {
-      var attr = node.attributes.item(i);
-      if (!gDebug && attr.nodeName.match(/^[-_]moz/)) {
-        continue;
-      }
-      str += ' <span class="attribute-name">'
-          +  attr.nodeName
-          +  '</span>=<span class="attribute-value">"'
-          +  unicodeTOentity(attr.nodeValue)
-          +  '"</span>';
-    }
-    if (!node.hasChildNodes()) {
-      str += '/&gt;';
-    }
-    else {
-      str += '&gt;';
-      var oldLine = gLineCount;
-      str += getInnerMarkup(node, indent + 2);
-      if (oldLine == gLineCount) {
-        newline = '';
-        padding = '';
-      }
-      else {
-        newline = (gLineCount == gEndTargetLine) ? '' : '\n';
-        gLineCount++;
-        if (gDebug) {
-          newline += gLineCount;
-        }
-      }
-      str += newline + padding
-          +  '&lt;/<span class="end-tag">' + node.nodeName + '</span>&gt;';
-    }
-    break;
-  case Node.TEXT_NODE: // Text
-    var tmp = node.nodeValue;
-    tmp = tmp.replace(/(\n|\r|\t)+/g, " ");
-    tmp = tmp.replace(/^ +/, "");
-    tmp = tmp.replace(/ +$/, "");
-    if (tmp.length != 0) {
-      str += '<span class="text">' + unicodeTOentity(tmp) + '</span>';
-    }
-    break;
-  default:
-    break;
-  }
-
-  if (node == gTargetNode) {
-    gEndTargetLine = gLineCount;
-    str += '</pre><pre>';
-  }
-  return str;
-}
-
-////////////////////////////////////////////////////////////////////////////////
-function unicodeTOentity(text)
-{
-  const charTable = {
-    '&': '&amp;<span class="entity">amp;</span>',
-    '<': '&amp;<span class="entity">lt;</span>',
-    '>': '&amp;<span class="entity">gt;</span>',
-    '"': '&amp;<span class="entity">quot;</span>'
-  };
-
-  function charTableLookup(letter) {
-    return charTable[letter];
-  }
-
-  function convertEntity(letter) {
-    try {
-      var unichar = gEntityConverter.ConvertToEntity(letter, entityVersion);
-      var entity = unichar.substring(1); // extract '&'
-      return '&amp;<span class="entity">' + entity + '</span>';
-    } catch (ex) {
-      return letter;
-    }
-  }
-
-  if (!gEntityConverter) {
-    try {
-      gEntityConverter =
-        Components.classes["@mozilla.org/intl/entityconverter;1"]
-                  .createInstance(Components.interfaces.nsIEntityConverter);
-    } catch(e) { }
-  }
-
-  const entityVersion = Components.interfaces.nsIEntityConverter.entityW3C;
-
-  var str = text;
-
-  // replace chars in our charTable
-  str = str.replace(/[<>&"]/g, charTableLookup);
-
-  // replace chars > 0x7f via nsIEntityConverter
-  str = str.replace(/[^\0-\u007f]/g, convertEntity);
-
-  return str;
-}
--- a/toolkit/components/viewsource/content/viewPartialSource.xul
+++ b/toolkit/components/viewsource/content/viewPartialSource.xul
@@ -1,34 +1,34 @@
 <?xml version="1.0"?>
 # -*- Mode: HTML -*-
 # 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/.
 
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> 
-<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?> 
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
 <?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 
 <!DOCTYPE window [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 %brandDTD;
 <!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
 %sourceDTD;
 ]>
 
 <window id="viewSource"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="onLoadViewPartialSource();"
         contenttitlesetting="true"
-        title="&mainWindow.title;" 
-        titlemodifier="&mainWindow.titlemodifier;" 
+        title="&mainWindow.title;"
+        titlemodifier="&mainWindow.titlemodifier;"
         titlepreface=""
-        titlemenuseparator ="&mainWindow.titlemodifierseparator;"  
+        titlemenuseparator ="&mainWindow.titlemodifierseparator;"
         windowtype="navigator:view-source"
         width="500" height="300"
         screenX="10" screenY="10"
         persist="screenX screenY width height sizemode">
 
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
   <script type="application/javascript" src="chrome://global/content/viewSource.js"/>
@@ -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,26 +75,26 @@
 
   <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 --> 
+  <!-- Menu -->
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
       <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
         <menupopup id="menu_FilePopup">
           <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
                     label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
           <menuitem command="cmd_pagesetup" id="menu_pageSetup"
@@ -126,37 +126,37 @@
           <menuitem id="menu_find"/>
           <menuitem id="menu_findAgain"/>
         </menupopup>
       </menu>
 
       <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
         <menupopup id="viewmenu-popup">
           <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
-            <menupopup>                    
-              <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge" 
+            <menupopup>
+              <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
                         label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
                         key="key_textZoomEnlarge"/>
-              <menuitem id="menu_textReduce" command="cmd_textZoomReduce" 
+              <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
                         label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
                         key="key_textZoomReduce"/>
               <menuseparator/>
               <menuitem id="menu_textReset" command="cmd_textZoomReset"
                         label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
                         key="key_textZoomReset"/>
             </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"
                     label="&menu_highlightSyntax.label;" accesskey="&menu_highlightSyntax.accesskey;"/>
         </menupopup>
       </menu>
-    </menubar>  
+    </menubar>
   </toolbox>
 
   <vbox id="appcontent" flex="1">
     <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
              disablehistory="true" context="viewSourceContextMenu" />
     <findbar id="FindToolbar" browserid="content"/>
   </vbox>
 
--- a/toolkit/components/viewsource/content/viewSource-content.js
+++ b/toolkit/components/viewsource/content/viewSource-content.js
@@ -7,18 +7,27 @@ const { utils: Cu, interfaces: Ci, class
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
 
+const NS_XHTML = "http://www.w3.org/1999/xhtml";
 const BUNDLE_URL = "chrome://global/locale/viewSource.properties";
 
+// These are markers used to delimit the selection during processing. They
+// are removed from the final rendering.
+// We use noncharacter Unicode codepoints to minimize the risk of clashing
+// with anything that might legitimately be present in the document.
+// U+FDD0..FDEF <noncharacters>
+const MARK_SELECTION_START = "\uFDD0";
+const MARK_SELECTION_END = "\uFDEF";
+
 let global = this;
 
 /**
  * ViewSourceContent should be loaded in the <xul:browser> of the
  * view source window, and initialized as soon as it has loaded.
  */
 let ViewSourceContent = {
   /**
@@ -34,28 +43,47 @@ let ViewSourceContent = {
    */
   messages: [
     "ViewSource:LoadSource",
     "ViewSource:LoadSourceDeprecated",
     "ViewSource:GoToLine",
     "ViewSource:ToggleWrapping",
     "ViewSource:ToggleSyntaxHighlighting",
     "ViewSource:SetCharacterSet",
+    "ViewSource:ScheduleDrawSelection",
   ],
 
   /**
+   * When showing selection source, chrome will construct a page fragment to
+   * show, and then instruct content to draw a selection after load.  This is
+   * set true when there is a pending request to draw selection.
+   */
+  needsDrawSelection: false,
+
+  /**
    * ViewSourceContent is attached as an nsISelectionListener on pageshow,
    * 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:") ||
+           (uri.startsWith("data:") && uri.includes("MathML"));
+  },
+
+  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 +116,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":
@@ -111,24 +142,30 @@ let ViewSourceContent = {
         this.toggleWrapping();
         break;
       case "ViewSource:ToggleSyntaxHighlighting":
         this.toggleSyntaxHighlighting();
         break;
       case "ViewSource:SetCharacterSet":
         this.setCharacterSet(data.charset, data.doPageLoad);
         break;
+      case "ViewSource:ScheduleDrawSelection":
+        this.scheduleDrawSelection();
+        break;
     }
   },
 
   /**
    * 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":
@@ -157,16 +194,24 @@ let ViewSourceContent = {
    */
   get selectionController() {
     return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsISelectionDisplay)
                    .QueryInterface(Ci.nsISelectionController);
   },
 
   /**
+   * A shortcut to the nsIWebBrowserFind for the content.
+   */
+  get webBrowserFind() {
+    return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIWebBrowserFind);
+  },
+
+  /**
    * Called when the parent sends a message to view some source code.
    *
    * @param URL (required)
    *        The URL string of the source to be shown.
    * @param outerWindowID (optional)
    *        The outerWindowID of the content window that has hosted
    *        the document, in case we want to retrieve it from the network
    *        cache.
@@ -245,18 +290,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) {
@@ -295,26 +345,38 @@ let ViewSourceContent = {
    */
   loadSourceFromURL(URL) {
     let loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
     webNav.loadURI(URL, loadFlags, null, null, null);
   },
 
   /**
-   * This handler is specifically for click events bubbling up from
-   * error page content, which can show up if the user attempts to
-   * view the source of an attack page.
+   * This handler is for click events from:
+   *   * error page content, which can show up if the user attempts to view the
+   *     source of an attack page.
+   *   * in-page context menu actions
    */
   onClick(event) {
+    let target = event.originalTarget;
+    // Check for content menu actions
+    if (target.id) {
+      this.contextMenuItems.forEach(itemSpec => {
+        if (itemSpec.id !== target.id) {
+          return;
+        }
+        itemSpec.handler.call(this, event);
+        event.stopPropagation();
+      });
+    }
+
     // Don't trust synthetic events
     if (!event.isTrusted || event.target.localName != "button")
       return;
 
-    let target = event.originalTarget;
     let errorDoc = target.ownerDocument;
 
     if (/^about:blocked/.test(errorDoc.documentURI)) {
       // The event came from a button on a malware/phishing block page
 
       if (target == errorDoc.getElementById("getMeOutButton")) {
         // Instead of loading some safe page, just close the window
         sendAsyncMessage("ViewSource:Close");
@@ -336,22 +398,36 @@ 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();
 
-    content.focus();
+    // If we need to draw the selection, wait until an actual view source page
+    // has loaded, instead of about:blank.
+    if (this.needsDrawSelection &&
+        content.document.documentURI.startsWith("view-source:")) {
+      this.needsDrawSelection = false;
+      this.drawSelection();
+    }
+
+    if (content.document.body) {
+      this.injectContextMenu();
+    }
+
     sendAsyncMessage("ViewSource:SourceLoaded");
   },
 
   /**
    * Handler for the pagehide event.
    *
    * @param event
    *        The pagehide event being handled.
@@ -599,23 +675,22 @@ let ViewSourceContent = {
    * or not long lines are wrapped.
    */
   toggleWrapping() {
     let body = content.document.body;
     body.classList.toggle("wrap");
   },
 
   /**
-   * Called when the parent has changed the syntax highlighting pref.
+   * Toggles the "highlight" class on the document body, which sets whether
+   * or not syntax highlighting is displayed.
    */
   toggleSyntaxHighlighting() {
-    // The parent process should have set the view_source.syntax_highlight
-    // pref to the desired value. The reload brings that setting into
-    // effect.
-    this.reload();
+    let body = content.document.body;
+    body.classList.toggle("highlight");
   },
 
   /**
    * Called when the parent has changed the character set to view the
    * source with.
    *
    * @param charset
    *        The character set to use.
@@ -694,10 +769,189 @@ let ViewSourceContent = {
     if (!this.updateStatusTask) {
       this.updateStatusTask = new DeferredTask(() => {
         this.updateStatus();
       }, 100);
     }
 
     this.updateStatusTask.arm();
   },
+
+  /**
+   * Chrome has requested that we draw a selection once the content loads.
+   * We set a flag, and wait for the load event, where drawSelection() will be
+   * called to do the real work.
+   */
+  scheduleDrawSelection() {
+    this.needsDrawSelection = true;
+  },
+
+  /**
+   * Using special markers left in the serialized source, this helper makes the
+   * underlying markup of the selected fragment to automatically appear as
+   * selected on the inflated view-source DOM.
+   */
+  drawSelection() {
+    content.document.title =
+      this.bundle.GetStringFromName("viewSelectionSourceTitle");
+
+    // find the special selection markers that we added earlier, and
+    // draw the selection between the two...
+    var findService = null;
+    try {
+      // get the find service which stores the global find state
+      findService = Cc["@mozilla.org/find/find_service;1"]
+                    .getService(Ci.nsIFindService);
+    } catch(e) { }
+    if (!findService)
+      return;
+
+    // cache the current global find state
+    var matchCase     = findService.matchCase;
+    var entireWord    = findService.entireWord;
+    var wrapFind      = findService.wrapFind;
+    var findBackwards = findService.findBackwards;
+    var searchString  = findService.searchString;
+    var replaceString = findService.replaceString;
+
+    // setup our find instance
+    var findInst = this.webBrowserFind;
+    findInst.matchCase = true;
+    findInst.entireWord = false;
+    findInst.wrapFind = true;
+    findInst.findBackwards = false;
+
+    // ...lookup the start mark
+    findInst.searchString = MARK_SELECTION_START;
+    var startLength = MARK_SELECTION_START.length;
+    findInst.findNext();
+
+    var selection = content.getSelection();
+    if (!selection.rangeCount)
+      return;
+
+    var range = selection.getRangeAt(0);
+
+    var startContainer = range.startContainer;
+    var startOffset = range.startOffset;
+
+    // ...lookup the end mark
+    findInst.searchString = MARK_SELECTION_END;
+    var endLength = MARK_SELECTION_END.length;
+    findInst.findNext();
+
+    var endContainer = selection.anchorNode;
+    var endOffset = selection.anchorOffset;
+
+    // reset the selection that find has left
+    selection.removeAllRanges();
+
+    // delete the special markers now...
+    endContainer.deleteData(endOffset, endLength);
+    startContainer.deleteData(startOffset, startLength);
+    if (startContainer == endContainer)
+      endOffset -= startLength; // has shrunk if on same text node...
+    range.setEnd(endContainer, endOffset);
+
+    // show the selection and scroll it into view
+    selection.addRange(range);
+    // the default behavior of the selection is to scroll at the end of
+    // the selection, whereas in this situation, it is more user-friendly
+    // to scroll at the beginning. So we override the default behavior here
+    try {
+      this.selectionController.scrollSelectionIntoView(
+                                 Ci.nsISelectionController.SELECTION_NORMAL,
+                                 Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
+                                 true);
+    }
+    catch(e) { }
+
+    // restore the current find state
+    findService.matchCase     = matchCase;
+    findService.entireWord    = entireWord;
+    findService.wrapFind      = wrapFind;
+    findService.findBackwards = findBackwards;
+    findService.searchString  = searchString;
+    findService.replaceString = replaceString;
+
+    findInst.matchCase     = matchCase;
+    findInst.entireWord    = entireWord;
+    findInst.wrapFind      = wrapFind;
+    findInst.findBackwards = findBackwards;
+    findInst.searchString  = searchString;
+  },
+
+  /**
+   * In-page context menu items that are injected after page load.
+   */
+  contextMenuItems: [
+    {
+      id: "goToLine",
+      handler() {
+        sendAsyncMessage("ViewSource:PromptAndGoToLine");
+      }
+    },
+    {
+      id: "wrapLongLines",
+      get checked() {
+        return Services.prefs.getBoolPref("view_source.wrap_long_lines");
+      },
+      handler() {
+        this.toggleWrapping();
+      }
+    },
+    {
+      id: "highlightSyntax",
+      get checked() {
+        return Services.prefs.getBoolPref("view_source.syntax_highlight");
+      },
+      handler() {
+        this.toggleSyntaxHighlighting();
+      }
+    },
+  ],
+
+  /**
+   * Add context menu items for view source specific actions.
+   */
+  injectContextMenu() {
+    let doc = content.document;
+
+    let menu = doc.createElementNS(NS_XHTML, "menu");
+    menu.setAttribute("type", "context");
+    menu.setAttribute("id", "actions");
+    doc.body.appendChild(menu);
+    doc.body.setAttribute("contextmenu", "actions");
+
+    this.contextMenuItems.forEach(itemSpec => {
+      let item = doc.createElementNS(NS_XHTML, "menuitem");
+      item.setAttribute("id", itemSpec.id);
+      let labelName = `context_${itemSpec.id}_label`;
+      let label = this.bundle.GetStringFromName(labelName);
+      item.setAttribute("label", label);
+      if ("checked" in itemSpec) {
+        item.setAttribute("type", "checkbox");
+      }
+      menu.appendChild(item);
+    });
+
+    this.updateContextMenu();
+  },
+
+  /**
+   * Update state of checkbox-style context menu items.
+   */
+  updateContextMenu() {
+    let doc = content.document;
+    this.contextMenuItems.forEach(itemSpec => {
+      if (!("checked" in itemSpec)) {
+        return;
+      }
+      let item = doc.getElementById(itemSpec.id);
+      if (itemSpec.checked) {
+        item.setAttribute("checked", true);
+      } else {
+        item.removeAttribute("checked");
+      }
+    });
+  },
 };
 ViewSourceContent.init();
--- 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,129 +27,134 @@ 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,
+
   /**
-   * 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.
+   * The <browser> that will be displaying the view source content.
    */
-  lastLineFound: null,
+  get browser() {
+    return gBrowser;
+  },
 
   /**
    * The context menu, when opened from the content process, sends
    * up a chunk of serialized data describing the items that the
    * context menu is being opened on. This allows us to avoid using
    * CPOWs.
    */
   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;
 
     switch(message.name) {
+      // Begin messages from super class
+      case "ViewSource:PromptAndGoToLine":
+        this.promptAndGoToLine();
+        break;
+      case "ViewSource:GoToLine:Success":
+        this.onGoToLineSuccess(data.lineNumber);
+        break;
+      case "ViewSource:GoToLine:Failed":
+        this.onGoToLineFailed();
+        break;
+      // End messages from super class
       case "ViewSource:SourceLoaded":
         this.onSourceLoaded();
         break;
       case "ViewSource:SourceUnloaded":
         this.onSourceUnloaded();
         break;
       case "ViewSource:Close":
         this.close();
         break;
       case "ViewSource:OpenURL":
         this.openURL(data.URL);
         break;
-      case "ViewSource:GoToLine:Failed":
-        this.onGoToLineFailed();
-        break;
-      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 +189,45 @@ 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;
-  },
-
-  /**
-   * Getter for the nsIWebNavigation of the view source browser.
-   */
-  get webNav() {
-    return gBrowser.webNavigation;
+    return window.messageManager;
   },
 
   /**
    * 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 +268,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 +396,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 +422,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);
+      if (!this.historyEnabled) {
+        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 +560,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");
@@ -614,89 +596,34 @@ let ViewSourceChrome = {
   updateStatus(label) {
     let statusBarField = document.getElementById("statusbar-line-col");
     if (statusBarField) {
       statusBarField.label = label;
     }
   },
 
   /**
-   * Opens the "Go to line" prompt for a user to hop to a particular line
-   * of the source code they're viewing. This will keep prompting until the
-   * user either cancels out of the prompt, or enters a valid line number.
-   */
-  promptAndGoToLine() {
-    let input = { value: this.lastLineFound };
-
-    let ok = Services.prompt.prompt(
-        window,
-        gViewSourceBundle.getString("goToLineTitle"),
-        gViewSourceBundle.getString("goToLineText"),
-        input,
-        null,
-        {value:0});
-
-    if (!ok)
-      return;
-
-    let line = parseInt(input.value, 10);
-
-    if (!(line > 0)) {
-      Services.prompt.alert(window,
-                            gViewSourceBundle.getString("invalidInputTitle"),
-                            gViewSourceBundle.getString("invalidInputText"));
-      this.promptAndGoToLine();
-    } else {
-      this.goToLine(line);
-    }
-  },
-
-  /**
-   * 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 });
-  },
-
-  /**
    * Called when the frame script reports that a line was successfully gotten
    * to.
    *
    * @param lineNumber
    *        The line number that we successfully got to.
    */
   onGoToLineSuccess(lineNumber) {
-    // We'll pre-populate the "Go to line" prompt with this value the next
-    // time it comes up.
-    this.lastLineFound = lineNumber;
+    ViewSourceBrowser.prototype.onGoToLineSuccess.call(this, lineNumber);
     document.getElementById("statusbar-line-col").label =
       gViewSourceBundle.getFormattedString("statusBarLineCol", [lineNumber, 1]);
   },
 
   /**
-   * Called when the frame script reports that we failed to go to a particular
-   * line. This informs the user that their selection was likely out of range,
-   * and then reprompts the user to try again.
-   */
-  onGoToLineFailed() {
-    Services.prompt.alert(window,
-                          gViewSourceBundle.getString("outOfRangeTitle"),
-                          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 +632,77 @@ 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.
+    // We flip it here, and then toggle a class in the child.
     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 +741,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 +764,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
@@ -1,46 +1,46 @@
 <?xml version="1.0"?>
 # -*- Mode: HTML -*-
 # 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/.
 
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> 
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://global/content/viewSource.css" type="text/css"?>
 <?xml-stylesheet href="chrome://mozapps/skin/viewsource/viewsource.css" type="text/css"?>
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 
 <!DOCTYPE window [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
 %brandDTD;
 <!ENTITY % sourceDTD SYSTEM "chrome://global/locale/viewSource.dtd" >
 %sourceDTD;
 <!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >
 %charsetDTD;
 ]>
 
 <window id="viewSource"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         contenttitlesetting="true"
-        title="&mainWindow.title;" 
-        titlemodifier="&mainWindow.titlemodifier;" 
+        title="&mainWindow.title;"
+        titlemodifier="&mainWindow.titlemodifier;"
         titlepreface="&mainWindow.preface;"
-        titlemenuseparator ="&mainWindow.titlemodifierseparator;"  
+        titlemenuseparator ="&mainWindow.titlemodifierseparator;"
         windowtype="navigator:view-source"
         width="640" height="480"
         screenX="10" screenY="10"
         persist="screenX screenY width height sizemode">
 
   <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/printUtils.js"/>
   <script type="application/javascript" src="chrome://global/content/viewSource.js"/>
   <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
   <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
-  
+
   <stringbundle id="viewSourceBundle" src="chrome://global/locale/viewSource.properties"/>
 
   <command id="cmd_savePage" oncommand="ViewSourceSavePage();"/>
   <command id="cmd_print" oncommand="PrintUtils.print(gBrowser.contentWindowAsCPOW, gBrowser);"/>
   <command id="cmd_printpreview" oncommand="PrintUtils.printPreview(PrintPreviewListener);"/>
   <command id="cmd_pagesetup" oncommand="PrintUtils.showPageSetup();"/>
   <command id="cmd_close" oncommand="window.close();"/>
   <commandset id="editMenuCommands"/>
@@ -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"/>
@@ -105,17 +105,17 @@
     <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="accel" />
 #endif
 #ifdef XP_UNIX
     <key id="goBackKb2" key="&goBackCmd.commandKey;" command="Browser:Back" modifiers="accel"/>
     <key id="goForwardKb2" key="&goForwardCmd.commandKey;" command="Browser:Forward" modifiers="accel"/>
 #endif
 
   </keyset>
-  
+
   <tooltip id="aHTMLTooltip" page="true"/>
 
   <menupopup id="viewSourceContextMenu">
     <menuitem id="context-back"
               label="&backCmd.label;"
               accesskey="&backCmd.accesskey;"
               command="Browser:Back"
               observes="viewSourceNavigation"/>
@@ -126,26 +126,26 @@
               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 --> 
+  <!-- Menu -->
   <toolbox id="viewSource-toolbox">
     <menubar id="viewSource-main-menubar">
 
       <menu id="menu_file" label="&fileMenu.label;" accesskey="&fileMenu.accesskey;">
         <menupopup id="menu_FilePopup">
           <menuitem key="key_savePage" command="cmd_savePage" id="menu_savePage"
                     label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;"/>
           <menuitem command="cmd_pagesetup" id="menu_pageSetup"
@@ -183,53 +183,53 @@
       </menu>
 
       <menu id="menu_view" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
         <menupopup id="viewmenu-popup">
           <menuitem id="menu_reload" command="cmd_reload" accesskey="&reloadCmd.accesskey;"
                     label="&reloadCmd.label;" key="key_reload"/>
           <menuseparator />
           <menu id="viewTextZoomMenu" label="&menu_textSize.label;" accesskey="&menu_textSize.accesskey;">
-            <menupopup>                    
-              <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge" 
+            <menupopup>
+              <menuitem id="menu_textEnlarge" command="cmd_textZoomEnlarge"
                         label="&menu_textEnlarge.label;" accesskey="&menu_textEnlarge.accesskey;"
                         key="key_textZoomEnlarge"/>
-              <menuitem id="menu_textReduce" command="cmd_textZoomReduce" 
+              <menuitem id="menu_textReduce" command="cmd_textZoomReduce"
                         label="&menu_textReduce.label;" accesskey="&menu_textReduce.accesskey;"
                         key="key_textZoomReduce"/>
               <menuseparator/>
               <menuitem id="menu_textReset" command="cmd_textZoomReset"
                         label="&menu_textReset.label;" accesskey="&menu_textReset.accesskey;"
                         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"
                     label="&menu_highlightSyntax.label;" accesskey="&menu_highlightSyntax.accesskey;"/>
         </menupopup>
       </menu>
-    </menubar>  
+    </menubar>
   </toolbox>
 
   <vbox id="appcontent" flex="1">
 
     <browser id="content" type="content-primary" name="content" src="about:blank" flex="1"
              context="viewSourceContextMenu" showcaret="true" tooltip="aHTMLTooltip" />
     <findbar id="FindToolbar" browserid="content"/>
-  </vbox> 
+  </vbox>
 
   <statusbar id="status-bar" class="chromeclass-status">
     <statusbarpanel id="statusbar-line-col" label="" flex="1"/>
   </statusbar>
 
 </window>
--- 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,82 @@ 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);
+  },
+
+  /**
+   * Displays view source for a selection from some document 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 aSelection
+   *        A Selection object for the content of interest.
+   * @param aViewSourceInBrowser
+   *        The browser to display the view source in.
+   */
+  viewSourceFromSelectionInBrowser: function(aSelection, aViewSourceInBrowser) {
+    let viewSourceBrowser = new ViewSourceBrowser(aViewSourceInBrowser);
+    viewSourceBrowser.loadViewSourceFromSelection(aSelection);
+  },
+
+  /**
+   * Displays view source for a MathML fragment from some document 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 aNode
+   *        Some element within the fragment of interest.
+   * @param aContext
+   *        A string denoting the type of fragment.  Currently, "mathml" is the
+   *        only accepted value.
+   * @param aViewSourceInBrowser
+   *        The browser to display the view source in.
+   */
+  viewSourceFromFragmentInBrowser: function(aNode, aContext,
+                                            aViewSourceInBrowser) {
+    let viewSourceBrowser = new ViewSourceBrowser(aViewSourceInBrowser);
+    viewSourceBrowser.loadViewSourceFromFragment(aNode, aContext);
+  },
+
   // 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)) {
@@ -109,43 +179,43 @@ var gViewSourceUtils = {
   // aCallBack is a function accepting two arguments - result (true=success) and a data object
   // It defaults to openInInternalViewer if undefined.
   openInExternalEditor: function(aURL, aPageDescriptor, aDocument, aLineNumber, aCallBack)
   {
     var data = {url: aURL, pageDescriptor: aPageDescriptor, doc: aDocument,
                 lineNumber: aLineNumber};
 
     try {
-      var editor = this.getExternalViewSourceEditor();    
+      var editor = this.getExternalViewSourceEditor();
       if (!editor) {
         this.handleCallBack(aCallBack, false, data);
         return;
       }
 
       // make a uri
       var ios = Components.classes["@mozilla.org/network/io-service;1"]
                           .getService(Components.interfaces.nsIIOService);
       var charset = aDocument ? aDocument.characterSet : null;
       var uri = ios.newURI(aURL, charset, null);
       data.uri = uri;
 
       var path;
       var contentType = aDocument ? aDocument.contentType : null;
-      if (uri.scheme == "file") {    
+      if (uri.scheme == "file") {
         // it's a local file; we can open it directly
         path = uri.QueryInterface(Components.interfaces.nsIFileURL).file.path;
 
         var editorArgs = this.buildEditorArgs(path, data.lineNumber);
         editor.runw(false, editorArgs, editorArgs.length);
         this.handleCallBack(aCallBack, true, data);
       } else {
         // set up the progress listener with what we know so far
         this.viewSourceProgressListener.editor = editor;
         this.viewSourceProgressListener.callBack = aCallBack;
-        this.viewSourceProgressListener.data = data;      
+        this.viewSourceProgressListener.data = data;
         if (!aPageDescriptor) {
           // without a page descriptor, loadPage has no chance of working. download the file.
           var file = this.getTemporaryFile(uri, aDocument, contentType);
           this.viewSourceProgressListener.file = file;
 
           let fromPrivateWindow = false;
           if (aDocument) {
             try {
@@ -184,17 +254,17 @@ var gViewSourceUtils = {
           // at all correctly; if somehow the view-source stuff managed to
           // execute script we'd be in big trouble here, I suspect.
           var webShell = Components.classes["@mozilla.org/docshell;1"].createInstance();
           webShell.QueryInterface(Components.interfaces.nsIBaseWindow).create();
           this.viewSourceProgressListener.webShell = webShell;
           var progress = webShell.QueryInterface(this.mnsIWebProgress);
           progress.addProgressListener(this.viewSourceProgressListener,
                                        this.mnsIWebProgress.NOTIFY_STATE_DOCUMENT);
-          var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor);    
+          var pageLoader = webShell.QueryInterface(this.mnsIWebPageDescriptor);
           pageLoader.loadPage(aPageDescriptor, this.mnsIWebPageDescriptor.DISPLAY_AS_SOURCE);
         }
       }
     } catch (ex) {
       // we failed loading it with the external editor.
       Components.utils.reportError(ex);
       this.handleCallBack(aCallBack, false, data);
       return;
@@ -291,31 +361,31 @@ var gViewSourceUtils = {
 
     onContentLoaded: function() {
       try {
         if (!this.file) {
           // it's not saved to file yet, it's in the webshell
 
           // get a temporary filename using the attributes from the data object that
           // openInExternalEditor gave us
-          this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc, 
+          this.file = gViewSourceUtils.getTemporaryFile(this.data.uri, this.data.doc,
                                                         this.data.doc.contentType);
 
           // we have to convert from the source charset.
           var webNavigation = this.webShell.QueryInterface(Components.interfaces.nsIWebNavigation);
           var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
                                    .createInstance(Components.interfaces.nsIFileOutputStream);
           foStream.init(this.file, 0x02 | 0x08 | 0x20, -1, 0); // write | create | truncate
           var coStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
                                    .createInstance(Components.interfaces.nsIConverterOutputStream);
           coStream.init(foStream, this.data.doc.characterSet, 0, null);
 
           // write the source to the file
           coStream.writeString(webNavigation.document.body.textContent);
-          
+
           // clean up
           coStream.close();
           foStream.close();
 
           let fromPrivateWindow =
             this.data.doc.defaultView
                          .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                          .getInterface(Components.interfaces.nsIWebNavigation)
--- 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(() => {
--- a/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
+++ b/toolkit/components/viewsource/test/browser/browser_viewsourceprefs.js
@@ -48,31 +48,24 @@ let exercisePrefs = Task.async(function*
   yield checkStyle(win, "white-space", "pre-wrap");
 
   simulateClick(wrapMenuItem);
   is(wrapMenuItem.hasAttribute("checked"), false, "Wrap menu item unchecked");
   is(SpecialPowers.getBoolPref("view_source.wrap_long_lines"), false, "Wrap pref set");
   yield checkStyle(win, "white-space", "pre");
 
   // Check that the Syntax Highlighting menu item works.
-  let pageShowPromise = BrowserTestUtils.waitForEvent(win.gBrowser, "pageshow");
   simulateClick(syntaxMenuItem);
-  yield pageShowPromise;
-
   is(syntaxMenuItem.hasAttribute("checked"), false, "Syntax menu item unchecked");
   is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), false, "Syntax highlighting pref set");
   yield checkHighlight(win, false);
 
-  pageShowPromise = BrowserTestUtils.waitForEvent(win.gBrowser, "pageshow");
   simulateClick(syntaxMenuItem);
-  yield pageShowPromise;
-
   is(syntaxMenuItem.hasAttribute("checked"), true, "Syntax menu item checked");
   is(SpecialPowers.getBoolPref("view_source.syntax_highlight"), true, "Syntax highlighting pref set");
-
   yield checkHighlight(win, highlightable);
   yield BrowserTestUtils.closeWindow(win);
 
   // Open a new view-source window to check that the prefs are obeyed.
   SpecialPowers.setIntPref("view_source.tab_size", 2);
   SpecialPowers.setBoolPref("view_source.wrap_long_lines", true);
   SpecialPowers.setBoolPref("view_source.syntax_highlight", false);
 
@@ -122,13 +115,14 @@ let checkStyle = Task.async(function* (w
   is(value, expected, "Correct value of " + styleProperty);
 });
 
 let checkHighlight = Task.async(function* (win, expected) {
   let browser = win.gBrowser;
   let highlighted = yield ContentTask.spawn(browser, {}, function* () {
     let spans = content.document.getElementsByTagName("span");
     return Array.some(spans, (span) => {
-      return span.className != "";
+      let style = content.getComputedStyle(span, null);
+      return style.getPropertyValue("color") !== "rgb(0, 0, 0)";
     });
   });
   is(highlighted, expected, "Syntax highlighting " + (expected ? "on" : "off"));
 });
--- a/toolkit/content/widgets/notification.xml
+++ b/toolkit/content/widgets/notification.xml
@@ -470,17 +470,17 @@
   </binding>
 
   <binding id="popup-notification">
     <content>
       <xul:vbox>
         <xul:image class="popup-notification-icon"
                    xbl:inherits="popupid,src=icon"/>
       </xul:vbox>
-      <xul:vbox>
+      <xul:vbox class="popup-notification-body" xbl:inherits="popupid">
         <xul:hbox align="start">
           <xul:vbox flex="1">
             <xul:label class="popup-notification-origin header"
                        xbl:inherits="value=origin,tooltiptext=origin"
                        crop="center"/>
             <xul:description class="popup-notification-description"
                              xbl:inherits="xbl:text=label,popupid"/>
           </xul:vbox>
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -973,17 +973,16 @@ TabActor.prototype = {
     }
 
     Object.defineProperty(this, "docShell", {
       value: null,
       configurable: true
     });
 
     this._extraActors = null;
-    this._styleSheetActors.clear();
 
     this._exited = true;
   },
 
   /**
    * Return true if the given global is associated with this tab and should be
    * added as a debuggee, false otherwise.
    */
@@ -1320,16 +1319,20 @@ TabActor.prototype = {
         Services.obs.removeObserver(this, "webnavigation-create");
       }
       Services.obs.removeObserver(this, "webnavigation-destroy");
     }
 
     this._popContext();
 
     // Shut down actors that belong to this tab's pool.
+    for (let sheetActor of this._styleSheetActors.values()) {
+      this._tabPool.removeActor(sheetActor);
+    }
+    this._styleSheetActors.clear();
     this.conn.removeActorPool(this._tabPool);
     this._tabPool = null;
     if (this._tabActorPool) {
       this.conn.removeActorPool(this._tabActorPool);
       this._tabActorPool = null;
     }
 
     this._attached = false;
@@ -1637,22 +1640,16 @@ TabActor.prototype = {
       threadActor.clearDebuggees();
       threadActor.dbg.enabled = true;
       threadActor.maybePauseOnExceptions();
       // Update the global no matter if the debugger is on or off,
       // otherwise the global will be wrong when enabled later.
       threadActor.global = window;
     }
 
-    for (let sheetActor of this._styleSheetActors.values()) {
-      this._tabPool.removeActor(sheetActor);
-    }
-    this._styleSheetActors.clear();
-
-
     // Refresh the debuggee list when a new window object appears (top window or
     // iframe).
     if (threadActor.attached) {
       threadActor.dbg.addDebuggees();
     }
   },
 
   _windowDestroyed: function (window, id = null, isFrozen = false) {
--- a/toolkit/locales/en-US/chrome/global/viewSource.properties
+++ b/toolkit/locales/en-US/chrome/global/viewSource.properties
@@ -6,8 +6,12 @@ goToLineTitle     = Go to line
 goToLineText      = Enter line number
 invalidInputTitle = Invalid input
 invalidInputText  = The line number entered is invalid.
 outOfRangeTitle   = Line not found
 outOfRangeText    = The specified line was not found.
 statusBarLineCol  = Line %1$S, Col %2$S
 viewSelectionSourceTitle = DOM Source of Selection
 viewMathMLSourceTitle    = DOM Source of MathML
+
+context_goToLine_label        = Go to Line…
+context_wrapLongLines_label   = Wrap Long Lines
+context_highlightSyntax_label = Syntax Highlighting
--- a/toolkit/themes/linux/global/notification.css
+++ b/toolkit/themes/linux/global/notification.css
@@ -58,18 +58,18 @@ notification[type="critical"] {
 
 .messageCloseButton {
   padding-left: 11px;
   padding-right: 11px;
 }
 
 /* Popup notification */
 
-.popup-notification-description {
-  max-width: 24em;
+.popup-notification-body {
+  max-width: 25em;
 }
 
 .popup-notification-origin:not([value]),
 .popup-notification-learnmore-link:not([href]) {
   display: none;
 }
 
 .popup-notification-origin {
--- a/toolkit/themes/osx/global/notification.css
+++ b/toolkit/themes/osx/global/notification.css
@@ -96,18 +96,18 @@ notification[type="info"]:not([value="tr
 @media (min-resolution: 2dppx) {
   .messageCloseButton > .toolbarbutton-icon {
     width: 16px;
   }
 }
 
 /* Popup notification */
 
-.popup-notification-description {
-  max-width: 24em;
+.popup-notification-body {
+  max-width: 25em;
 }
 
 .popup-notification-origin:not([value]),
 .popup-notification-learnmore-link:not([href]) {
   display: none;
 }
 
 .popup-notification-origin {
--- a/toolkit/themes/windows/global/notification.css
+++ b/toolkit/themes/windows/global/notification.css
@@ -53,18 +53,18 @@ notification[type="critical"] {
 }
 
 .messageCloseButton > .toolbarbutton-icon {
   -moz-margin-end: 5px;
 }
 
 /* Popup notification */
 
-.popup-notification-description {
-  max-width: 24em;
+.popup-notification-body {
+  max-width: 25em;
 }
 
 .popup-notification-origin:not([value]),
 .popup-notification-learnmore-link:not([href]) {
   display: none;
 }
 
 .popup-notification-origin {