Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 26 Jun 2014 16:32:53 -0400
changeset 190995 1b16fb9ff3529839823501f46017ea726516554b
parent 190972 5f52980f394ac0e64902e4665806ec6e074c0386 (current diff)
parent 190994 0c9ed31fcb968125a708238d68aaedbaa16cd29e (diff)
child 190996 398a5f3142c150f26a2b41a53f5b8b22b4966e78
push id8417
push userryanvm@gmail.com
push dateThu, 26 Jun 2014 21:15:47 +0000
treeherderb2g-inbound@3bef42144aab [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
Merge fx-team to m-c. a=merge
browser/devtools/projecteditor/chrome/content/projecteditor-test.html
browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -642,17 +642,17 @@ pref("network.protocol-handler.expose.nn
 pref("accessibility.typeaheadfind", false);
 pref("accessibility.typeaheadfind.timeout", 5000);
 pref("accessibility.typeaheadfind.linksonly", false);
 pref("accessibility.typeaheadfind.flashBar", 1);
 
 // plugin finder service url
 pref("pfs.datasource.url", "https://pfs.mozilla.org/plugins/PluginFinderService.php?mimetype=%PLUGIN_MIMETYPE%&appID=%APP_ID%&appVersion=%APP_VERSION%&clientOS=%CLIENT_OS%&chromeLocale=%CHROME_LOCALE%&appRelease=%APP_RELEASE%");
 
-pref("plugins.update.url", "https://www.mozilla.org/%LOCALE%/plugincheck/");
+pref("plugins.update.url", "https://www.mozilla.org/%LOCALE%/plugincheck/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=plugincheck-update");
 pref("plugins.update.notifyUser", false);
 
 pref("plugins.click_to_play", true);
 
 pref("plugins.hideMissingPluginsNotification", false);
 
 pref("plugin.default.state", 1);
 
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -23,16 +23,20 @@ var FullZoom = {
   // From EventStateManager.h.
   ACTION_ZOOM: 3,
 
   // This maps the browser to monotonically increasing integer
   // tokens. _browserTokenMap[browser] is increased each time the zoom is
   // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses.
   _browserTokenMap: new WeakMap(),
 
+  // Stores initial locations if we receive onLocationChange
+  // events before we're initialized.
+  _initialLocations: new WeakMap(),
+
   get siteSpecific() {
     return this._siteSpecificPref;
   },
 
   //**************************************************************************//
   // nsISupports
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
@@ -55,16 +59,28 @@ var FullZoom = {
 
     this._siteSpecificPref =
       gPrefService.getBoolPref("browser.zoom.siteSpecific");
     this.updateBackgroundTabs =
       gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
     // Listen for changes to the browser.zoom branch so we can enable/disable
     // updating background tabs and per-site saving and restoring of zoom levels.
     gPrefService.addObserver("browser.zoom.", this, true);
+
+    // If we received onLocationChange events for any of the current browsers
+    // before we were initialized we want to replay those upon initialization.
+    for (let browser of gBrowser.browsers) {
+      if (this._initialLocations.has(browser)) {
+        this.onLocationChange(...this._initialLocations.get(browser), browser);
+      }
+    }
+
+    // This should be nulled after initialization.
+    this._initialLocations.clear();
+    this._initialLocations = null;
   },
 
   destroy: function FullZoom_destroy() {
     gPrefService.removeObserver("browser.zoom.", this);
     this._cps2.removeObserverForName(this.name, this);
     window.removeEventListener("DOMMouseScroll", this, false);
   },
 
@@ -212,20 +228,28 @@ var FullZoom = {
    * @param aURI
    *        A URI object representing the new location.
    * @param aIsTabSwitch
    *        Whether this location change has happened because of a tab switch.
    * @param aBrowser
    *        (optional) browser object displaying the document
    */
   onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
+    let browser = aBrowser || gBrowser.selectedBrowser;
+
+    // If we haven't been initialized yet but receive an onLocationChange
+    // notification then let's store and replay it upon initialization.
+    if (this._initialLocations) {
+      this._initialLocations.set(browser, [aURI, aIsTabSwitch]);
+      return;
+    }
+
     // Ignore all pending async zoom accesses in the browser.  Pending accesses
     // that started before the location change will be prevented from applying
     // to the new location.
-    let browser = aBrowser || gBrowser.selectedBrowser;
     this._ignorePendingZoomAccesses(browser);
 
     if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
       this._notifyOnLocationChange();
       return;
     }
 
     // Avoid the cps roundtrip and apply the default/global pref.
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -794,17 +794,16 @@
         <toolbarbutton id="loop-call-button"
                        class="toolbarbutton-1 chromeclass-toolbar-additional"
                        persist="class"
                        type="badged"
                        removable="true"
                        tooltiptext="&loopCallButton.tooltip;"
                        oncommand="LoopUI.openCallPanel(event);"
                        cui-areatype="toolbar"
-                       overflows="false"
                        >
         </toolbarbutton>
 #endif
 
         <toolbarbutton id="bookmarks-menu-button"
                        class="toolbarbutton-1 chromeclass-toolbar-additional"
                        persist="class"
                        removable="true"
--- a/browser/components/loop/test/shared/frontend_tester.py
+++ b/browser/components/loop/test/shared/frontend_tester.py
@@ -70,23 +70,23 @@ class BaseTestFrontendUnits(MarionetteTe
         # We may be run from a different path than topsrcdir, e.g. in the case
         # of packaged tests. If so, then we have to work out the right directory
         # for the local server.
 
         # First find the top of the working directory.
         commonPath = os.path.commonprefix([__file__, os.getcwd()])
 
         # Now get the relative path between the two
-        relPath = os.path.relpath(os.path.dirname(__file__), commonPath)
+        self.relPath = os.path.relpath(os.path.dirname(__file__), commonPath)
 
-        relPath = urllib.pathname2url(os.path.join(relPath, srcdir_path))
+        self.relPath = urllib.pathname2url(os.path.join(self.relPath, srcdir_path))
 
         # Finally join the relative path with the given src path
         self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port),
-                                              relPath)
+                                              self.relPath)
 
     def check_page(self, page):
 
         self.marionette.navigate(urlparse.urljoin(self.server_prefix, page))
         self.marionette.find_element("id", 'complete')
 
         fail_node = self.marionette.find_element("css selector",
                                                  '.failures > em')
@@ -98,20 +98,31 @@ class BaseTestFrontendUnits(MarionetteTe
         #
         # If you have browser-based unit tests which work when loaded manually
         # but not from marionette, uncomment the two lines below to break
         # on failing tests, so that the browsers won't be torn down, and you
         # can use the browser debugging facilities to see what's going on.
         #from ipdb import set_trace
         #set_trace()
 
-        raise AssertionError(self.get_failure_details())
+        raise AssertionError(self.get_failure_details(page))
 
-    def get_failure_details(self):
+    def get_failure_details(self, page):
         fail_nodes = self.marionette.find_elements("css selector",
                                                    '.test.fail')
-        details = ["%d failure(s) encountered:" % len(fail_nodes)]
+        fullPageUrl = urlparse.urljoin(self.relPath, page)
+
+        details = ["%s: %d failure(s) encountered:" % (fullPageUrl, len(fail_nodes))]
+
         for node in fail_nodes:
+            errorText = node.find_element("css selector", '.error').text
+
+            # We have to work our own failure message here, as we could be reporting multiple failures.
+            # XXX Ideally we'd also give the full test tree for <test name> - that requires walking
+            # up the DOM tree.
+
+            # Format: TEST-UNEXPECTED-FAIL | <filename> | <test name> - <test error>
             details.append(
-                node.find_element("tag name", 'h2').text.split("\n")[0])
+                "TEST-UNEXPECTED-FAIL | %s | %s - %s" % \
+                (fullPageUrl, node.find_element("tag name", 'h2').text.split("\n")[0], errorText.split("\n")[0]))
             details.append(
-                node.find_element("css selector", '.error').text)
+                errorText)
         return "\n".join(details)
--- a/browser/components/preferences/in-content/content.js
+++ b/browser/components/preferences/in-content/content.js
@@ -56,18 +56,18 @@ var gContentPane = {
   showPopupExceptions: function ()
   {
     var bundlePreferences = document.getElementById("bundlePreferences");
     var params = { blockVisible: false, sessionVisible: false, allowVisible: true,
                    prefilledHost: "", permissionType: "popup" }
     params.windowTitle = bundlePreferences.getString("popuppermissionstitle");
     params.introText = bundlePreferences.getString("popuppermissionstext");
 
-    openDialog("chrome://browser/content/preferences/permissions.xul", 
-               "Browser:Permissions", "resizable=yes", params);
+    gSubDialog.open("chrome://browser/content/preferences/permissions.xul",
+                    "resizable=yes", params);
   },
 
   // FONTS
 
   /**
    * Populates the default font list in UI.
    */
   _rebuildFonts: function ()
@@ -158,29 +158,27 @@ var gContentPane = {
   },
 
   /**
    * Displays the colors dialog, where default web page/link/etc. colors can be
    * configured.
    */
   configureColors: function ()
   {
-    openDialog("chrome://browser/content/preferences/colors.xul", 
-               "Browser:ColorPreferences", null);  
+    gSubDialog.open("chrome://browser/content/preferences/colors.xul");
   },
 
   // LANGUAGES
 
   /**
    * Shows a dialog in which the preferred language for web content may be set.
    */
   showLanguages: function ()
   {
-    openDialog("chrome://browser/content/preferences/languages.xul", 
-               "Browser:LanguagePreferences", null);
+    gSubDialog.open("chrome://browser/content/preferences/languages.xul");
   },
 
   /**
    * Displays the translation exceptions dialog where specific site and language
    * translation preferences can be set.
    */
   showTranslationExceptions: function ()
   {
--- a/browser/components/preferences/in-content/jar.mn
+++ b/browser/components/preferences/in-content/jar.mn
@@ -1,14 +1,16 @@
 # 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/.
 
 browser.jar:
    content/browser/preferences/in-content/preferences.js
 *  content/browser/preferences/in-content/preferences.xul
+   content/browser/preferences/in-content/subdialogs.js
+
 *  content/browser/preferences/in-content/main.js
 *  content/browser/preferences/in-content/privacy.js
 *  content/browser/preferences/in-content/advanced.js
 *  content/browser/preferences/in-content/applications.js
    content/browser/preferences/in-content/content.js
    content/browser/preferences/in-content/sync.js
    content/browser/preferences/in-content/security.js
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -106,18 +106,24 @@ var gMainPane = {
   /**
    * Displays a dialog in which the user can select a bookmark to use as home
    * page.  If the user selects a bookmark, that bookmark's name is displayed in
    * UI and the bookmark's address is stored to the home page preference.
    */
   setHomePageToBookmark: function ()
   {
     var rv = { urls: null, names: null };
-    openDialog("chrome://browser/content/preferences/selectBookmark.xul",
-               "Select Bookmark", "resizable=yes, modal=yes", rv);
+    var dialog = gSubDialog.open("chrome://browser/content/preferences/selectBookmark.xul",
+                                 "resizable=yes, modal=yes", rv,
+                                 this._setHomePageToBookmarkClosed.bind(this, rv));
+  },
+
+  _setHomePageToBookmarkClosed: function(rv, aEvent) {
+    if (aEvent.detail.button != "accept")
+      return;
     if (rv.urls && rv.names) {
       var homePage = document.getElementById("browser.startup.homepage");
 
       // XXX still using dangerous "|" joiner!
       homePage.value = rv.urls.join("|");
     }
   },
 
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -16,23 +16,26 @@ let gLastHash = "";
 
 addEventListener("DOMContentLoaded", function onLoad() {
   removeEventListener("DOMContentLoaded", onLoad);
   init_all();
 });
 
 function init_all() {
   document.documentElement.instantApply = true;
+
+  gSubDialog.init();
   gMainPane.init();
   gPrivacyPane.init();
   gAdvancedPane.init();
   gApplicationsPane.init();
   gContentPane.init();
   gSyncPane.init();
   gSecurityPane.init();
+
   var initFinished = new CustomEvent("Initialized", {
   'bubbles': true,
   'cancelable': true
   });
   document.dispatchEvent(initFinished);
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
@@ -49,16 +52,20 @@ function init_all() {
   window.addEventListener("hashchange", onHashChange);
   gotoPref();
 
   // Wait until initialization of all preferences are complete before
   // notifying observers that the UI is now ready.
   Services.obs.notifyObservers(window, "advanced-pane-loaded", null);
 }
 
+window.addEventListener("unload", function onUnload() {
+  gSubDialog.uninit();
+});
+
 function onHashChange() {
   gotoPref();
 }
 
 function gotoPref(aCategory) {
   let categories = document.getElementById("categories");
   const kDefaultCategoryInternalName = categories.firstElementChild.value;
   let hash = document.location.hash;
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -11,16 +11,17 @@
 <?xml-stylesheet 
   href="chrome://browser/skin/preferences/in-content/preferences.css"?>
 <?xml-stylesheet 
   href="chrome://browser/content/preferences/handlers.css"?>
 <?xml-stylesheet href="chrome://browser/skin/preferences/applications.css"?>
 
 <!DOCTYPE page [
 <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+<!ENTITY % globalPreferencesDTD SYSTEM "chrome://global/locale/preferences.dtd">
 <!ENTITY % preferencesDTD SYSTEM 
   "chrome://browser/locale/preferences/preferences.dtd">
 <!ENTITY % privacyDTD SYSTEM "chrome://browser/locale/preferences/privacy.dtd">
 <!ENTITY % tabsDTD SYSTEM "chrome://browser/locale/preferences/tabs.dtd">
 <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
 <!ENTITY % syncDTD SYSTEM "chrome://browser/locale/preferences/sync.dtd">
 <!ENTITY % securityDTD SYSTEM 
   "chrome://browser/locale/preferences/security.dtd">
@@ -28,16 +29,17 @@
 <!ENTITY % mainDTD SYSTEM "chrome://browser/locale/preferences/main.dtd">
 <!ENTITY % aboutHomeDTD SYSTEM "chrome://browser/locale/aboutHome.dtd">
 <!ENTITY % contentDTD SYSTEM "chrome://browser/locale/preferences/content.dtd">
 <!ENTITY % applicationsDTD SYSTEM 
   "chrome://browser/locale/preferences/applications.dtd">
 <!ENTITY % advancedDTD SYSTEM 
   "chrome://browser/locale/preferences/advanced.dtd">
 %brandDTD;
+%globalPreferencesDTD;
 %preferencesDTD;
 %privacyDTD;
 %tabsDTD;
 %syncBrandDTD;
 %syncDTD;
 %securityDTD;
 %sanitizeDTD;
 %mainDTD;
@@ -61,27 +63,29 @@
 
   <html:link rel="shortcut icon"
               href="chrome://browser/skin/preferences/in-content/favicon.ico"/>
 
   <script type="application/javascript"
           src="chrome://browser/content/utilityOverlay.js"/>
   <script type="application/javascript"
           src="chrome://browser/content/preferences/in-content/preferences.js"/>
+  <script src="chrome://browser/content/preferences/in-content/subdialogs.js"/>
 
   <stringbundle id="bundleBrand" 
                 src="chrome://branding/locale/brand.properties"/>
   <stringbundle id="bundlePreferences"
                 src="chrome://browser/locale/preferences/preferences.properties"/>
 
   <stringbundleset id="appManagerBundleset">
     <stringbundle id="appManagerBundle"
                   src="chrome://browser/locale/preferences/applicationManager.properties"/>
   </stringbundleset>
 
+  <stack flex="1">
   <hbox flex="1">
 
     <!-- category list -->
     <richlistbox id="categories">
       <richlistitem id="category-general"
                     class="category"
                     value="paneGeneral"
                     helpTopic="prefs-main"
@@ -169,9 +173,29 @@
       <hbox pack="end">
         <button class="help-button"
                 aria-label="&helpButton.label;"
                 oncommand="helpButtonCommand();"/>
       </hbox>
     </vbox>
 
   </hbox>
+
+    <vbox id="dialogOverlay" align="center" pack="center">
+      <groupbox id="dialogBox"
+                orient="vertical"
+                pack="end"
+                role="dialog"
+                aria-labelledby="dialogTitle">
+        <caption class="titlebar" flex="1" align="center">
+          <label id="dialogTitle" class="header" flex="1"></label>
+          <button id="dialogClose"
+                  class="close-icon"
+                  aria-label="&preferencesCloseButton.label;"/>
+        </caption>
+        <browser id="dialogFrame"
+                 name="dialogFrame"
+                 autoscroll="false"
+                 disablehistory="true"/>
+      </groupbox>
+    </vbox>
+  </stack>
 </page>
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/subdialogs.js
@@ -0,0 +1,138 @@
+/* - 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";
+
+let gSubDialog = {
+  _closingCallback: null,
+  _frame: null,
+  _overlay: null,
+  _injectedStyleSheets: ["chrome://mozapps/content/preferences/preferences.css",
+                         "chrome://browser/skin/preferences/preferences.css",
+                         "chrome://browser/skin/preferences/in-content/preferences.css"],
+
+  init: function() {
+    this._frame = document.getElementById("dialogFrame");
+    this._overlay = document.getElementById("dialogOverlay");
+
+    // Make the close button work.
+    let dialogClose = document.getElementById("dialogClose");
+    dialogClose.addEventListener("command", this.close.bind(this));
+
+    // DOMTitleChanged isn't fired on the frame, only on the chromeEventHandler
+    let chromeBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebNavigation)
+                              .QueryInterface(Ci.nsIDocShell)
+                              .chromeEventHandler;
+    chromeBrowser.addEventListener("DOMTitleChanged", this.updateTitle, true);
+
+    // Similarly DOMFrameContentLoaded only fires on the top window
+    window.addEventListener("DOMFrameContentLoaded", this._onContentLoaded.bind(this), true);
+
+    // Wait for the stylesheets injected during DOMContentLoaded to load before showing the dialog
+    // otherwise there is a flicker of the stylesheet applying.
+    this._frame.addEventListener("load", this._onLoad.bind(this));
+  },
+
+  uninit: function() {
+    let chromeBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebNavigation)
+                              .QueryInterface(Ci.nsIDocShell)
+                              .chromeEventHandler;
+    chromeBrowser.removeEventListener("DOMTitleChanged", gSubDialog.updateTitle, true);
+  },
+
+  updateTitle: function(aEvent) {
+    if (aEvent.target != gSubDialog._frame.contentDocument)
+      return;
+    document.getElementById("dialogTitle").textContent = gSubDialog._frame.contentDocument.title;
+  },
+
+  injectXMLStylesheet: function(aStylesheetURL) {
+    let contentStylesheet = this._frame.contentDocument.createProcessingInstruction(
+      'xml-stylesheet',
+      'href="' + aStylesheetURL + '" type="text/css"'
+    );
+    this._frame.contentDocument.insertBefore(contentStylesheet,
+                                             this._frame.contentDocument.documentElement);
+  },
+
+  open: function(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
+    let features = aFeatures || "modal,centerscreen,resizable=no";
+    let dialog = window.openDialog(aURL, "dialogFrame", features, aParams);
+    if (aClosingCallback) {
+      this._closingCallback = aClosingCallback.bind(dialog);
+    }
+    return dialog;
+  },
+
+  close: function(aEvent = null) {
+    if (this._closingCallback) {
+      try {
+        this._closingCallback.call(null, aEvent);
+      } catch (ex) {
+        Cu.reportError(ex);
+      }
+      this._closingCallback = null;
+    }
+
+    this._overlay.style.visibility = "";
+    // Clear the sizing inline styles.
+    this._frame.removeAttribute("style");
+
+    setTimeout(() => {
+      // Unload the dialog after the event listeners run so that the load of about:blank isn't
+      // cancelled by the ESC <key>.
+      this._frame.loadURI("about:blank");
+    }, 0);
+  },
+
+  /* Private methods */
+
+  _onContentLoaded: function(aEvent) {
+    if (aEvent.target != this._frame || aEvent.target.contentWindow.location == "about:blank")
+      return;
+
+    for (let styleSheetURL of this._injectedStyleSheets) {
+      this.injectXMLStylesheet(styleSheetURL);
+    }
+
+    // Make window.close calls work like dialog closing.
+    let oldClose = this._frame.contentWindow.close;
+    this._frame.contentWindow.close = function() {
+      var closingEvent = new CustomEvent("dialogclosing", {
+        bubbles: true,
+        detail: { button: null },
+      });
+      gSubDialog._frame.contentWindow.dispatchEvent(closingEvent);
+
+      oldClose.call(gSubDialog._frame.contentWindow);
+    };
+
+    // XXX: Hack to make focus during the dialog's load functions work. Make the element visible
+    // sooner in DOMContentLoaded but mostly invisible instead of changing visibility just before
+    // the dialog's load event.
+    this._overlay.style.visibility = "visible";
+    this._overlay.style.opacity = "0.01";
+
+    this._frame.contentWindow.addEventListener("dialogclosing", function closingDialog(aEvent) {
+      gSubDialog._frame.contentWindow.removeEventListener("dialogclosing", closingDialog);
+      gSubDialog.close(aEvent);
+    });
+  },
+
+  _onLoad: function(aEvent) {
+    if (aEvent.target.contentWindow.location == "about:blank")
+      return;
+
+    // Do this on load to wait for the CSS to load and apply before calculating the size.
+    let docEl = this._frame.contentDocument.documentElement;
+    this._frame.style.width = docEl.style.width || docEl.scrollWidth + "px";
+    this._frame.style.height = docEl.style.height || docEl.scrollHeight + "px";
+
+    this._overlay.style.visibility = "visible";
+    this._frame.focus();
+    this._overlay.style.opacity = ""; // XXX: focus hack continued from _onContentLoaded
+  },
+};
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -14,8 +14,10 @@ support-files =
 skip-if = !healthreport || (os == 'linux' && debug)
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
 [browser_privacypane_4.js]
 [browser_privacypane_5.js]
 [browser_privacypane_8.js]
 skip-if = e10s # Bug ?????? -  "leaked until shutdown [nsGlobalWindow #99 about:preferences]"
+[browser_subdialogs.js]
+support-files = subdialog.xul
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_subdialogs.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+* http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests for the sub-dialog infrastructure, not for actual sub-dialog functionality.
+ */
+
+let gTeardownAfterClose = false;
+const gDialogURL = getRootDirectory(gTestPath) + "subdialog.xul";
+
+function test() {
+  waitForExplicitFinish();
+  open_preferences((win) => {
+    Task.spawn(function () {
+      for (let test of gTests) {
+        info("STARTING TEST: " + test.desc);
+        try {
+          yield test.run();
+        } finally {
+          if (test.teardown) {
+            yield test.teardown();
+          }
+        }
+      }
+    });
+  });
+}
+
+let gTests = [{
+  desc: "Check titlebar, focus, return value, title changes, and accepting",
+  run: function* () {
+    let rv = { acceptCount: 0 };
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, rv,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    // Check focus is in the textbox
+    ise(dialog.document.activeElement.value, "Default text", "Textbox with correct text is focused");
+
+    // Titlebar
+    ise(content.document.getElementById("dialogTitle").textContent, "Sample sub-dialog",
+       "Dialog title should be correct initially");
+    let receivedEvent = waitForEvent(gBrowser.selectedBrowser, "DOMTitleChanged");
+    dialog.document.title = "Updated title";
+    // Wait for the title change listener
+    yield receivedEvent;
+    ise(content.document.getElementById("dialogTitle").textContent, "Updated title",
+       "Dialog title should be updated with changes");
+
+    let closingPromise = promiseDialogClosing(dialog);
+
+    // Accept the dialog
+    dialog.document.documentElement.acceptDialog();
+    let closingEvent = yield closingPromise;
+    ise(closingEvent.detail.button, "accept", "closing event should indicate button was 'accept'");
+
+    yield deferredClose.promise;
+    ise(rv.acceptCount, 1, "return value should have been updated");
+  },
+},
+{
+  desc: "Check canceling the dialog",
+  run: function* () {
+    let rv = { acceptCount: 0 };
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, rv,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    let closingPromise = promiseDialogClosing(dialog);
+
+    info("cancelling the dialog");
+    dialog.document.documentElement.cancelDialog();
+
+    let closingEvent = yield closingPromise;
+    ise(closingEvent.detail.button, "cancel", "closing event should indicate button was 'accept'");
+
+    yield deferredClose.promise;
+    ise(rv.acceptCount, 0, "return value should NOT have been updated");
+  },
+},
+{
+  desc: "Check window.close on the dialog",
+  run: function* () {
+    let rv = { acceptCount: 0 };
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, rv,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    let closingPromise = promiseDialogClosing(dialog);
+    info("window.close called on the dialog");
+    dialog.window.close();
+
+    let closingEvent = yield closingPromise;
+    ise(closingEvent.detail.button, null, "closing event should indicate no button was clicked");
+
+    yield deferredClose.promise;
+    ise(rv.acceptCount, 0, "return value should NOT have been updated");
+  },
+},
+{
+  desc: "Check clicking the close button on the dialog",
+  run: function* () {
+    let rv = { acceptCount: 0 };
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, rv,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    yield EventUtils.synthesizeMouseAtCenter(content.document.getElementById("dialogClose"), {},
+                                             content.window);
+
+    yield deferredClose.promise;
+    ise(rv.acceptCount, 0, "return value should NOT have been updated");
+  },
+},
+{
+  desc: "Hitting escape in the dialog",
+  run: function* () {
+    let rv = { acceptCount: 0 };
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, rv,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    EventUtils.synthesizeKey("VK_ESCAPE", {}, content.window);
+
+    yield deferredClose.promise;
+    ise(rv.acceptCount, 0, "return value should NOT have been updated");
+  },
+},
+{
+  desc: "Check that width and height from the sub-dialog are used to size the <browser>",
+  run: function* () {
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, null,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+    let dialog = yield dialogPromise;
+
+    ise(content.gSubDialog._frame.style.width, "32em", "Width should be set on the frame from the dialog");
+    ise(content.gSubDialog._frame.style.height, "40em", "Height should be set on the frame from the dialog");
+
+    content.gSubDialog.close();
+    yield deferredClose.promise;
+  },
+},
+{
+  desc: "Check that scrollWidth and scrollHeight from the sub-dialog are used to size the <browser>",
+  run: function* () {
+    let deferredClose = Promise.defer();
+    let dialogPromise = openAndLoadSubDialog(gDialogURL, null, null,
+                                             (aEvent) => dialogClosingCallback(deferredClose, aEvent));
+
+    content.addEventListener("DOMFrameContentLoaded", function frameLoaded() {
+      content.removeEventListener("DOMFrameContentLoaded", frameLoaded);
+      content.gSubDialog._frame.contentDocument.documentElement.style.height = "";
+      content.gSubDialog._frame.contentDocument.documentElement.style.width = "";
+    });
+
+    let dialog = yield dialogPromise;
+
+    ok(content.gSubDialog._frame.style.width.endsWith("px"),
+       "Width should be set to a px value of the scrollWidth from the dialog");
+    ok(content.gSubDialog._frame.style.height.endsWith("px"),
+       "Height should be set to a px value of the scrollHeight from the dialog");
+
+    gTeardownAfterClose = true;
+    content.gSubDialog.close();
+    yield deferredClose.promise;
+  },
+}];
+
+function promiseDialogClosing(dialog) {
+  return waitForEvent(dialog, "dialogclosing");
+}
+
+function dialogClosingCallback(aPromise, aEvent) {
+  // Wait for the close handler to unload the page
+  waitForEvent(content.gSubDialog._frame, "load", 4000).then((aEvt) => {
+    info("Load event happened: " + !(aEvt instanceof Error));
+    is_element_hidden(content.gSubDialog._overlay, "Overlay is not visible");
+    ise(content.gSubDialog._frame.getAttribute("style"), "",
+        "Check that inline styles were cleared");
+    ise(content.gSubDialog._frame.contentWindow.location.toString(), "about:blank",
+       "Check the sub-dialog was unloaded");
+    if (gTeardownAfterClose) {
+      content.close();
+      finish();
+    }
+    aPromise.resolve();
+  }, Cu.reportError);
+}
--- a/browser/components/preferences/in-content/tests/head.js
+++ b/browser/components/preferences/in-content/tests/head.js
@@ -1,11 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+const kDefaultWait = 2000;
+
 function is_hidden(aElement) {
   var style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, "");
   if (style.display == "none")
     return true;
   if (style.visibility != "visible")
     return true;
 
   // Hiding a parent element will hide all its children
@@ -28,8 +32,88 @@ function is_element_hidden(aElement, aMs
 function open_preferences(aCallback) {
   gBrowser.selectedTab = gBrowser.addTab("about:preferences");
   let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
   newTabBrowser.addEventListener("Initialized", function () {
     newTabBrowser.removeEventListener("Initialized", arguments.callee, true);
     aCallback(gBrowser.contentWindow);
   }, true);
 }
+
+function openAndLoadSubDialog(aURL, aFeatures = null, aParams = null, aClosingCallback = null) {
+  let dialog = content.gSubDialog.open(aURL, aFeatures, aParams, aClosingCallback);
+  let deferred = Promise.defer();
+
+  content.gSubDialog._frame.addEventListener("load", function load(aEvent) {
+    if (aEvent.target.contentWindow.location == "about:blank")
+      return;
+    content.gSubDialog._frame.removeEventListener("load", load);
+
+    ise(content.gSubDialog._frame.contentWindow.location.toString(), aURL,
+        "Check the proper URL is loaded");
+
+    // Check visibility
+    is_element_visible(content.gSubDialog._overlay, "Overlay is visible");
+
+    // Check that stylesheets were injected
+    let expectedStyleSheetURLs = content.gSubDialog._injectedStyleSheets.slice(0);
+    for (let styleSheet of content.document.styleSheets) {
+      let i = expectedStyleSheetURLs.indexOf(styleSheet.href);
+      if (i >= 0) {
+        info("found " + styleSheet.href);
+        expectedStyleSheetURLs.splice(i, 1);
+      }
+    }
+    ise(expectedStyleSheetURLs.length, 0, "All expectedStyleSheetURLs should have been found");
+
+    deferred.resolve(dialog);
+  });
+
+  return deferred.promise;
+}
+
+/**
+ * Waits a specified number of miliseconds for a specified event to be
+ * fired on a specified element.
+ *
+ * Usage:
+ *    let receivedEvent = waitForEvent(element, "eventName");
+ *    // Do some processing here that will cause the event to be fired
+ *    // ...
+ *    // Now yield until the Promise is fulfilled
+ *    yield receivedEvent;
+ *    if (receivedEvent && !(receivedEvent instanceof Error)) {
+ *      receivedEvent.msg == "eventName";
+ *      // ...
+ *    }
+ *
+ * @param aSubject the element that should receive the event
+ * @param aEventName the event to wait for
+ * @param aTimeoutMs the number of miliseconds to wait before giving up
+ * @returns a Promise that resolves to the received event, or to an Error
+ */
+function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
+  let eventDeferred = Promise.defer();
+  let timeoutMs = aTimeoutMs || kDefaultWait;
+  let stack = new Error().stack;
+  let timerID = setTimeout(function wfe_canceller() {
+    aSubject.removeEventListener(aEventName, listener);
+    eventDeferred.reject(new Error(aEventName + " event timeout at " + stack));
+  }, timeoutMs);
+
+  var listener = function (aEvent) {
+    if (aTarget && aTarget !== aEvent.target)
+        return;
+
+    // stop the timeout clock and resume
+    clearTimeout(timerID);
+    eventDeferred.resolve(aEvent);
+  };
+
+  function cleanup(aEventOrError) {
+    // unhook listener in case of success or failure
+    aSubject.removeEventListener(aEventName, listener);
+    return aEventOrError;
+  }
+  aSubject.addEventListener(aEventName, listener, false);
+  return eventDeferred.promise.then(cleanup, cleanup);
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/subdialog.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<dialog id="subDialog"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="Sample sub-dialog" style="width: 32em; height: 40em;"
+        onload="document.getElementById('textbox').focus();"
+        ondialogaccept="acceptSubdialog();">
+  <script>
+    function acceptSubdialog() {
+      window.arguments[0].acceptCount++;
+    }
+  </script>
+
+  <description>A sample sub-dialog for testing</description>
+
+  <textbox id="textbox" value="Default text" />
+
+  <separator class="thin"/>
+
+  <button oncommand="close();" icon="close" label="Close" />
+
+</dialog>
--- a/browser/devtools/app-manager/app-validator.js
+++ b/browser/devtools/app-manager/app-validator.js
@@ -113,17 +113,17 @@ AppValidator.prototype._getManifest = fu
 AppValidator.prototype.validateManifest = function (manifest) {
   if (!manifest.name) {
     this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
   }
 
   if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
     this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
   } else if (!manifest.icons["128"]) {
-    this.warning(strings.GetStringFromName("validator.missIconMarketplace"));
+    this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
   }
 };
 
 AppValidator.prototype._getOriginURL = function () {
   if (this.project.type == "packaged") {
     let manifestURL = Services.io.newURI(this.manifestURL, null, null);
     return Services.io.newURI(".", null, manifestURL).spec;
   } else if (this.project.type == "hosted") {
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -1,20 +1,20 @@
 [DEFAULT]
-skip-if = e10s # Bug ?????? - devtools tests disabled with e10s
 subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   head.js
 
 [browser_devtools_api.js]
 [browser_dynamic_tool_enabling.js]
 [browser_keybindings.js]
+skip-if = e10s # Bug 1030318
 [browser_new_activation_workflow.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_highlight.js]
 [browser_toolbox_hosts.js]
 [browser_toolbox_options.js]
 [browser_toolbox_options_disable_buttons.js]
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -5,17 +5,17 @@
 browser.jar:
     content/browser/devtools/widgets.css                               (shared/widgets/widgets.css)
     content/browser/devtools/widgets/VariablesView.xul                 (shared/widgets/VariablesView.xul)
     content/browser/devtools/markup-view.xhtml                         (markupview/markup-view.xhtml)
     content/browser/devtools/markup-view.css                           (markupview/markup-view.css)
     content/browser/devtools/projecteditor.xul                               (projecteditor/chrome/content/projecteditor.xul)
     content/browser/devtools/readdir.js                                (projecteditor/lib/helpers/readdir.js)
     content/browser/devtools/projecteditor-loader.xul                        (projecteditor/chrome/content/projecteditor-loader.xul)
-    content/browser/devtools/projecteditor-test.html                         (projecteditor/chrome/content/projecteditor-test.html)
+    content/browser/devtools/projecteditor-test.xul                          (projecteditor/chrome/content/projecteditor-test.xul)
     content/browser/devtools/projecteditor-loader.js                         (projecteditor/chrome/content/projecteditor-loader.js)
     content/browser/devtools/netmonitor.xul                            (netmonitor/netmonitor.xul)
     content/browser/devtools/netmonitor.css                            (netmonitor/netmonitor.css)
     content/browser/devtools/netmonitor-controller.js                  (netmonitor/netmonitor-controller.js)
     content/browser/devtools/netmonitor-view.js                        (netmonitor/netmonitor-view.js)
     content/browser/devtools/NetworkPanel.xhtml                        (webconsole/NetworkPanel.xhtml)
     content/browser/devtools/webconsole.xul                            (webconsole/webconsole.xul)
 *   content/browser/devtools/scratchpad.xul                            (scratchpad/scratchpad.xul)
deleted file mode 100644
--- a/browser/devtools/projecteditor/chrome/content/projecteditor-test.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE html>
-<head>
-  <meta charset='utf-8' />
-</head>
-<body>
-  <style type="text/css">
-    html { height: 100%; }
-    body {display: flex; padding: 0; margin: 0; min-height: 100%; }
-    iframe {flex: 1; border: 0;}
-  </style>
-  <iframe id='projecteditor-iframe'></iframe>
-</body>
-</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor-test.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml">
+
+  <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
+
+  <commandset id="mainCommandSet">
+    <commandset id="editMenuCommands"/>
+  </commandset>
+  <menubar></menubar>
+  <iframe id='projecteditor-iframe' flex="1"></iframe>
+</window>
--- a/browser/devtools/projecteditor/chrome/content/projecteditor.xul
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor.xul
@@ -41,28 +41,22 @@
           accesskey="&editMenu.accesskey;">
       <menupopup id="edit-menu-popup">
         <menuitem id="menu_undo"/>
         <menuitem id="menu_redo"/>
         <menuseparator/>
         <menuitem id="menu_cut"/>
         <menuitem id="menu_copy"/>
         <menuitem id="menu_paste"/>
-        <menuseparator/>
-        <menuitem id="menu_selectAll"/>
-        <menuseparator/>
-        <menuitem id="menu_find"/>
-        <menuitem id="menu_findAgain"/>
       </menupopup>
     </menu>
   </menubar>
 
-
   <popupset>
-    <menupopup id="directory-menu-popup">
+    <menupopup id="context-menu-popup">
     </menupopup>
   </popupset>
 
   <deck id="main-deck" flex="1">
     <vbox flex="1" id="source-deckitem">
       <hbox id="sources-body" flex="1">
         <vbox width="250" id="sources">
           <vbox flex="1">
--- a/browser/devtools/projecteditor/lib/editors.js
+++ b/browser/devtools/projecteditor/lib/editors.js
@@ -21,36 +21,44 @@ const XUL_NS = "http://www.mozilla.org/k
 var ItchEditor = Class({
   extends: EventTarget,
 
   /**
    * A boolean specifying if the toolbar above the editor should be hidden.
    */
   hidesToolbar: false,
 
+  /**
+   * A boolean specifying whether the editor can be edited / saved.
+   * For instance, a 'save' doesn't make sense on an image.
+   */
+  isEditable: false,
+
   toString: function() {
     return this.label || "";
   },
 
   emit: function(name, ...args) {
     emit(this, name, ...args);
   },
 
   /**
-   * Initialize the editor with a single document.  This should be called
+   * Initialize the editor with a single host.  This should be called
    * by objects extending this object with:
    * ItchEditor.prototype.initialize.apply(this, arguments)
    */
-  initialize: function(document) {
-    this.doc = document;
+  initialize: function(host) {
+    this.doc = host.document;
     this.label = "";
     this.elt = this.doc.createElement("vbox");
     this.elt.setAttribute("flex", "1");
     this.elt.editor = this;
     this.toolbar = this.doc.querySelector("#projecteditor-toolbar");
+    this.projectEditorKeyset = host.projectEditorKeyset;
+    this.projectEditorCommandset = host.projectEditorCommandset;
   },
 
   /**
    * Sets the visibility of the element that shows up above the editor
    * based on the this.hidesToolbar property.
    */
   setToolbarVisibility: function() {
     if (this.hidesToolbar) {
@@ -98,40 +106,43 @@ exports.ItchEditor = ItchEditor;
 /**
  * The main implementation of the ItchEditor class.  The TextEditor is used
  * when editing any sort of plain text file, and can be created with different
  * modes for syntax highlighting depending on the language.
  */
 var TextEditor = Class({
   extends: ItchEditor,
 
+  isEditable: true,
+
   /**
    * Extra keyboard shortcuts to use with the editor.  Shortcuts defined
    * within projecteditor should be triggered when they happen in the editor, and
    * they would usually be swallowed without registering them.
    * See "devtools/sourceeditor/editor" for more information.
    */
   get extraKeys() {
     let extraKeys = {};
 
     // Copy all of the registered keys into extraKeys object, to notify CodeMirror
     // that it should be ignoring these keys
-    [...this.doc.querySelectorAll("#projecteditor-keyset key")].forEach((key) => {
+    [...this.projectEditorKeyset.querySelectorAll("key")].forEach((key) => {
       let keyUpper = key.getAttribute("key").toUpperCase();
       let toolModifiers = key.getAttribute("modifiers");
       let modifiers = {
         alt: toolModifiers.contains("alt"),
         shift: toolModifiers.contains("shift")
       };
 
       // On the key press, we will dispatch the event within projecteditor.
       extraKeys[Editor.accel(keyUpper, modifiers)] = () => {
-        let event = this.doc.createEvent('Event');
+        let doc = this.projectEditorCommandset.ownerDocument;
+        let event = doc.createEvent('Event');
         event.initEvent('command', true, true);
-        let command = this.doc.querySelector("#" + key.getAttribute("command"));
+        let command = this.projectEditorCommandset.querySelector("#" + key.getAttribute("command"));
         command.dispatchEvent(event);
       };
     });
 
     return extraKeys;
   },
 
   initialize: function(document, mode=Editor.modes.text) {
@@ -222,32 +233,32 @@ var TextEditor = Class({
       this.editor.focus();
     });
   }
 });
 
 /**
  * Wrapper for TextEditor using JavaScript syntax highlighting.
  */
-function JSEditor(document) {
-  return TextEditor(document, Editor.modes.js);
+function JSEditor(host) {
+  return TextEditor(host, Editor.modes.js);
 }
 
 /**
  * Wrapper for TextEditor using CSS syntax highlighting.
  */
-function CSSEditor(document) {
-  return TextEditor(document, Editor.modes.css);
+function CSSEditor(host) {
+  return TextEditor(host, Editor.modes.css);
 }
 
 /**
  * Wrapper for TextEditor using HTML syntax highlighting.
  */
-function HTMLEditor(document) {
-  return TextEditor(document, Editor.modes.html);
+function HTMLEditor(host) {
+  return TextEditor(host, Editor.modes.html);
 }
 
 /**
  * Get the type of editor that can handle a particular resource.
  * @param Resource resource
  *        The single file that is going to be opened.
  * @returns Type:Editor
  *          The type of editor that can handle this resource.  The
--- a/browser/devtools/projecteditor/lib/plugins/app-manager/app-project-editor.js
+++ b/browser/devtools/projecteditor/lib/plugins/app-manager/app-project-editor.js
@@ -9,17 +9,17 @@ const { Class } = require("sdk/core/heri
 const promise = require("projecteditor/helpers/promise");
 const { ItchEditor } = require("projecteditor/editors");
 
 var AppProjectEditor = Class({
   extends: ItchEditor,
 
   hidesToolbar: true,
 
-  initialize: function(document, host) {
+  initialize: function(host) {
     ItchEditor.prototype.initialize.apply(this, arguments);
     this.appended = promise.resolve();
     this.host = host;
     this.label = "app-manager";
   },
 
   destroy: function() {
     this.elt.remove();
--- a/browser/devtools/projecteditor/lib/plugins/delete/delete.js
+++ b/browser/devtools/projecteditor/lib/plugins/delete/delete.js
@@ -9,21 +9,21 @@ const { registerPlugin, Plugin } = requi
 const { confirm } = require("projecteditor/helpers/prompts");
 const { getLocalizedString } = require("projecteditor/helpers/l10n");
 
 var DeletePlugin = Class({
   extends: Plugin,
   shouldConfirm: true,
 
   init: function(host) {
-    this.host.addCommand({
+    this.host.addCommand(this, {
       id: "cmd-delete"
     });
     this.host.createMenuItem({
-      parent: "#directory-menu-popup",
+      parent: this.host.contextMenuPopup,
       label: getLocalizedString("projecteditor.deleteLabel"),
       command: "cmd-delete"
     });
   },
 
   confirmDelete: function(resource) {
     let deletePromptMessage = resource.isDir ?
       getLocalizedString("projecteditor.deleteFolderPromptMessage") :
--- a/browser/devtools/projecteditor/lib/plugins/image-view/image-editor.js
+++ b/browser/devtools/projecteditor/lib/plugins/image-view/image-editor.js
@@ -7,17 +7,17 @@
 const { Cu } = require("chrome");
 const { Class } = require("sdk/core/heritage");
 const promise = require("projecteditor/helpers/promise");
 const { ItchEditor } = require("projecteditor/editors");
 
 var ImageEditor = Class({
   extends: ItchEditor,
 
-  initialize: function(document) {
+  initialize: function() {
     ItchEditor.prototype.initialize.apply(this, arguments);
     this.label = "image";
     this.appended = promise.resolve();
   },
 
   load: function(resource) {
     this.elt.innerHTML = "";
     let image = this.image = this.doc.createElement("image");
--- a/browser/devtools/projecteditor/lib/plugins/new/new.js
+++ b/browser/devtools/projecteditor/lib/plugins/new/new.js
@@ -7,34 +7,33 @@
 const { Class } = require("sdk/core/heritage");
 const { registerPlugin, Plugin } = require("projecteditor/plugins/core");
 const { getLocalizedString } = require("projecteditor/helpers/l10n");
 
 // Handles the new command.
 var NewFile = Class({
   extends: Plugin,
 
-  init: function(host) {
-    this.host.createMenuItem({
-      parent: "#file-menu-popup",
-      label: getLocalizedString("projecteditor.newLabel"),
-      command: "cmd-new",
-      key: "key-new"
-    });
-    this.host.createMenuItem({
-      parent: "#directory-menu-popup",
-      label: getLocalizedString("projecteditor.newLabel"),
-      command: "cmd-new"
-    });
-
-    this.command = this.host.addCommand({
+  init: function() {
+    this.command = this.host.addCommand(this, {
       id: "cmd-new",
       key: getLocalizedString("projecteditor.new.commandkey"),
       modifiers: "accel"
     });
+    this.host.createMenuItem({
+      parent: this.host.fileMenuPopup,
+      label: getLocalizedString("projecteditor.newLabel"),
+      command: "cmd-new",
+      key: "key_cmd-new"
+    });
+    this.host.createMenuItem({
+      parent: this.host.contextMenuPopup,
+      label: getLocalizedString("projecteditor.newLabel"),
+      command: "cmd-new"
+    });
   },
 
   onCommand: function(cmd) {
     if (cmd === "cmd-new") {
       let tree = this.host.projectTree;
       let resource = tree.getSelectedResource();
       parent = resource.isDir ? resource : resource.parent;
       sibling = resource.isDir ? null : resource;
--- a/browser/devtools/projecteditor/lib/plugins/save/save.js
+++ b/browser/devtools/projecteditor/lib/plugins/save/save.js
@@ -10,39 +10,43 @@ const picker = require("projecteditor/he
 const { getLocalizedString } = require("projecteditor/helpers/l10n");
 
 // Handles the save command.
 var SavePlugin = Class({
   extends: Plugin,
 
   init: function(host) {
 
-    this.host.addCommand({
-      id: "cmd-saveas",
-      key: getLocalizedString("projecteditor.save.commandkey"),
-      modifiers: "accel shift"
-    });
-    this.host.addCommand({
+    this.host.addCommand(this, {
       id: "cmd-save",
       key: getLocalizedString("projecteditor.save.commandkey"),
       modifiers: "accel"
     });
+    this.host.addCommand(this, {
+      id: "cmd-saveas",
+      key: getLocalizedString("projecteditor.save.commandkey"),
+      modifiers: "accel shift"
+    });
+    this.host.createMenuItem({
+      parent: this.host.fileMenuPopup,
+      label: getLocalizedString("projecteditor.saveLabel"),
+      command: "cmd-save",
+      key: "key_cmd-save"
+    });
+    this.host.createMenuItem({
+      parent: this.host.fileMenuPopup,
+      label: getLocalizedString("projecteditor.saveAsLabel"),
+      command: "cmd-saveas",
+      key: "key_cmd-saveas"
+    });
+  },
 
-    // Wait until we can add things into the app manager menu
-    // this.host.createMenuItem({
-    //   parent: "#file-menu-popup",
-    //   label: "Save",
-    //   command: "cmd-save",
-    //   key: "key-save"
-    // });
-    // this.host.createMenuItem({
-    //   parent: "#file-menu-popup",
-    //   label: "Save As",
-    //   command: "cmd-saveas",
-    // });
+  isCommandEnabled: function(cmd) {
+    let currentEditor = this.host.currentEditor;
+    return currentEditor.isEditable;
   },
 
   onCommand: function(cmd) {
     if (cmd === "cmd-save") {
       this.save();
     } else if (cmd === "cmd-saveas") {
       this.saveAs();
     }
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -71,25 +71,34 @@ var ProjectEditor = Class({
 
   /**
    * Initialize ProjectEditor, and load into an iframe if specified.
    *
    * @param Iframe iframe
    *        The iframe to inject the DOM into.  If this is not
    *        specified, then this.load(frame) will need to be called
    *        before accessing ProjectEditor.
+   * @param Object options
+   *         - menubar: a <menubar> element to inject menus into
+   *         - menuindex: Integer child index to insert menus
    */
-  initialize: function(iframe) {
+  initialize: function(iframe, options = {}) {
     this._onTreeSelected = this._onTreeSelected.bind(this);
     this._onTreeResourceRemoved = this._onTreeResourceRemoved.bind(this);
     this._onEditorCreated = this._onEditorCreated.bind(this);
     this._onEditorActivated = this._onEditorActivated.bind(this);
     this._onEditorDeactivated = this._onEditorDeactivated.bind(this);
-    this._updateEditorMenuItems = this._updateEditorMenuItems.bind(this);
-
+    this._updateMenuItems = this._updateMenuItems.bind(this);
+    this.destroy = this.destroy.bind(this);
+    this.menubar = options.menubar || null;
+    this.menuindex = options.menuindex || null;
+    this._menuEnabled = true;
+    this._destroyed = false;
+    this._loaded = false;
+    this._pluginCommands = new Map();
     if (iframe) {
       this.load(iframe);
     }
   },
 
   /**
    * Load the instance inside of a specified iframe.
    * This can be called more than once, and it will return the promise
@@ -106,17 +115,22 @@ var ProjectEditor = Class({
       return this.loaded;
     }
 
     let deferred = promise.defer();
     this.loaded = deferred.promise;
     this.iframe = iframe;
 
     let domReady = () => {
+      if (this._destroyed) {
+        deferred.reject("Error: ProjectEditor has been destroyed before loading");
+        return;
+      }
       this._onLoad();
+      this._loaded = true;
       deferred.resolve(this);
     };
 
     let domHelper = new DOMHelpers(this.iframe.contentWindow);
     domHelper.onceDOMReady(domReady);
 
     this.iframe.setAttribute("src", ITCHPAD_URL);
 
@@ -125,67 +139,105 @@ var ProjectEditor = Class({
 
   /**
    * Build the projecteditor DOM inside of this.iframe.
    */
   _onLoad: function() {
     this.document = this.iframe.contentDocument;
     this.window = this.iframe.contentWindow;
 
+    this._initCommands();
+    this._buildMenubar();
     this._buildSidebar();
 
-    this.window.addEventListener("unload", this.destroy.bind(this));
+    this.window.addEventListener("unload", this.destroy, false);
 
     // Editor management
     this.shells = new ShellDeck(this, this.document);
     this.shells.on("editor-created", this._onEditorCreated);
     this.shells.on("editor-activated", this._onEditorActivated);
     this.shells.on("editor-deactivated", this._onEditorDeactivated);
 
     let shellContainer = this.document.querySelector("#shells-deck-container");
     shellContainer.appendChild(this.shells.elt);
 
-    let popup = this.document.querySelector("#edit-menu-popup");
-    popup.addEventListener("popupshowing", this.updateEditorMenuItems);
-
     // We are not allowing preset projects for now - rebuild a fresh one
     // each time.
     this.setProject(new Project({
       id: "",
       name: "",
       directories: [],
       openFiles: []
     }));
 
-    this._initCommands();
     this._initPlugins();
   },
 
+  _buildMenubar: function() {
+
+    this.editMenu = this.document.getElementById("edit-menu");
+    this.fileMenu = this.document.getElementById("file-menu");
+
+    this.editMenuPopup = this.document.getElementById("edit-menu-popup");
+    this.fileMenuPopup = this.document.getElementById("file-menu-popup");
+    this.editMenu.addEventListener("popupshowing", this._updateMenuItems);
+    this.fileMenu.addEventListener("popupshowing", this._updateMenuItems);
+
+    if (this.menubar) {
+      let body = this.menubar.ownerDocument.body ||
+                 this.menubar.ownerDocument.querySelector("window");
+      body.appendChild(this.projectEditorCommandset);
+      body.appendChild(this.projectEditorKeyset);
+      body.appendChild(this.editorCommandset);
+      body.appendChild(this.editorKeyset);
+      body.appendChild(this.contextMenuPopup);
+
+      let index = this.menuindex || 0;
+      this.menubar.insertBefore(this.editMenu, this.menubar.children[index]);
+      this.menubar.insertBefore(this.fileMenu, this.menubar.children[index]);
+    } else {
+      this.document.getElementById("projecteditor-menubar").style.display = "block";
+    }
+
+    // Insert a controller to allow enabling and disabling of menu items.
+    this._commandWindow = this.editorCommandset.ownerDocument.defaultView;
+    this._commandController = getCommandController(this);
+    this._commandWindow.controllers.insertControllerAt(0, this._commandController);
+  },
 
   /**
    * Create the project tree sidebar that lists files.
    */
   _buildSidebar: function() {
     this.projectTree = new ProjectTreeView(this.document, {
       resourceVisible: this.resourceVisible.bind(this),
-      resourceFormatter: this.resourceFormatter.bind(this)
+      resourceFormatter: this.resourceFormatter.bind(this),
+      contextMenuPopup: this.contextMenuPopup
     });
     on(this, this.projectTree, "selection", this._onTreeSelected);
     on(this, this.projectTree, "resource-removed", this._onTreeResourceRemoved);
 
     let sourcesBox = this.document.querySelector("#sources > vbox");
     sourcesBox.appendChild(this.projectTree.elt);
   },
 
   /**
    * Set up listeners for commands to dispatch to all of the plugins
    */
   _initCommands: function() {
-    this.commands = this.document.querySelector("#projecteditor-commandset");
-    this.commands.addEventListener("command", (evt) => {
+
+    this.projectEditorCommandset = this.document.getElementById("projecteditor-commandset");
+    this.projectEditorKeyset = this.document.getElementById("projecteditor-keyset");
+
+    this.editorCommandset = this.document.getElementById("editMenuCommands");
+    this.editorKeyset = this.document.getElementById("editMenuKeys");
+
+    this.contextMenuPopup = this.document.getElementById("context-menu-popup");
+
+    this.projectEditorCommandset.addEventListener("command", (evt) => {
       evt.stopPropagation();
       evt.preventDefault();
       this.pluginDispatch("onCommand", evt.target.id, evt.target);
     });
   },
 
   /**
    * Initialize each plugin in registeredPlugins
@@ -202,35 +254,64 @@ var ProjectEditor = Class({
     }
 
     this.pluginDispatch("lateInit");
   },
 
   /**
    * Enable / disable necessary menu items using globalOverlay.js.
    */
-  _updateEditorMenuItems: function() {
-    this.window.goUpdateGlobalEditMenuItems();
-    this.window.goUpdateGlobalEditMenuItems();
-    let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_findAgain'];
-    commands.forEach(this.window.goUpdateCommand);
+  _updateMenuItems: function() {
+    let window = this.editMenu.ownerDocument.defaultView;
+    let commands = ['cmd_undo', 'cmd_redo', 'cmd_delete', 'cmd_cut', 'cmd_copy', 'cmd_paste'];
+    commands.forEach(window.goUpdateCommand);
+
+    for (let c of this._pluginCommands.keys()) {
+      window.goUpdateCommand(c);
+    }
   },
 
   /**
    * Destroy all objects on the iframe unload event.
    */
   destroy: function() {
+    this._destroyed = true;
+
+
+    // If been destroyed before the iframe finished loading, then
+    // the properties below will not exist.
+    if (!this._loaded) {
+      this.iframe.setAttribute("src", "about:blank");
+      return;
+    }
+
+    // Reset the src for the iframe so if it reused for a new ProjectEditor
+    // instance, the load will fire properly.
+    this.window.removeEventListener("unload", this.destroy, false);
+    this.iframe.setAttribute("src", "about:blank");
+
     this._plugins.forEach(plugin => { plugin.destroy(); });
 
     forget(this, this.projectTree);
     this.projectTree.destroy();
     this.projectTree = null;
 
     this.shells.destroy();
 
+    this.projectEditorCommandset.remove();
+    this.projectEditorKeyset.remove();
+    this.editorCommandset.remove();
+    this.editorKeyset.remove();
+    this.contextMenuPopup.remove();
+    this.editMenu.remove();
+    this.fileMenu.remove();
+
+    this._commandWindow.controllers.removeController(this._commandController);
+    this._commandController = null;
+
     forget(this, this.project);
     this.project.destroy();
     this.project = null;
   },
 
   /**
    * Set the current project viewed by the projecteditor.
    *
@@ -379,35 +460,37 @@ var ProjectEditor = Class({
    *
    * @param Object definition
    *               key: a key/keycode string. Example: "f".
    *               id: Unique ID.  Example: "find".
    *               modifiers: Key modifiers. Example: "accel".
    * @returns DOMElement
    *          The command element that has been created.
    */
-  addCommand: function(definition) {
-    let command = this.document.createElement("command");
+  addCommand: function(plugin, definition) {
+    this._pluginCommands.set(definition.id, plugin);
+    let document = this.projectEditorKeyset.ownerDocument;
+    let command = document.createElement("command");
     command.setAttribute("id", definition.id);
     if (definition.key) {
-      let key = this.document.createElement("key");
+      let key = document.createElement("key");
       key.id = "key_" + definition.id;
 
       let keyName = definition.key;
       if (keyName.startsWith("VK_")) {
         key.setAttribute("keycode", keyName);
       } else {
         key.setAttribute("key", keyName);
       }
       key.setAttribute("modifiers", definition.modifiers);
       key.setAttribute("command", definition.id);
-      this.document.getElementById("projecteditor-keyset").appendChild(key);
+      this.projectEditorKeyset.appendChild(key);
     }
     command.setAttribute("oncommand", "void(0);"); // needed. See bug 371900
-    this.document.getElementById("projecteditor-commandset").appendChild(command);
+    this.projectEditorCommandset.appendChild(command);
     return command;
   },
 
   /**
    * Get the instance of a plugin registered with a certain type.
    *
    * @param Type pluginType
    *             The type, such as SavePlugin
@@ -605,11 +688,54 @@ var ProjectEditor = Class({
 
   get currentShell() {
     return this.shells.currentShell;
   },
 
   get currentEditor() {
     return this.shells.currentEditor;
   },
+
+  /**
+   * Whether or not menu items should be able to be enabled.
+   * Note that even if this is true, certain menu items will not be
+   * enabled until the correct state is achieved (for instance, the
+   * 'copy' menu item is only enabled when there is a selection).
+   * But if this is false, then nothing will be enabled.
+   */
+  set menuEnabled(val) {
+    this._menuEnabled = val;
+    this._updateMenuItems();
+  },
+
+  get menuEnabled() {
+    return this._menuEnabled;
+  }
 });
 
+
+/**
+ * Returns a controller object that can be used for
+ * editor-specific commands such as find, jump to line,
+ * copy/paste, etc.
+ */
+function getCommandController(host) {
+  return {
+    supportsCommand: function (cmd) {
+      return host._pluginCommands.get(cmd);
+    },
+
+    isCommandEnabled: function (cmd) {
+      if (!host.menuEnabled) {
+        return false;
+      }
+      let plugin = host._pluginCommands.get(cmd);
+      if (plugin && plugin.isCommandEnabled) {
+        return plugin.isCommandEnabled(cmd);
+      }
+      return true;
+    },
+    doCommand: function(cmd) {
+    }
+  };
+}
+
 exports.ProjectEditor = ProjectEditor;
--- a/browser/devtools/projecteditor/lib/shells.js
+++ b/browser/devtools/projecteditor/lib/shells.js
@@ -31,17 +31,17 @@ var Shell = Class({
     this.host = host;
     this.doc = host.document;
     this.resource = resource;
     this.elt = this.doc.createElement("vbox");
     this.elt.shell = this;
 
     let constructor = this._editorTypeForResource();
 
-    this.editor = constructor(this.doc, this.host);
+    this.editor = constructor(this.host);
     this.editor.shell = this;
     this.editorAppended = this.editor.appended;
 
     this.editor.on("load", () => {
       this.editorDeferred.resolve();
     });
     this.elt.appendChild(this.editor.elt);
   },
--- a/browser/devtools/projecteditor/lib/tree.js
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -101,17 +101,17 @@ var ResourceContainer = Class({
    * Open the context menu when right clicking on the view.
    * XXX: We could pass this to plugins to allow themselves
    * to be register/remove items from the context menu if needed.
    *
    * @param Event e
    */
   openContextMenu: function(ev) {
     ev.preventDefault();
-    let popup = this.tree.doc.getElementById("directory-menu-popup");
+    let popup = this.tree.options.contextMenuPopup;
     popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
   },
 
   /**
    * Update the view based on the current state of the Resource.
    */
   update: function() {
     let visible = this.tree.options.resourceVisible ?
@@ -203,39 +203,40 @@ var ResourceContainer = Class({
  * Use ProjectTreeView instead.
  */
 var TreeView = Class({
   extends: EventTarget,
 
   /**
    * @param Document document
    * @param Object options
+   *               - contextMenuPopup: a <menupopup> element
    *               - resourceFormatter: a function(Resource, DOMNode)
    *                 that renders the resource into the view
    *               - resourceVisible: a function(Resource) -> Boolean
    *                 that determines if the resource should show up.
    */
-  initialize: function(document, options) {
-    this.doc = document;
+  initialize: function(doc, options) {
+    this.doc = doc;
     this.options = merge({
       resourceFormatter: function(resource, elt) {
         elt.textContent = resource.toString();
       }
     }, options);
     this.models = new Set();
     this.roots = new Set();
     this._containers = new Map();
-    this.elt = document.createElementNS(HTML_NS, "div");
+    this.elt = this.doc.createElementNS(HTML_NS, "div");
     this.elt.tree = this;
     this.elt.className = "sources-tree";
     this.elt.setAttribute("with-arrows", "true");
     this.elt.setAttribute("theme", "dark");
     this.elt.setAttribute("flex", "1");
 
-    this.children = document.createElementNS(HTML_NS, "ul");
+    this.children = this.doc.createElementNS(HTML_NS, "ul");
     this.elt.appendChild(this.children);
 
     this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this);
     this.removeResource = this.removeResource.bind(this);
     this.updateResource = this.updateResource.bind(this);
   },
 
   destroy: function() {
--- a/browser/devtools/projecteditor/test/browser.ini
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -7,11 +7,13 @@ support-files =
 
 [browser_projecteditor_app_options.js]
 [browser_projecteditor_delete_file.js]
 [browser_projecteditor_editing_01.js]
 [browser_projecteditor_editors_image.js]
 [browser_projecteditor_external_change.js]
 [browser_projecteditor_immediate_destroy.js]
 [browser_projecteditor_init.js]
+[browser_projecteditor_menubar_01.js]
+[browser_projecteditor_menubar_02.js]
 [browser_projecteditor_new_file.js]
 [browser_projecteditor_stores.js]
 [browser_projecteditor_tree_selection.js]
--- a/browser/devtools/projecteditor/test/browser_projecteditor_delete_file.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_delete_file.js
@@ -32,17 +32,17 @@ let test = asyncTest(function*() {
       node.ownerDocument.defaultView
     );
   }
 
   function deleteWithContextMenu(container) {
     let defer = promise.defer();
 
     let resource = container.resource;
-    let popup = projecteditor.document.getElementById("directory-menu-popup");
+    let popup = projecteditor.contextMenuPopup;
     info ("Going to attempt deletion for: " + resource.path)
 
     onPopupShow(popup).then(function () {
       let deleteCommand = popup.querySelector("[command=cmd-delete]");
       ok (deleteCommand, "Delete command exists in popup");
       is (deleteCommand.getAttribute("hidden"), "", "Delete command is visible");
       is (deleteCommand.getAttribute("disabled"), "", "Delete command is enabled");
 
--- a/browser/devtools/projecteditor/test/browser_projecteditor_immediate_destroy.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_immediate_destroy.js
@@ -5,58 +5,82 @@
 "use strict";
 
 // Test that projecteditor can be destroyed in various states of loading
 // without causing any leaks or exceptions.
 
 let test = asyncTest(function* () {
 
   info ("Testing tab closure when projecteditor is in various states");
+  let loaderUrl = "chrome://browser/content/devtools/projecteditor-test.xul";
 
-  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+  yield addTab(loaderUrl).then(() => {
     let iframe = content.document.getElementById("projecteditor-iframe");
     ok (iframe, "Tab has placeholder iframe for projecteditor");
 
     info ("Closing the tab without doing anything");
     gBrowser.removeCurrentTab();
   });
 
-  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+  yield addTab(loaderUrl).then(() => {
     let iframe = content.document.getElementById("projecteditor-iframe");
     ok (iframe, "Tab has placeholder iframe for projecteditor");
 
     let projecteditor = ProjectEditor.ProjectEditor();
     ok (projecteditor, "ProjectEditor has been initialized");
 
     info ("Closing the tab before attempting to load");
     gBrowser.removeCurrentTab();
   });
 
-  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+  yield addTab(loaderUrl).then(() => {
     let iframe = content.document.getElementById("projecteditor-iframe");
     ok (iframe, "Tab has placeholder iframe for projecteditor");
 
     let projecteditor = ProjectEditor.ProjectEditor();
     ok (projecteditor, "ProjectEditor has been initialized");
 
     projecteditor.load(iframe);
 
     info ("Closing the tab after a load is requested, but before load is finished");
     gBrowser.removeCurrentTab();
   });
 
-  yield addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+  yield addTab(loaderUrl).then(() => {
     let iframe = content.document.getElementById("projecteditor-iframe");
     ok (iframe, "Tab has placeholder iframe for projecteditor");
 
     let projecteditor = ProjectEditor.ProjectEditor();
     ok (projecteditor, "ProjectEditor has been initialized");
 
     return projecteditor.load(iframe).then(() => {
       info ("Closing the tab after a load has been requested and finished");
       gBrowser.removeCurrentTab();
     });
   });
 
+  yield addTab(loaderUrl).then(() => {
+    let iframe = content.document.getElementById("projecteditor-iframe");
+    ok (iframe, "Tab has placeholder iframe for projecteditor");
+
+    let projecteditor = ProjectEditor.ProjectEditor(iframe);
+    ok (projecteditor, "ProjectEditor has been initialized");
+
+    let loadedDone = promise.defer();
+    projecteditor.loaded.then(() => {
+      ok (false, "Loaded has finished after destroy() has been called");
+      loadedDone.resolve();
+    }, () => {
+      ok (true, "Loaded has been rejected after destroy() has been called");
+      loadedDone.resolve();
+    });
+
+    projecteditor.destroy();
+
+    return loadedDone.promise.then(() => {
+      gBrowser.removeCurrentTab();
+    });
+  });
+
   finish();
 });
 
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_menubar_01.js
@@ -0,0 +1,28 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that menu bar appends to the correct document.
+
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory({
+    menubar: false
+  });
+  ok(projecteditor, "ProjectEditor has loaded");
+
+  let fileMenu = projecteditor.document.getElementById("file-menu");
+  let editMenu = projecteditor.document.getElementById("edit-menu");
+  ok (fileMenu, "The menu has loaded in the projecteditor document");
+  ok (editMenu, "The menu has loaded in the projecteditor document");
+
+  let projecteditor2 = yield addProjectEditorTabForTempDirectory();
+  let menubar = projecteditor2.menubar;
+  let fileMenu = projecteditor2.document.getElementById("file-menu");
+  let editMenu = projecteditor2.document.getElementById("edit-menu");
+  ok (!fileMenu, "The menu has NOT loaded in the projecteditor document");
+  ok (!editMenu, "The menu has NOT loaded in the projecteditor document");
+  ok (content.document.querySelector("#file-menu"), "The menu has loaded in the specified element");
+  ok (content.document.querySelector("#edit-menu"), "The menu has loaded in the specified element");
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_menubar_02.js
@@ -0,0 +1,126 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+loadHelperScript("helper_edits.js");
+
+// Test menu bar enabled / disabled state.
+
+let test = asyncTest(function*() {
+  let projecteditor = yield addProjectEditorTabForTempDirectory();
+  let menubar = projecteditor.menubar;
+
+  // let projecteditor = yield addProjectEditorTabForTempDirectory();
+  ok(projecteditor, "ProjectEditor has loaded");
+
+  let fileMenu = menubar.querySelector("#file-menu");
+  let editMenu = menubar.querySelector("#edit-menu");
+  ok (fileMenu, "The menu has loaded in the projecteditor document");
+  ok (editMenu, "The menu has loaded in the projecteditor document");
+
+  let cmdNew = fileMenu.querySelector("[command=cmd-new]");
+  let cmdSave = fileMenu.querySelector("[command=cmd-save]");
+  let cmdSaveas = fileMenu.querySelector("[command=cmd-saveas]");
+
+  let cmdUndo = editMenu.querySelector("[command=cmd_undo]");
+  let cmdRedo = editMenu.querySelector("[command=cmd_redo]");
+  let cmdCut = editMenu.querySelector("[command=cmd_cut]");
+  let cmdCopy = editMenu.querySelector("[command=cmd_copy]");
+  let cmdPaste = editMenu.querySelector("[command=cmd_paste]");
+
+  info ("Checking initial state of menus");
+  yield openAndCloseMenu(fileMenu);
+  yield openAndCloseMenu(editMenu);
+
+  is (cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+  is (cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+  is (cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+  is (cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+  projecteditor.menuEnabled = false;
+
+  info ("Checking with menuEnabled = false");
+  yield openAndCloseMenu(fileMenu);
+  yield openAndCloseMenu(editMenu);
+
+  is (cmdNew.getAttribute("disabled"), "true", "File menu item is disabled");
+  is (cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+  is (cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+  is (cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+  info ("Checking with menuEnabled=true");
+  projecteditor.menuEnabled = true;
+
+  yield openAndCloseMenu(fileMenu);
+  yield openAndCloseMenu(editMenu);
+
+  is (cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+  is (cmdSave.getAttribute("disabled"), "true", "File menu item is disabled");
+  is (cmdSaveas.getAttribute("disabled"), "true", "File menu item is disabled");
+
+  is (cmdUndo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdPaste.getAttribute("disabled"), "true", "Edit menu item is disabled");
+
+  info ("Checking with resource selected");
+  let resource = projecteditor.project.allResources()[2];
+  yield selectFile(projecteditor, resource);
+  let editor = projecteditor.currentEditor;
+
+  editor.editor.focus();
+  EventUtils.synthesizeKey("foo", { }, projecteditor.window);
+
+  yield openAndCloseMenu(fileMenu);
+  yield openAndCloseMenu(editMenu);
+
+  is (cmdNew.getAttribute("disabled"), "", "File menu item is enabled");
+  is (cmdSave.getAttribute("disabled"), "", "File menu item is enabled");
+  is (cmdSaveas.getAttribute("disabled"), "", "File menu item is enabled");
+
+  is (cmdUndo.getAttribute("disabled"), "", "Edit menu item is enabled");
+  is (cmdRedo.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCut.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdCopy.getAttribute("disabled"), "true", "Edit menu item is disabled");
+  is (cmdPaste.getAttribute("disabled"), "", "Edit menu item is enabled");
+});
+
+function openAndCloseMenu(menu) {
+  let shown = onPopupShow(menu)
+  EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
+  yield shown;
+  let hidden = onPopupHidden(menu)
+  EventUtils.synthesizeMouseAtCenter(menu, {}, menu.ownerDocument.defaultView);
+  yield hidden;
+}
+
+function onPopupShow(menu) {
+  let defer = promise.defer();
+  menu.addEventListener("popupshown", function onpopupshown() {
+    menu.removeEventListener("popupshown", onpopupshown);
+    defer.resolve();
+  });
+  return defer.promise;
+}
+
+function onPopupHidden(menu) {
+  let defer = promise.defer();
+  menu.addEventListener("popuphidden", function onpopupshown() {
+    menu.removeEventListener("popuphidden", onpopupshown);
+    defer.resolve();
+  });
+  return defer.promise;
+}
--- a/browser/devtools/projecteditor/test/head.js
+++ b/browser/devtools/projecteditor/test/head.js
@@ -83,35 +83,39 @@ function addTab(url) {
  *                 - "helper_attributes_test_runner.js"
  *                 - "../../../commandline/test/helpers.js"
  */
 function loadHelperScript(filePath) {
   let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
   Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
 }
 
-function addProjectEditorTabForTempDirectory() {
+function addProjectEditorTabForTempDirectory(opts = {}) {
   TEMP_PATH = buildTempDirectoryStructure();
-  let CUSTOM_OPTS = {
+  let customOpts = {
     name: "Test",
     iconUrl: "chrome://browser/skin/devtools/tool-options.svg",
     projectOverviewURL: SAMPLE_WEBAPP_URL
   };
 
-  return addProjectEditorTab().then((projecteditor) => {
-    return projecteditor.setProjectToAppPath(TEMP_PATH, CUSTOM_OPTS).then(() => {
+  return addProjectEditorTab(opts).then((projecteditor) => {
+    return projecteditor.setProjectToAppPath(TEMP_PATH, customOpts).then(() => {
       return projecteditor;
     });
   });
 }
 
-function addProjectEditorTab() {
-  return addTab("chrome://browser/content/devtools/projecteditor-test.html").then(() => {
+function addProjectEditorTab(opts = {}) {
+  return addTab("chrome://browser/content/devtools/projecteditor-test.xul").then(() => {
     let iframe = content.document.getElementById("projecteditor-iframe");
-    let projecteditor = ProjectEditor.ProjectEditor(iframe);
+    if (opts.menubar !== false) {
+      opts.menubar = content.document.querySelector("menubar");
+    }
+    let projecteditor = ProjectEditor.ProjectEditor(iframe, opts);
+
 
     ok (iframe, "Tab has placeholder iframe for projecteditor");
     ok (projecteditor, "ProjectEditor has been initialized");
 
     return projecteditor.loaded.then((projecteditor) => {
       return projecteditor;
     });
   });
--- a/browser/devtools/webaudioeditor/test/browser.ini
+++ b/browser/devtools/webaudioeditor/test/browser.ini
@@ -33,12 +33,13 @@ support-files =
 [browser_wa_graph-markers.js]
 [browser_wa_graph-selected.js]
 
 [browser_wa_inspector.js]
 [browser_wa_inspector-toggle.js]
 
 [browser_wa_properties-view.js]
 [browser_wa_properties-view-media-nodes.js]
-# [browser_wa_properties-view-edit.js]
+# [browser_wa_properties-view-edit-01.js]
+# [browser_wa_properties-view-edit-02.js]
 # Disabled for too many intermittents bug 1010423
 [browser_wa_properties-view-params.js]
 [browser_wa_properties-view-params-objects.js]
rename from browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit.js
rename to browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-01.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-edit-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that properties are not updated when modifying the VariablesView.
+ */
+
+function spawnTest() {
+  let [target, debuggee, panel] = yield initWebAudioEditor(COMPLEX_CONTEXT_URL);
+  let { panelWin } = panel;
+  let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin;
+  let gVars = WebAudioInspectorView._propsView;
+
+  let started = once(gFront, "start-context");
+
+  reload(target);
+
+  let [actors] = yield Promise.all([
+    getN(gFront, "create-node", 8),
+    waitForGraphRendered(panelWin, 8, 8)
+  ]);
+  let nodeIds = actors.map(actor => actor.actorID);
+
+  click(panelWin, findGraphNode(panelWin, nodeIds[3]));
+  // Wait for the node to be set as well as the inspector to come fully into the view
+  yield Promise.all([
+    once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET),
+    once(panelWin, EVENTS.UI_INSPECTOR_TOGGLED),
+  ]);
+
+  let errorEvent = once(panelWin, EVENTS.UI_SET_PARAM_ERROR);
+
+  try {
+    yield modifyVariableView(panelWin, gVars, 0, "bufferSize", 2048);
+  } catch(e) {
+    // we except modifyVariableView to fail here, because bufferSize is not writable
+  }
+
+  yield errorEvent;
+
+  checkVariableView(gVars, 0, {bufferSize: 4096}, "check that unwritable variable is not updated");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/webaudioeditor/webaudioeditor-view.js
+++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js
@@ -33,17 +33,17 @@ const MARKER_STYLING = {
 
 const GRAPH_DEBOUNCE_TIMER = 100;
 
 const GENERIC_VARIABLES_VIEW_SETTINGS = {
   searchEnabled: false,
   editableValueTooltip: "",
   editableNameTooltip: "",
   preventDisableOnChange: true,
-  preventDescriptorModifiers: true,
+  preventDescriptorModifiers: false,
   eval: () => {}
 };
 
 /**
  * Functions handling the graph UI.
  */
 let WebAudioGraphView = {
   /**
@@ -432,18 +432,21 @@ let WebAudioInspectorView = {
 
     let audioParamsScope = propsView.addScope("AudioParams");
     let props = yield node.getParams();
 
     // Disable AudioParams VariableView expansion
     // when there are no props i.e. AudioDestinationNode
     this._togglePropertiesView(!!props.length);
 
-    props.forEach(({ param, value }) => {
-      let descriptor = { value: value };
+    props.forEach(({ param, value, flags }) => {
+      let descriptor = {
+        value: value,
+        writable: !flags || !flags.readonly,
+      };
       audioParamsScope.addItem(param, descriptor);
     });
 
     audioParamsScope.expanded = true;
 
     window.emit(EVENTS.UI_PROPERTIES_TAB_RENDERED, node.id);
   }),
 
@@ -472,28 +475,32 @@ let WebAudioInspectorView = {
    * Executed when an audio prop is changed in the UI.
    */
   _onEval: Task.async(function* (variable, value) {
     let ownerScope = variable.ownerView;
     let node = this._currentNode;
     let propName = variable.name;
     let error;
 
-    // Cast value to proper type
-    try {
-      let number = parseFloat(value);
-      if (!isNaN(number)) {
-        value = number;
-      } else {
-        value = JSON.parse(value);
+    if (!variable._initialDescriptor.writable) {
+      error = new Error("Variable " + propName + " is not writable.");
+    } else {
+      // Cast value to proper type
+      try {
+        let number = parseFloat(value);
+        if (!isNaN(number)) {
+          value = number;
+        } else {
+          value = JSON.parse(value);
+        }
+        error = yield node.actor.setParam(propName, value);
       }
-      error = yield node.actor.setParam(propName, value);
-    }
-    catch (e) {
-      error = e;
+      catch (e) {
+        error = e;
+      }
     }
 
     // TODO figure out how to handle and display set prop errors
     // and enable `test/brorwser_wa_properties-view-edit.js`
     // Bug 994258
     if (!error) {
       ownerScope.get(propName).setGrip(value);
       window.emit(EVENTS.UI_SET_PARAM, node.id, propName, value);
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -316,23 +316,44 @@ let UI = {
       buttonNode.classList.remove("no-project");
       labelNode.setAttribute("value", project.name);
       imageNode.setAttribute("src", project.icon);
     }
   },
 
   // ProjectEditor & details screen
 
+  destroyProjectEditor: function() {
+    if (this.projecteditor) {
+      this.projecteditor.destroy();
+      this.projecteditor = null;
+    }
+  },
+
+  updateProjectEditorMenusVisibility: function() {
+    if (this.projecteditor) {
+      let panel = document.querySelector("#deck").selectedPanel;
+      if (panel && panel.id == "deck-panel-projecteditor") {
+        this.projecteditor.menuEnabled = true;
+      } else {
+        this.projecteditor.menuEnabled = false;
+      }
+    }
+  },
+
   getProjectEditor: function() {
     if (this.projecteditor) {
       return this.projecteditor.loaded;
     }
 
     let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
-    this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe);
+    this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, {
+      menubar: document.querySelector("#main-menubar"),
+      menuindex: 1
+    });
     this.projecteditor.on("onEditorSave", (editor, resource) => {
       AppManager.validateProject(AppManager.selectedProject);
     });
     return this.projecteditor.loaded;
   },
 
   updateProjectEditorHeader: function() {
     let project = AppManager.selectedProject;
@@ -404,22 +425,24 @@ let UI = {
   },
 
   selectDeckPanel: function(id) {
     this.hidePanels();
     this.resetFocus();
     let deck = document.querySelector("#deck");
     let panel = deck.querySelector("#deck-panel-" + id);
     deck.selectedPanel = panel;
+    this.updateProjectEditorMenusVisibility();
   },
 
   resetDeck: function() {
     this.resetFocus();
     let deck = document.querySelector("#deck");
     deck.selectedPanel = null;
+    this.updateProjectEditorMenusVisibility();
   },
 
   /********** COMMANDS **********/
 
   updateCommands: function() {
 
     if (document.querySelector("window").classList.contains("busy")) {
       document.querySelector("#cmd_newApp").setAttribute("disabled", "true");
@@ -817,17 +840,21 @@ let Cmds = {
     }
   },
 
   removeProject: function() {
     return AppManager.removeSelectedProject();
   },
 
   toggleEditors: function() {
-    Services.prefs.setBoolPref("devtools.webide.showProjectEditor", !UI.isProjectEditorEnabled());
+    let isNowEnabled = !UI.isProjectEditorEnabled();
+    Services.prefs.setBoolPref("devtools.webide.showProjectEditor", isNowEnabled);
+    if (!isNowEnabled) {
+      UI.destroyProjectEditor();
+    }
     UI.openProject();
   },
 
   showTroubleShooting: function() {
     UI.openInBrowser(HELP_URL);
   },
 
   showAddons: function() {
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -4,30 +4,33 @@
    - 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/. -->
 
 <!DOCTYPE window [
   <!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
   %webideDTD;
 ]>
 
+<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
+
 <?xml-stylesheet href="chrome://global/skin/global.css"?>
 <?xml-stylesheet href="chrome://webide/skin/webide.css"?>
 
 <window id="webide"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml"
         title="&windowTitle;"
         windowtype="devtools:webide"
         macanimationtype="document"
         fullscreenbutton="true"
         screenX="4" screenY="4"
         width="640" height="480"
         persist="screenX screenY width height">
 
+  <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script>
   <script type="application/javascript" src="webide.js"></script>
   <script type="application/javascript" src="cli.js"></script>
 
   <commandset id="mainCommandSet">
     <commandset id="editMenuCommands"/>
     <commandset id="webideCommands">
       <command id="cmd_quit" oncommand="Cmds.quit()"/>
       <command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/>
--- a/browser/locales/en-US/chrome/browser/devtools/app-manager.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.properties
@@ -24,17 +24,17 @@ validator.noAccessManifestURL=Unable to 
 # LOCALIZATION NOTE (validator.invalidHostedManifestURL): %1$S is the URI of
 # the manifest, %2$S is the error message.
 validator.invalidHostedManifestURL=Invalid hosted manifest URL '%1$S': %2$S
 validator.invalidProjectType=Unknown project type '%S'
 # LOCALIZATION NOTE (validator.missNameManifestProperty, validator.missIconsManifestProperty):
 # don't translate 'icons' and 'name'.
 validator.missNameManifestProperty=Missing mandatory 'name' in Manifest.
 validator.missIconsManifestProperty=Missing 'icons' in Manifest.
-validator.missIconMarketplace=app submission to the Marketplace needs at least a 128px icon
+validator.missIconMarketplace2=app submission to the Marketplace requires a 128px icon
 validator.invalidAppType=Unknown app type: '%S'.
 validator.invalidHostedPriviledges=Hosted App can't be type '%S'.
 validator.noCertifiedSupport='certified' apps are not fully supported on the App manager.
 validator.nonAbsoluteLaunchPath=Launch path has to be an absolute path starting with '/': '%S'
 validator.accessFailedLaunchPath=Unable to access the app starting document '%S'
 # LOCALIZATION NOTE (validator.accessFailedLaunchPathBadHttpCode): %1$S is the URI of
 # the launch document, %2$S is the http error code.
 validator.accessFailedLaunchPathBadHttpCode=Unable to access the app starting document '%1$S', got HTTP code %2$S
--- a/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/projecteditor.properties
@@ -27,20 +27,29 @@ projecteditor.deletePromptTitle=Delete
 projecteditor.deleteFolderPromptMessage=Are you sure you want to delete this folder?
 
 # LOCALIZATION NOTE (projecteditor.deleteFilePromptMessage):
 # This string is displayed as as the message of the confirm prompt that checks
 # to make sure if a file should be removed.
 projecteditor.deleteFilePromptMessage=Are you sure you want to delete this file?
 
 # LOCALIZATION NOTE (projecteditor.newLabel):
-# This string is displayed as a context menu item for adding a new file to
+# This string is displayed as a menu item for adding a new file to
 # the directory.
 projecteditor.newLabel=New…
 
+# LOCALIZATION NOTE (projecteditor.saveLabel):
+# This string is displayed as a menu item for saving the current file.
+projecteditor.saveLabel=Save
+
+# LOCALIZATION NOTE (projecteditor.saveAsLabel):
+# This string is displayed as a menu item for saving the current file
+# with a new name.
+projecteditor.saveAsLabel=Save As…
+
 # LOCALIZATION NOTE (projecteditor.selectFileLabel):
 # This string is displayed as the title on the file picker when saving a file.
 projecteditor.selectFileLabel=Select a File
 
 # LOCALIZATION NOTE (projecteditor.openFolderLabel):
 # This string is displayed as the title on the file picker when opening a folder.
 projecteditor.openFolderLabel=Select a Folder
 
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1869,28 +1869,30 @@ richlistitem[type~="action"][actiontype=
   background-color: Highlight;
   transition: none;
 }
 
 #TabsToolbar .toolbarbutton-1 {
   margin-bottom: @tabToolbarNavbarOverlap@;
 }
 
-#TabsToolbar .toolbarbutton-1 > .toolbarbutton-icon,
-#TabsToolbar .toolbarbutton-1 > .toolbarbutton-menu-dropmarker,
-#TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
-  margin-top: -2px;
-  margin-bottom: -2px;
+#alltabs-button {
+  list-style-image: url("chrome://browser/skin/tabbrowser/alltabs.png");
+}
+
+#TabsToolbar[brighttext] > #alltabs-button,
+#TabsToolbar[brighttext] > toolbarpaletteitem > #alltabs-button {
+  list-style-image: url("chrome://browser/skin/tabbrowser/alltabs-inverted.png");
+}
+
+#alltabs-button > .toolbarbutton-icon {
+  padding: 9px 6px 6px;
 }
 
 #alltabs-button > .toolbarbutton-menu-dropmarker {
-  margin-bottom: -2px;
-}
-
-#alltabs-button > .toolbarbutton-icon {
   display: none;
 }
 
 /* All tabs menupopup */
 .alltabs-item > .menu-iconic-left > .menu-iconic-icon {
   list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
 }
 
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -155,16 +155,18 @@ browser.jar:
   skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
   skin/classic/browser/social/services-16.png         (social/services-16.png)
   skin/classic/browser/social/services-64.png         (social/services-64.png)
   skin/classic/browser/social/share-button.png        (social/share-button.png)
   skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
   skin/classic/browser/social/chat-icons.png          (social/chat-icons.png)
   skin/classic/browser/social/gear_default.png        (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png        (../shared/social/gear_clicked.png)
+  skin/classic/browser/tabbrowser/alltabs.png         (tabbrowser/alltabs.png)
+  skin/classic/browser/tabbrowser/alltabs-inverted.png (tabbrowser/alltabs-inverted.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab-active-middle.png     (tabbrowser/tab-active-middle.png)
   skin/classic/browser/tabbrowser/tab-arrow-left.png        (tabbrowser/tab-arrow-left.png)
   skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
   skin/classic/browser/tabbrowser/tab-background-end.png    (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-start.png  (tabbrowser/tab-background-start.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f3261f1da05abdf5fdabdfa17083243deb74b78d
GIT binary patch
literal 221
zc%17D@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a@dl*-CY>|
zgW!U_%O``>7kES#Gca%qgD@k*tT_@O14=x7UD@w2aq<fD?o2z91Qd$(ba4!kxLkVH
zkgGXBfaQX?&YH>IPfM;X`;@eM!`20H50@JzxGbMjX~4hdW6^xxwpFukyROPQ#$bEw
zOiZ7Gv3Sq#b%%w6UDmAKDkiACZ14NJ{q-NX<~)4PCzDvu7JpdGd79g?*n>ce7(8A5
KT-G@yGywn~Buftf
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a7abe739634da367dd3af96dacbe8c4c1327f071
GIT binary patch
literal 287
zc%17D@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a@dl*-CY>|
zgW!U_%O``>7kES#Gca%qgD@k*tT_@O14=x7UD@w2aq<dEaWpHd1BK>$x;TbNTrNGe
zmg`UgL+iux$G6qhn(H5IxV?4OTC>PFqnUeT^beetVC#0=z~NS?VVog#`#|zcrA6%b
z3LX@yd}TcS|0!FBMc?){CVyw8duyNgJmpW7ou_L?!`u*`>gI=w?q!AdZWM`hE9#u=
zTr`K-TD+q%V}E4pwa{B%Z%2GDw9mP2tiY6{viFq-m)ql$zCi(XDjz%VeGvZRRKx6P
a!zgv7?)`BlUuU3G89ZJ6T-G@yGywp8IB1*z
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -156,18 +156,21 @@ toolbarseparator {
   position: relative;
   z-index: 1;
 }
 
 #nav-bar-customization-target {
   padding: 4px;
 }
 
+#PersonalToolbar {
+  padding: 0 4px 4px;
+}
+
 #PersonalToolbar:not([collapsed=true]) {
-  padding: 0 4px 4px;
   /* 4px padding ^  plus 19px personal-bookmarks (see below) */
   min-height: 23px;
 }
 
 #navigator-toolbox > toolbar:not(#TabsToolbar):-moz-lwtheme {
   background-color: @toolbarColorLWT@;
   background-image: url(chrome://browser/skin/Toolbar-background-noise.png);
 }
--- a/browser/themes/osx/devtools/floating-scrollbars.css
+++ b/browser/themes/osx/devtools/floating-scrollbars.css
@@ -2,29 +2,32 @@
 
 scrollbar {
   -moz-appearance: none;
   position: relative;
   background-color: transparent;
   background-image: none;
   border: 0px solid transparent;
   z-index: 2147483647;
-  -moz-box-align: start;
   padding: 2px;
 }
 
 scrollbar[orient="vertical"] {
   -moz-margin-start: -8px;
   min-width: 8px;
   max-width: 8px;
 }
 
 scrollbar[orient="horizontal"] {
   margin-top: -8px;
   min-height: 8px;
   max-height: 8px;
 }
 
+slider {
+  -moz-appearance: none !important;
+}
+
 thumb {
   -moz-appearance: none !important;
   background-color: rgba(0,0,0,0.2);
   border-radius: 3px;
 }
--- a/browser/themes/shared/devtools/projecteditor/projecteditor.css
+++ b/browser/themes/shared/devtools/projecteditor/projecteditor.css
@@ -21,18 +21,16 @@
   -moz-appearance: treetwistyopen;
 }
 
 .arrow[invisible] {
   visibility: hidden;
 }
 
 #projecteditor-menubar {
-  /* XXX: Hide menu bar until we have option to add menu items
-     to an existing one. */
   display: none;
 }
 
 #projecteditor-toolbar,
 #projecteditor-toolbar-bottom {
   display: none; /* For now don't show the status bars */
   min-height: 22px;
   height: 22px;
--- a/browser/themes/shared/incontentprefs/preferences.css
+++ b/browser/themes/shared/incontentprefs/preferences.css
@@ -1,18 +1,23 @@
 %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
 @namespace html "http://www.w3.org/1999/xhtml";
 
+#dialogBox,
+dialog,
+prefwindow,
+.windowDialog,
 page {
   -moz-appearance: none;
   background-color: white;
+  color: #424E5A;
 }
 
 * {
   -moz-user-select: text;
 }
 
 caption {
   -moz-appearance: none;
@@ -32,17 +37,16 @@ caption > label {
 }
 
 prefpane {
   max-width: 800px;
   padding: 0;
   font: message-box;
   font-size: 1.25rem;
   line-height: 22px;
-  color: #424E5A;
 }
 
 prefpane > .content-box {
   overflow: visible;
 }
 
 /* groupboxes */
 
@@ -65,17 +69,16 @@ groupbox label {
 }
 
 /* tabpanels and tabs */
 
 tabpanels {
   -moz-appearance: none;
   font-size: 1.25rem;
   line-height: 22px;
-  color: #424E5A;
   border: none;
   padding: 0;
   background-color: transparent;
 }
 
 tabs {
   -moz-margin-start: 60px;
   margin-bottom: 15px;
@@ -179,16 +182,17 @@ button[type="menu"] > .button-box > .but
   background-image: none;
 }
 
 .help-button:not([disabled="true"]):hover:active {
   background-color: #EABA00;
   background-image: none;
 }
 
+.close-icon > .button-box,
 .help-button > .button-box {
   padding-top: 0;
   padding-bottom: 0;
   padding-right: 0 !important;
   padding-left: 0 !important;
 }
 
 .help-button > .button-box > .button-icon {
@@ -827,8 +831,46 @@ description > html|a {
   /* center the links */
   margin-top: 8px;
   margin-bottom: 8px;
 }
 
 .indent {
   -moz-margin-start: 33px;
 }
+
+/**
+ * Sub-dialog
+ */
+
+#dialogOverlay {
+  background-color: rgba(0,0,0,0.5);
+  visibility: hidden;
+}
+
+#dialogBox {
+  border: 1px solid #666;
+  display: -moz-box;
+}
+
+.close-icon {
+  background-color: transparent !important;
+  border: none;
+  box-shadow: none;
+  height: 18px;
+  padding: 0;
+  min-width: 18px;
+}
+
+#dialogBox > .groupbox-body {
+  -moz-appearance: none;
+  padding: 0;
+}
+
+#dialogFrame {
+  /* Default dialog dimensions */
+  height: 30em;
+  width: 30em;
+}
+
+/**
+ * End sub-dialog
+ */
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -2655,23 +2655,23 @@ public class BrowserApp extends GeckoApp
      * If the app has been launched a certain number of times, and we haven't asked for feedback before,
      * open a new tab with about:feedback when launching the app from the icon shortcut.
      */
     @Override
     protected void onNewIntent(Intent intent) {
         String action = intent.getAction();
 
         final boolean isViewAction = Intent.ACTION_VIEW.equals(action);
-        final boolean isBookmarkAction = GeckoApp.ACTION_BOOKMARK.equals(action);
+        final boolean isBookmarkAction = GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action);
 
         if (mInitialized && (isViewAction || isBookmarkAction)) {
             // Dismiss editing mode if the user is loading a URL from an external app.
             mBrowserToolbar.cancelEdit();
 
-            // GeckoApp.ACTION_BOOKMARK means we're opening a bookmark that
+            // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
             // was added to Android's homescreen.
             final TelemetryContract.Method method =
                 (isViewAction ? TelemetryContract.Method.INTENT : TelemetryContract.Method.HOMESCREEN);
 
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method);
         }
 
         super.onNewIntent(intent);
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -141,17 +141,17 @@ public abstract class GeckoApp
 
     private static enum StartupAction {
         NORMAL,     /* normal application start */
         URL,        /* launched with a passed URL */
         PREFETCH    /* launched with a passed URL that we prefetch */
     }
 
     public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
-    public static final String ACTION_BOOKMARK             = "org.mozilla.gecko.BOOKMARK";
+    public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
     public static final String ACTION_DEBUG                = "org.mozilla.gecko.DEBUG";
     public static final String ACTION_LAUNCH_SETTINGS      = "org.mozilla.gecko.SETTINGS";
     public static final String ACTION_LOAD                 = "org.mozilla.gecko.LOAD";
     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
     public static final String ACTION_WEBAPP_PREFIX        = "org.mozilla.gecko.WEBAPP";
 
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
 
@@ -1888,17 +1888,17 @@ public abstract class GeckoApp
                                             Tabs.LOADURL_USER_ENTERED |
                                             Tabs.LOADURL_EXTERNAL);
         } else if (action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) {
             // A lightweight mechanism for loading a web page as a webapp
             // without installing the app natively nor registering it in the DOM
             // application registry.
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createWebappLoadEvent(uri));
-        } else if (ACTION_BOOKMARK.equals(action)) {
+        } else if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBookmarkLoadEvent(uri));
         } else if (Intent.ACTION_SEARCH.equals(action)) {
             String uri = getURIFromIntent(intent);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(uri));
         } else if (ACTION_ALERT_CALLBACK.equals(action)) {
             processAlertCallback(intent);
         } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
@@ -1918,17 +1918,17 @@ public abstract class GeckoApp
         final String action = intent.getAction();
         if (ACTION_ALERT_CALLBACK.equals(action))
             return null;
 
         String uri = intent.getDataString();
         if (uri != null)
             return uri;
 
-        if ((action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) || ACTION_BOOKMARK.equals(action)) {
+        if ((action != null && action.startsWith(ACTION_WEBAPP_PREFIX)) || ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             uri = intent.getStringExtra("args");
             if (uri != null && uri.startsWith("--url=")) {
                 uri.replace("--url=", "");
             }
         }
         return uri;
     }
 
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -853,17 +853,17 @@ public class GeckoAppShell
     private static void doCreateShortcut(final String aTitle, final String aURI, final String aUniqueURI,
                                          final Bitmap aIcon, final String aType) {
         // The intent to be launched by the shortcut.
         Intent shortcutIntent;
         if (aType.equalsIgnoreCase(SHORTCUT_TYPE_WEBAPP)) {
             shortcutIntent = getWebappIntent(aURI, aUniqueURI, aTitle, aIcon);
         } else {
             shortcutIntent = new Intent();
-            shortcutIntent.setAction(GeckoApp.ACTION_BOOKMARK);
+            shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
             shortcutIntent.setData(Uri.parse(aURI));
             shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
                                         AppConstants.BROWSER_INTENT_CLASS_NAME);
         }
 
         Intent intent = new Intent();
         intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
         intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, getLauncherIcon(aIcon, aType));
@@ -892,17 +892,17 @@ public class GeckoAppShell
                 // the intent to be launched by the shortcut
                 Intent shortcutIntent;
                 if (aType.equalsIgnoreCase(SHORTCUT_TYPE_WEBAPP)) {
                     shortcutIntent = getWebappIntent(aURI, aUniqueURI, "", null);
                     if (shortcutIntent == null)
                         return;
                 } else {
                     shortcutIntent = new Intent();
-                    shortcutIntent.setAction(GeckoApp.ACTION_BOOKMARK);
+                    shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
                     shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
                                                 AppConstants.BROWSER_INTENT_CLASS_NAME);
                     shortcutIntent.setData(Uri.parse(aURI));
                 }
         
                 Intent intent = new Intent();
                 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
                 if (aTitle != null)
--- a/mobile/android/base/GeckoThread.java
+++ b/mobile/android/base/GeckoThread.java
@@ -122,17 +122,17 @@ public class GeckoThread extends Thread 
 
         return resourcePath;
     }
 
     private String getTypeFromAction(String action) {
         if (action != null && action.startsWith(GeckoApp.ACTION_WEBAPP_PREFIX)) {
             return "-webapp";
         }
-        if (GeckoApp.ACTION_BOOKMARK.equals(action)) {
+        if (GeckoApp.ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
             return "-bookmark";
         }
         return null;
     }
 
     private String addCustomProfileArg(String args) {
         String profile = "";
         String guest = "";
--- a/mobile/android/base/background/announcements/AnnouncementsBroadcastService.java
+++ b/mobile/android/base/background/announcements/AnnouncementsBroadcastService.java
@@ -117,17 +117,18 @@ public class AnnouncementsBroadcastServi
   }
 
   @Override
   protected void onHandleIntent(Intent intent) {
     Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
 
     // Intent can be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     final String action = intent.getAction();
     Logger.debug(LOG_TAG, "Broadcast onReceive. Intent is " + action);
 
     if (AnnouncementsConstants.ACTION_ANNOUNCEMENTS_PREF.equals(action)) {
       handlePrefIntent(intent);
       return;
--- a/mobile/android/base/background/announcements/AnnouncementsService.java
+++ b/mobile/android/base/background/announcements/AnnouncementsService.java
@@ -115,17 +115,18 @@ public class AnnouncementsService extend
    * by the AlarmManager.
    */
   @Override
   public void onHandleIntent(Intent intent) {
     Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
 
     // Intent can be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     Logger.debug(LOG_TAG, "Running AnnouncementsService.");
 
     if (AnnouncementsConstants.DISABLED) {
       Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService.");
       return;
     }
--- a/mobile/android/base/background/healthreport/HealthReportBroadcastService.java
+++ b/mobile/android/base/background/healthreport/HealthReportBroadcastService.java
@@ -97,17 +97,18 @@ public class HealthReportBroadcastServic
   }
 
   @Override
   protected void onHandleIntent(Intent intent) {
     Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
 
     // Intent can be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     // The same intent can be handled by multiple methods so do not short-circuit evaluate.
     boolean handled = attemptHandleIntentForUpload(intent);
     handled = attemptHandleIntentForPrune(intent) ? true : handled;
 
     if (!handled) {
       Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + ".");
--- a/mobile/android/base/background/healthreport/prune/HealthReportPruneService.java
+++ b/mobile/android/base/background/healthreport/prune/HealthReportPruneService.java
@@ -37,17 +37,18 @@ public class HealthReportPruneService ex
   }
 
   @Override
   public void onHandleIntent(Intent intent) {
     Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
 
     // Intent can be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     Logger.debug(LOG_TAG, "Handling prune intent.");
 
     if (!isIntentValid(intent)) {
       Logger.warn(LOG_TAG, "Intent not valid - returning.");
       return;
     }
--- a/mobile/android/base/background/healthreport/upload/HealthReportUploadService.java
+++ b/mobile/android/base/background/healthreport/upload/HealthReportUploadService.java
@@ -40,17 +40,18 @@ public class HealthReportUploadService e
   }
 
   @Override
   public void onHandleIntent(Intent intent) {
     Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
 
     // Intent can be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
       Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling upload intent.");
       return;
     }
 
     Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; handling upload intent.");
--- a/mobile/android/base/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/base/fxa/receivers/FxAccountDeletedService.java
@@ -26,17 +26,18 @@ public class FxAccountDeletedService ext
   public FxAccountDeletedService() {
     super(LOG_TAG);
   }
 
   @Override
   protected void onHandleIntent(final Intent intent) {
     // Intent can, in theory, be null. Bug 1025937.
     if (intent == null) {
-        Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     final Context context = this;
 
     long intentVersion = intent.getLongExtra(
         FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, 0);
     long expectedVersion = FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION;
     if (intentVersion != expectedVersion) {
--- a/mobile/android/base/sync/receivers/SyncAccountDeletedService.java
+++ b/mobile/android/base/sync/receivers/SyncAccountDeletedService.java
@@ -30,16 +30,17 @@ public class SyncAccountDeletedService e
     super(LOG_TAG);
   }
 
   @Override
   protected void onHandleIntent(Intent intent) {
     // Intent can, in theory, be null. Bug 1025937.
     if (intent == null) {
       Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
+      return;
     }
 
     final Context context = this;
 
     long intentVersion = intent.getLongExtra(Constants.JSON_KEY_VERSION, 0);
     long expectedVersion = SyncConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION;
     if (intentVersion != expectedVersion) {
       Logger.warn(LOG_TAG, "Intent malformed: version " + intentVersion + " given but version " + expectedVersion + "expected. " +
--- a/mobile/android/chrome/content/Readability.js
+++ b/mobile/android/chrome/content/Readability.js
@@ -442,20 +442,22 @@ Readability.prototype = {
       }
 
       for (let nodeIndex = 0; nodeIndex < allElements.length; nodeIndex++) {
         if (!(node = allElements[nodeIndex]))
           continue;
 
         let matchString = node.className + node.id;
         if (matchString.search(this.REGEXPS.byline) !== -1 && !this._articleByline) {
-          this._articleByline = node.textContent;
-          node.parentNode.removeChild(node);
-          purgeNode(node);
-          continue;
+          if (this._isValidByline(node.textContent)) {
+            this._articleByline = node.textContent.trim();
+            node.parentNode.removeChild(node);
+            purgeNode(node);
+            continue;
+          }
         }
 
         // Remove unlikely candidates
         if (stripUnlikelyCandidates) {
           if (matchString.search(this.REGEXPS.unlikelyCandidates) !== -1 &&
             matchString.search(this.REGEXPS.okMaybeItsACandidate) === -1 &&
             node.tagName !== "BODY") {
             this.log("Removing unlikely candidate - " + matchString);
@@ -714,16 +716,32 @@ Readability.prototype = {
         }
 
         return articleContent;
       }
     }
   },
 
   /**
+   * Check whether the input string could be a byline.
+   * This verifies that the input is a string, and that the length
+   * is less than 100 chars.
+   *
+   * @param possibleByline {string} - a string to check whether its a byline.
+   * @return Boolean - whether the input string is a byline.
+   */
+  _isValidByline: function(byline) {
+    if (typeof byline == 'string' || byline instanceof String) {
+      byline = byline.trim();
+      return (byline.length > 0) && (byline.length < 100);
+    }
+    return false;
+  },
+
+  /**
    * Attempts to get the excerpt from these
    * sources in the following order:
    * - meta description tag
    * - open-graph description
    * - twitter cards description
    * - article's first paragraph
    * If no excerpt is found, an empty string will be
    * returned.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofill.jsm
@@ -0,0 +1,115 @@
+/* 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/. */
+
+/*
+ * Main module handling references to objects living in the main process.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "FormAutofill",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillIntegration",
+                                  "resource://gre/modules/FormAutofillIntegration.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
+/**
+ * Main module handling references to objects living in the main process.
+ */
+this.FormAutofill = {
+  /**
+   * Dynamically generated object implementing the FormAutofillIntegration
+   * methods.  Platform-specific code and add-ons can override methods of this
+   * object using the registerIntegration method.
+   */
+  get integration() {
+    // This lazy getter is only called if registerIntegration was never called.
+    this._refreshIntegrations();
+    return this.integration;
+  },
+
+  /**
+   * Registers new overrides for the FormAutofillIntegration methods.  Example:
+   *
+   *   FormAutofill.registerIntegration(base => ({
+   *     createRequestAutocompleteUI: Task.async(function* () {
+   *       yield base.createRequestAutocompleteUI.apply(this, arguments);
+   *     }),
+   *   }));
+   *
+   * @param aIntegrationFn
+   *        Function returning an object defining the methods that should be
+   *        overridden.  Its only parameter is an object that contains the base
+   *        implementation of all the available methods.
+   *
+   * @note The integration function is called every time the list of registered
+   *       integration functions changes.  Thus, it should not have any side
+   *       effects or do any other initialization.
+   */
+  registerIntegration: function (aIntegrationFn) {
+    this._integrationFns.add(aIntegrationFn);
+    this._refreshIntegrations();
+  },
+
+  /**
+   * Removes a previously registered FormAutofillIntegration override.
+   *
+   * Overrides don't usually need to be unregistered, unless they are added by a
+   * restartless add-on, in which case they should be unregistered when the
+   * add-on is disabled or uninstalled.
+   *
+   * @param aIntegrationFn
+   *        This must be the same function object passed to registerIntegration.
+   */
+  unregisterIntegration: function (aIntegrationFn) {
+    this._integrationFns.delete(aIntegrationFn);
+    this._refreshIntegrations();
+  },
+
+  /**
+   * Ordered list of registered functions defining integration overrides.
+   */
+  _integrationFns: new Set(),
+
+  /**
+   * Updates the "integration" getter with the object resulting from combining
+   * all the registered integration overrides with the default implementation.
+   */
+  _refreshIntegrations: function () {
+    delete this.integration;
+
+    let combined = FormAutofillIntegration;
+    for (let integrationFn of this._integrationFns) {
+      try {
+        // Obtain a new set of methods from the next integration function in the
+        // list, specifying the current combined object as the base argument.
+        let integration = integrationFn.call(null, combined);
+
+        // Retrieve a list of property descriptors from the returned object, and
+        // use them to build a new combined object whose prototype points to the
+        // previous combined object.
+        let descriptors = {};
+        for (let name of Object.getOwnPropertyNames(integration)) {
+          descriptors[name] = Object.getOwnPropertyDescriptor(integration, name);
+        }
+        combined = Object.create(combined, descriptors);
+      } catch (ex) {
+        // Any error will result in the current integration being skipped.
+        Cu.reportError(ex);
+      }
+    }
+
+    this.integration = combined;
+  },
+};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/formautofill/FormAutofillIntegration.jsm
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ *
+ * This module should not be imported directly, but the "integration" getter of
+ * the FormAutofill module should be used to get a reference to the currently
+ * defined implementations of the methods.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "FormAutofillIntegration",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
+/**
+ * This module defines the default implementation of platform-specific functions
+ * that can be overridden by the host application and by add-ons.
+ */
+this.FormAutofillIntegration = {
+  /**
+   * Creates a new RequestAutocompleteUI object.
+   *
+   * @param aProperties
+   *        Provides the initial properties for the newly created object.
+   *
+   * @return {Promise}
+   * @resolves The newly created RequestAutocompleteUI object.
+   * @rejects JavaScript exception.
+   */
+  createRequestAutocompleteUI: Task.async(function* (aProperties) {
+    return {};
+  }),
+};
--- a/toolkit/components/formautofill/moz.build
+++ b/toolkit/components/formautofill/moz.build
@@ -22,8 +22,13 @@ XPIDL_SOURCES += [
 ]
 
 XPIDL_MODULE = 'toolkit_formautofill'
 
 EXTRA_COMPONENTS += [
     'formautofill.manifest',
     'FormAutofillContentService.js',
 ]
+
+EXTRA_JS_MODULES += [
+    'FormAutofill.jsm',
+    'FormAutofillIntegration.jsm',
+]
--- a/toolkit/components/formautofill/test/head_common.js
+++ b/toolkit/components/formautofill/test/head_common.js
@@ -6,16 +6,18 @@
  */
 
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                   "resource://gre/modules/DownloadPaths.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FormAutofill",
+                                  "resource://gre/modules/FormAutofill.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
new file mode 100644
--- /dev/null
+++ b/toolkit/components/formautofill/test/xpcshell/test_integration.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests overriding the FormAutofillIntegration module functions.
+ */
+
+"use strict";
+
+/**
+ * Registers and unregisters an integration override function.
+ */
+add_task(function* test_integration_override() {
+  let overrideCalled = false;
+
+  let newIntegrationFn = base => ({
+    createRequestAutocompleteUI: Task.async(function* () {
+      yield base.createRequestAutocompleteUI.apply(this, arguments);
+      overrideCalled = true;
+    }),
+  });
+
+  FormAutofill.registerIntegration(newIntegrationFn);
+  try {
+    yield FormAutofill.integration.createRequestAutocompleteUI({});
+  } finally {
+    FormAutofill.unregisterIntegration(newIntegrationFn);
+  }
+
+  Assert.ok(overrideCalled);
+});
+
+/**
+ * Registers an integration override function that throws an exception, and
+ * ensures that this does not block other functions from being registered.
+ */
+add_task(function* test_integration_override_error() {
+  let overrideCalled = false;
+
+  let errorIntegrationFn = base => { throw "Expected error." };
+
+  let newIntegrationFn = base => ({
+    createRequestAutocompleteUI: Task.async(function* () {
+      yield base.createRequestAutocompleteUI.apply(this, arguments);
+      overrideCalled = true;
+    }),
+  });
+
+  FormAutofill.registerIntegration(errorIntegrationFn);
+  FormAutofill.registerIntegration(newIntegrationFn);
+  try {
+    yield FormAutofill.integration.createRequestAutocompleteUI({});
+  } finally {
+    FormAutofill.unregisterIntegration(errorIntegrationFn);
+    FormAutofill.unregisterIntegration(newIntegrationFn);
+  }
+
+  Assert.ok(overrideCalled);
+});
+
+add_task(terminationTaskFn);
--- a/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/formautofill/test/xpcshell/xpcshell.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 head = head.js ../head_common.js
 tail =
 
 [test_infrastructure.js]
+[test_integration.js]
--- a/toolkit/content/widgets/dialog.xml
+++ b/toolkit/content/widgets/dialog.xml
@@ -352,18 +352,24 @@
       <method name="_doButtonCommand">
         <parameter name="aDlgType"/>
         <body>
         <![CDATA[
           var button = this.getButton(aDlgType);
           if (!button.disabled) {
             var noCancel = this._fireButtonEvent(aDlgType);
             if (noCancel) {
-              if (aDlgType == "accept" || aDlgType == "cancel")
+              if (aDlgType == "accept" || aDlgType == "cancel") {
+                var closingEvent = new CustomEvent("dialogclosing", {
+                  bubbles: true,
+                  detail: { button: aDlgType },
+                });
+                this.dispatchEvent(closingEvent);
                 window.close();
+              }
             }
             return noCancel;
           }
           return true;
         ]]>
         </body>
       </method>
       
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/tern/README
@@ -0,0 +1,10 @@
+This is the Tern code-analysis engine packaged for the Mozilla Project.
+
+Tern is a stand-alone code-analysis engine for JavaScript. It is intended to be used with a code editor plugin to enhance the editor's support for intelligent JavaScript editing
+
+
+# Upgrade
+
+Currently used version is 0.6.2.  To upgrade, download the latest release from http://ternjs.net/, and copy the files from lib/ into this directory.
+
+You may also need to update the CodeMirror plugin found in browser/devtools/sourceeditor/codemirror/tern, but it will most likely work without updating.
--- a/toolkit/devtools/tern/browser.js
+++ b/toolkit/devtools/tern/browser.js
@@ -1238,17 +1238,19 @@ module.exports = {
         "!type": "+HTMLCollection",
         "!url": "https://developer.mozilla.org/en/docs/DOM/document.forms",
         "!doc": "Returns a collection (an HTMLCollection) of the form elements within the current document."
       },
       "styleSheets": {
         "!type": "+HTMLCollection",
         "!url": "https://developer.mozilla.org/en/docs/DOM/document.styleSheets",
         "!doc": "Returns a list of stylesheet objects for stylesheets explicitly linked into or embedded in a document."
-      }
+      },
+      "querySelector": "Element.prototype.querySelector",
+      "querySelectorAll": "Element.prototype.querySelectorAll"
     },
     "!url": "https://developer.mozilla.org/en/docs/DOM/document",
     "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
   },
   "document": {
     "!type": "+Document",
     "!url": "https://developer.mozilla.org/en/docs/DOM/document",
     "!doc": "Each web page loaded in the browser has its own document object. This object serves as an entry point to the web page's content (the DOM tree, including elements such as <body> and <table>) and provides functionality global to the document (such as obtaining the page's URL and creating new elements in the document)."
old mode 100644
new mode 100755
old mode 100644
new mode 100755
--- a/toolkit/devtools/tern/condense.js
+++ b/toolkit/devtools/tern/condense.js
@@ -78,30 +78,26 @@
     var len = 1, pos = 0, dot;
     while ((dot = path.indexOf(".", pos)) != -1) {
       pos = dot + 1;
       len += path.charAt(pos) == "!" ? 10 : 1;
     }
     return len;
   }
 
-  function isConcrete(path) {
-    return !/\!|<i>/.test(path);
-  }
-
   function hop(obj, prop) {
     return Object.prototype.hasOwnProperty.call(obj, prop);
   }
 
   function isSimpleInstance(o) {
     return o.proto && !(o instanceof infer.Fn) && o.proto != infer.cx().protos.Object &&
       o.proto.hasCtor && !o.hasCtor;
   }
 
-  function reach(type, path, id, state) {
+  function reach(type, path, id, state, byName) {
     var actual = type.getType(false);
     if (!actual) return;
     var orig = type.origin || actual.origin, relevant = false;
     if (orig) {
       var origPos = state.cx.origins.indexOf(orig);
       // This is a path that is newer than the code we are interested in.
       if (origPos > state.maxOrigin) return;
       relevant = state.isTarget(orig);
@@ -114,40 +110,41 @@
         var data = state.types[oldPath];
         if (data) {
           delete state.types[oldPath];
           state.altPaths[oldPath] = actual;
         } else data = {type: actual};
         data.span = state.getSpan(type) || (actual != type && state.isTarget(actual.origin) && state.getSpan(actual)) || data.span;
         data.doc = type.doc || (actual != type && state.isTarget(actual.origin) && type.doc) || data.doc;
         data.data = actual.metaData;
+        data.byName = data.byName == null ? !!byName : data.byName && byName;
         state.types[newPath] = data;
       }
     } else {
       if (relevant) state.altPaths[newPath] = actual;
     }
   }
-  function reachTypeOnly(aval, path, id, state) {
+  function reachByName(aval, path, id, state) {
     var type = aval.getType();
-    if (type) reach(type, path, id, state);
+    if (type) reach(type, path, id, state, true);
   }
 
   infer.Prim.prototype.reached = function() {return true;};
 
   infer.Arr.prototype.reached = function(path, state, concrete) {
-    if (!concrete) reachTypeOnly(this.getProp("<i>"), path, "<i>", state);
+    if (!concrete) reachByName(this.getProp("<i>"), path, "<i>", state);
     return true;
   };
 
   infer.Fn.prototype.reached = function(path, state, concrete) {
     infer.Obj.prototype.reached.call(this, path, state, concrete);
     if (!concrete) {
       for (var i = 0; i < this.args.length; ++i)
-        reachTypeOnly(this.args[i], path, "!" + i, state);
-      reachTypeOnly(this.retval, path, "!ret", state);
+        reachByName(this.args[i], path, "!" + i, state);
+      reachByName(this.retval, path, "!ret", state);
     }
     return true;
   };
 
   infer.Obj.prototype.reached = function(path, state, concrete) {
     if (isSimpleInstance(this) && !this.condenseForceInclude) {
       if (state.patchUp.indexOf(this) == -1) state.patchUp.push(this);
       return true;
@@ -173,27 +170,30 @@
     } else {
       path = obj.path;
     }
     for (var prop in obj.props)
       reach(obj.props[prop], path, prop, state);
   }
 
   function createPath(parts, state) {
-    var base = state.output;
-    for (var i = parts.length - 1; i >= 0; --i) if (!isConcrete(parts[i])) {
-      var def = parts.slice(0, i + 1).join(".");
-      var defs = state.output["!define"];
-      if (hop(defs, def)) base = defs[def];
-      else defs[def] = base = {};
-      parts = parts.slice(i + 1);
-    }
-    for (var i = 0; i < parts.length; ++i) {
-      if (hop(base, parts[i])) base = base[parts[i]];
-      else base = base[parts[i]] = {};
+    var base = state.output, defs = state.output["!define"];
+    for (var i = 0, path; i < parts.length; ++i) {
+      var part = parts[i], known = path && state.types[path];
+      path = path ? path + "." + part : part;
+      var me = state.types[path];
+      if (part.charAt(0) == "!" ||
+          known && known.type.constructor != infer.Obj ||
+          me && me.byName) {
+        if (hop(defs, path)) base = defs[path];
+        else defs[path] = base = {};
+      } else {
+        if (hop(base, parts[i])) base = base[part];
+        else base = base[part] = {};
+      }
     }
     return base;
   }
 
   function store(out, info, state) {
     var name = typeName(info.type);
     if (name != info.type.path && name != "?") {
       out["!type"] = name;
@@ -203,17 +203,20 @@
     }
     if (info.span) out["!span"] = info.span;
     if (info.doc) out["!doc"] = info.doc;
     if (info.data) out["!data"] = info.data;
   }
 
   function storeAlt(path, type, state) {
     var parts = path.split("."), last = parts.pop();
+    if (last[0] == "!") return;
+    var known = state.types[parts.join(".")];
     var base = createPath(parts, state);
+    if (known && known.type.constructor != infer.Obj) return;
     if (!hop(base, last)) base[last] = type.nameOverride || type.path;
   }
 
   var typeNameStack = [];
   function typeName(type) {
     var actual = type.getType(false);
     if (!actual || typeNameStack.indexOf(actual) > -1)
       return actual && actual.path || "?";
@@ -263,20 +266,22 @@
         data[prop] = simplify(data[prop], sort);
     }
     if (sawType && !sawOther) return data["!type"];
     return sort ? sortObject(data) : data;
   }
 
   function sortObject(obj) {
     var props = [], out = {};
-    for (var prop in obj) props.push({name: prop, val: obj[prop]});
-    props.sort(function(a, b) { return a.name < b.name ? -1 : 1; });
-    for (var i = 0; i < props.length; ++i)
-      out[props[i].name] = props[i].val;
+    for (var prop in obj) props.push(prop);
+    props.sort();
+    for (var i = 0; i < props.length; ++i) {
+      var prop = props[i];
+      out[prop] = obj[prop];
+    }
     return out;
   }
 
   function runPass(functions) {
     if (functions) for (var i = 0; i < functions.length; ++i)
       functions[i].apply(null, Array.prototype.slice.call(arguments, 1));
   }
 });
old mode 100644
new mode 100755
--- a/toolkit/devtools/tern/def.js
+++ b/toolkit/devtools/tern/def.js
@@ -1,15 +1,15 @@
 // Type description parser
-
+//
 // Type description JSON files (such as ecma5.json and browser.json)
 // are used to
 //
 // A) describe types that come from native code
-
+//
 // B) to cheaply load the types for big libraries, or libraries that
 //    can't be inferred well
 
 (function(mod) {
   if (typeof exports == "object" && typeof module == "object") // CommonJS
     return exports.init = mod;
   if (typeof define == "function" && define.amd) // AMD
     return define({init: mod});
@@ -76,16 +76,20 @@
       if (computeRetStart != null) fn.computeRetSource = this.spec.slice(computeRetStart, this.pos);
       return fn;
     },
     parseType: function(name, top) {
       if (this.eat("fn(")) {
         return this.parseFnType(name, top);
       } else if (this.eat("[")) {
         var inner = this.parseType();
+        if (inner == infer.ANull && this.spec == "[b.<i>]") {
+          var b = parsePath("b");
+          console.log(b.props["<i>"].types.length);
+        }
         this.eat("]") || this.error();
         if (top && this.base) {
           infer.Arr.call(this.base, inner);
           return this.base;
         }
         return new infer.Arr(inner);
       } else if (this.eat("+")) {
         var path = this.word(/[\w$<>\.!]/);
@@ -245,20 +249,20 @@
       if (prop.charAt(0) == "!") {
         if (prop == "!proto") {
           base = (base instanceof infer.Obj && base.proto) || infer.ANull;
         } else {
           var fn = base.getFunctionType();
           if (!fn) {
             base = infer.ANull;
           } else if (prop == "!ret") {
-            base = fn.retval && fn.retval.getType() || infer.ANull;
+            base = fn.retval && fn.retval.getType(false) || infer.ANull;
           } else {
             var arg = fn.args && fn.args[Number(prop.slice(1))];
-            base = (arg && arg.getType()) || infer.ANull;
+            base = (arg && arg.getType(false)) || infer.ANull;
           }
         }
       } else if (base instanceof infer.Obj) {
         var propVal = (prop == "prototype" && base instanceof infer.Fn) ? base.getProp(prop) : base.props[prop];
         if (!propVal || propVal.isEmpty())
           base = infer.ANull;
         else
           base = propVal.types[0];
@@ -273,17 +277,17 @@
   function emptyObj(ctor) {
     var empty = Object.create(ctor.prototype);
     empty.props = Object.create(null);
     empty.isShell = true;
     return empty;
   }
 
   function isSimpleAnnotation(spec) {
-    if (!spec["!type"] || /^fn\(/.test(spec["!type"])) return false;
+    if (!spec["!type"] || /^(fn\(|\[)/.test(spec["!type"])) return false;
     for (var prop in spec)
       if (prop != "!type" && prop != "!doc" && prop != "!url" && prop != "!span" && prop != "!data")
         return false;
     return true;
   }
 
   function passOne(base, spec, path) {
     if (!base) {
@@ -299,17 +303,17 @@
       }
       base.name = path;
     }
 
     for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
       var inner = spec[name];
       if (typeof inner == "string" || isSimpleAnnotation(inner)) continue;
       var prop = base.defProp(name);
-      passOne(prop.getType(), inner, path ? path + "." + name : name).propagate(prop);
+      passOne(prop.getType(false), inner, path ? path + "." + name : name).propagate(prop);
     }
     return base;
   }
 
   function passTwo(base, spec, path) {
     if (base.isShell) {
       delete base.isShell;
       var tp = spec["!type"];
@@ -323,26 +327,26 @@
 
     var effects = spec["!effects"];
     if (effects && base instanceof infer.Fn) for (var i = 0; i < effects.length; ++i)
       parseEffect(effects[i], base);
     copyInfo(spec, base);
 
     for (var name in spec) if (hop(spec, name) && name.charCodeAt(0) != 33) {
       var inner = spec[name], known = base.defProp(name), innerPath = path ? path + "." + name : name;
-      var type = known.getType();
+      var type = known.getType(false);
       if (typeof inner == "string") {
         if (type) continue;
         parseType(inner, innerPath).propagate(known);
       } else {
         if (!isSimpleAnnotation(inner)) {
           passTwo(type, inner, innerPath);
         } else if (!type) {
           parseType(inner["!type"], innerPath, null, true).propagate(known);
-          type = known.getType();
+          type = known.getType(false);
           if (type instanceof infer.Obj) copyInfo(inner, type);
         } else continue;
         if (inner["!doc"]) known.doc = inner["!doc"];
         if (inner["!url"]) known.url = inner["!url"];
         if (inner["!span"]) known.span = inner["!span"];
       }
     }
   }
@@ -403,22 +407,22 @@
   // computation.
   var customFunctions = Object.create(null);
   infer.registerFunction = function(name, f) { customFunctions[name] = f; };
 
   var IsCreated = infer.constraint("created, target, spec", {
     addType: function(tp) {
       if (tp instanceof infer.Obj && this.created++ < 5) {
         var derived = new infer.Obj(tp), spec = this.spec;
-        if (spec instanceof infer.AVal) spec = spec.getType();
+        if (spec instanceof infer.AVal) spec = spec.getType(false);
         if (spec instanceof infer.Obj) for (var prop in spec.props) {
           var cur = spec.props[prop].types[0];
           var p = derived.defProp(prop);
           if (cur && cur instanceof infer.Obj && cur.props.value) {
-            var vtp = cur.props.value.getType();
+            var vtp = cur.props.value.getType(false);
             if (vtp) p.addType(vtp);
           }
         }
         this.target.addType(derived);
       }
     }
   });
 
old mode 100644
new mode 100755
--- a/toolkit/devtools/tern/infer.js
+++ b/toolkit/devtools/tern/infer.js
@@ -114,27 +114,37 @@
     },
 
     getType: function(guess) {
       if (this.types.length == 0 && guess !== false) return this.makeupType();
       if (this.types.length == 1) return this.types[0];
       return canonicalType(this.types);
     },
 
+    computedPropType: function() {
+      if (!this.propertyOf || !this.propertyOf.hasProp("<i>")) return null;
+      var computedProp = this.propertyOf.getProp("<i>");
+      if (computedProp == this) return null;
+      return computedProp.getType();
+    },
+
     makeupType: function() {
+      var computed = this.computedPropType();
+      if (computed) return computed;
+
       if (!this.forward) return null;
       for (var i = this.forward.length - 1; i >= 0; --i) {
         var hint = this.forward[i].typeHint();
         if (hint && !hint.isEmpty()) {guessing = true; return hint;}
       }
 
       var props = Object.create(null), foundProp = null;
       for (var i = 0; i < this.forward.length; ++i) {
         var prop = this.forward[i].propHint();
-        if (prop && prop != "length" && prop != "<i>" && prop != "✖") {
+        if (prop && prop != "length" && prop != "<i>" && prop != "✖" && prop != cx.completingProperty) {
           props[prop] = true;
           foundProp = prop;
         }
       }
       if (!foundProp) return null;
 
       var objs = objsWithProp(foundProp);
       if (objs) {
@@ -158,16 +168,18 @@
         this.types[i].gatherProperties(f, depth);
     },
 
     guessProperties: function(f) {
       if (this.forward) for (var i = 0; i < this.forward.length; ++i) {
         var prop = this.forward[i].propHint();
         if (prop) f(prop, null, 0);
       }
+      var guessed = this.makeupType();
+      if (guessed) guessed.gatherProperties(f);
     }
   });
 
   function canonicalType(types) {
     var arrays = 0, fns = 0, objs = 0, prim = null;
     for (var i = 0; i < types.length; ++i) {
       var tp = types[i];
       if (tp instanceof Arr) ++arrays;
@@ -219,17 +231,18 @@
 
   var PropIsSubset = constraint("prop, target", {
     addType: function(type, weight) {
       if (type.getProp)
         type.getProp(this.prop).propagate(this.target, weight);
     },
     propHint: function() { return this.prop; },
     propagatesTo: function() {
-      return {target: this.target, pathExt: "." + this.prop};
+      if (this.prop == "<i>" || !/[^\w_]/.test(this.prop))
+        return {target: this.target, pathExt: "." + this.prop};
     }
   });
 
   var PropHasSubset = exports.PropHasSubset = constraint("prop, type, originNode", {
     addType: function(type, weight) {
       if (!(type instanceof Obj)) return;
       var prop = type.defProp(this.prop, this.originNode);
       prop.origin = this.origin;
@@ -374,25 +387,27 @@
     typeHint: function() { return this.inner.typeHint(); },
     propHint: function() { return this.inner.propHint(); }
   });
 
   // TYPE OBJECTS
 
   var Type = exports.Type = function() {};
   Type.prototype = extend(ANull, {
+    constructor: Type,
     propagate: function(c, w) { c.addType(this, w); },
     hasType: function(other) { return other == this; },
     isEmpty: function() { return false; },
     typeHint: function() { return this; },
     getType: function() { return this; }
   });
 
   var Prim = exports.Prim = function(proto, name) { this.name = name; this.proto = proto; };
   Prim.prototype = extend(Type.prototype, {
+    constructor: Prim,
     toString: function() { return this.name; },
     getProp: function(prop) {return this.proto.hasProp(prop) || ANull;},
     gatherProperties: function(f, depth) {
       if (this.proto) this.proto.gatherProperties(f, depth);
     }
   });
 
   var Obj = exports.Obj = function(proto, name) {
@@ -402,16 +417,17 @@
       var match = /^(.*)\.prototype$/.exec(this.proto.name);
       if (match) name = match[1];
     }
     this.name = name;
     this.maybeProps = null;
     this.origin = cx.curOrigin;
   };
   Obj.prototype = extend(Type.prototype, {
+    constructor: Obj,
     toString: function(maxDepth) {
       if (!maxDepth && this.name) return this.name;
       var props = [], etc = false;
       for (var prop in this.props) if (prop != "<i>") {
         if (props.length > 5) { etc = true; break; }
         if (maxDepth)
           props.push(prop + ": " + toString(this.props[prop].getType(), maxDepth - 1));
         else
@@ -425,40 +441,44 @@
       var found = this.props[prop];
       if (searchProto !== false)
         for (var p = this.proto; p && !found; p = p.proto) found = p.props[prop];
       return found;
     },
     defProp: function(prop, originNode) {
       var found = this.hasProp(prop, false);
       if (found) {
+        if (found.maybePurge) found.maybePurge = false;
         if (originNode && !found.originNode) found.originNode = originNode;
         return found;
       }
       if (prop == "__proto__" || prop == "✖") return ANull;
 
       var av = this.maybeProps && this.maybeProps[prop];
       if (av) {
         delete this.maybeProps[prop];
         this.maybeUnregProtoPropHandler();
       } else {
         av = new AVal;
+        av.propertyOf = this;
       }
 
       this.props[prop] = av;
       av.originNode = originNode;
       av.origin = cx.curOrigin;
       this.broadcastProp(prop, av, true);
       return av;
     },
     getProp: function(prop) {
       var found = this.hasProp(prop, true) || (this.maybeProps && this.maybeProps[prop]);
       if (found) return found;
       if (prop == "__proto__" || prop == "✖") return ANull;
-      return this.ensureMaybeProps()[prop] = new AVal;
+      var av = this.ensureMaybeProps()[prop] = new AVal;
+      av.propertyOf = this;
+      return av;
     },
     broadcastProp: function(prop, val, local) {
       if (local) {
         this.signal("addProp", prop, val);
         // If this is a scope, it shouldn't be registered
         if (!(this instanceof Scope)) registerProp(prop, this);
       }
 
@@ -524,16 +544,17 @@
   var Fn = exports.Fn = function(name, self, args, argNames, retval) {
     Obj.call(this, cx.protos.Function, name);
     this.self = self;
     this.args = args;
     this.argNames = argNames;
     this.retval = retval;
   };
   Fn.prototype = extend(Obj.prototype, {
+    constructor: Fn,
     toString: function(maxDepth) {
       if (maxDepth) maxDepth--;
       var str = "fn(";
       for (var i = 0; i < this.args.length; ++i) {
         if (i) str += ", ";
         var name = this.argNames[i];
         if (name && name != "?") str += name + ": ";
         str += toString(this.args[i].getType(), maxDepth, this);
@@ -571,16 +592,17 @@
   });
 
   var Arr = exports.Arr = function(contentType) {
     Obj.call(this, cx.protos.Array);
     var content = this.defProp("<i>");
     if (contentType) contentType.propagate(content);
   };
   Arr.prototype = extend(Obj.prototype, {
+    constructor: Arr,
     toString: function(maxDepth) {
       return "[" + toString(this.getProp("<i>").getType(), maxDepth, this) + "]";
     }
   });
 
   // THE PROPERTY REGISTRY
 
   function registerProp(prop, obj) {
@@ -631,48 +653,68 @@
 
   exports.withContext = function(context, f) {
     var old = cx;
     cx = context;
     try { return f(); }
     finally { cx = old; }
   };
 
+  exports.TimedOut = function() {
+    this.message = "Timed out";
+    this.stack = (new Error()).stack;
+  }
+  exports.TimedOut.prototype = Object.create(Error.prototype);
+  exports.TimedOut.prototype.name = "infer.TimedOut";
+
+  var timeout;
+  exports.withTimeout = function(ms, f) {
+    var end = +new Date + ms;
+    var oldEnd = timeout;
+    if (oldEnd && oldEnd < end) return f();
+    timeout = end;
+    try { return f(); }
+    finally { timeout = oldEnd; }
+  };
+
   exports.addOrigin = function(origin) {
     if (cx.origins.indexOf(origin) < 0) cx.origins.push(origin);
   };
 
   var baseMaxWorkDepth = 20, reduceMaxWorkDepth = .0001;
   function withWorklist(f) {
     if (cx.workList) return f(cx.workList);
 
     var list = [], depth = 0;
     var add = cx.workList = function(type, target, weight) {
       if (depth < baseMaxWorkDepth - reduceMaxWorkDepth * list.length)
         list.push(type, target, weight, depth);
     };
     try {
       var ret = f(add);
       for (var i = 0; i < list.length; i += 4) {
+        if (timeout && +new Date >= timeout)
+          throw new exports.TimedOut();
         depth = list[i + 3] + 1;
         list[i + 1].addType(list[i], list[i + 2]);
       }
       return ret;
     } finally {
       cx.workList = null;
     }
   }
 
   // SCOPES
 
   var Scope = exports.Scope = function(prev) {
     Obj.call(this, prev || true);
     this.prev = prev;
   };
   Scope.prototype = extend(Obj.prototype, {
+    constructor: Scope,
     defVar: function(name, originNode) {
       for (var s = this; ; s = s.proto) {
         var found = s.props[name];
         if (found) return found;
         if (!s.prev) return s.defProp(name, originNode);
       }
     }
   });
@@ -712,18 +754,19 @@
     for (var i = 0; i < fn.args.length; ++i) fn.args[i] = new AVal;
     fn.self = new AVal;
     fn.computeRet = function(self, args) {
       // Prevent recursion
       return withDisabledComputing(fn, function() {
         var oldOrigin = cx.curOrigin;
         cx.curOrigin = fn.origin;
         var scopeCopy = new Scope(scope.prev);
+        scopeCopy.originNode = scope.originNode;
         for (var v in scope.props) {
-          var local = scopeCopy.defProp(v);
+          var local = scopeCopy.defProp(v, scope.props[v].originNode);
           for (var i = 0; i < args.length; ++i) if (fn.argNames[i] == v && i < args.length)
             args[i].propagate(local);
         }
         var argNames = fn.argNames.length != args.length ? fn.argNames.slice(0, args.length) : fn.argNames;
         while (argNames.length < args.length) argNames.push("?");
         scopeCopy.fnType = new Fn(fn.name, self, args, argNames, ANull);
         if (fn.arguments) {
           var argset = scopeCopy.fnType.arguments = new AVal;
@@ -775,25 +818,23 @@
       fn.computeRetSource = foundPath;
       return true;
     }
   }
 
   // SCOPE GATHERING PASS
 
   function addVar(scope, nameNode) {
-    var val = scope.defProp(nameNode.name, nameNode);
-    if (val.maybePurge) val.maybePurge = false;
-    return val;
+    return scope.defProp(nameNode.name, nameNode);
   }
 
   var scopeGatherer = walk.make({
     Function: function(node, scope, c) {
       var inner = node.body.scope = new Scope(scope);
-      inner.node = node;
+      inner.originNode = node;
       var argVals = [], argNames = [];
       for (var i = 0; i < node.params.length; ++i) {
         var param = node.params[i];
         argNames.push(param.name);
         argVals.push(addVar(inner, param));
       }
       inner.fnType = new Fn(node.id && node.id.name, new AVal, argVals, argNames, ANull);
       inner.fnType.originNode = node;
@@ -1070,20 +1111,23 @@
       for (var i = 0; i < node.declarations.length; ++i) {
         var decl = node.declarations[i], prop = scope.getProp(decl.id.name);
         if (decl.init)
           infer(decl.init, scope, c, prop, decl.id.name);
       }
     },
 
     ReturnStatement: function(node, scope, c) {
-      if (node.argument && scope.fnType) {
+      if (!node.argument) return;
+      var output = ANull;
+      if (scope.fnType) {
         if (scope.fnType.retval == ANull) scope.fnType.retval = new AVal;
-        infer(node.argument, scope, c, scope.fnType.retval);
+        output = scope.fnType.retval;
       }
+      infer(node.argument, scope, c, output);
     },
 
     ForInStatement: function(node, scope, c) {
       var source = infer(node.right, scope, c);
       if ((node.right.type == "Identifier" && node.right.name in scope.props) ||
           (node.right.type == "MemberExpression" && node.right.property.name == "prototype")) {
         maybeInstantiate(scope, 5);
         var varName;
@@ -1181,16 +1225,17 @@
         f.purge(test);
       }
     }
   };
   ANull.purge = function() {};
   Obj.prototype.purge = function(test) {
     if (this.purgeGen == cx.purgeGen) return true;
     this.purgeGen = cx.purgeGen;
+    var props = [];
     for (var p in this.props) {
       var av = this.props[p];
       if (test(av, av.originNode))
         this.removeProp(p);
       av.purge(test);
     }
   };
   Fn.prototype.purge = function(test) {
old mode 100644
new mode 100755
old mode 100644
new mode 100755
--- a/toolkit/devtools/tern/tern.js
+++ b/toolkit/devtools/tern/tern.js
@@ -12,23 +12,24 @@
     return define(["exports", "./infer", "./signal", "acorn/acorn", "acorn/util/walk"], mod);
   mod(self.tern || (self.tern = {}), tern, tern.signal, acorn, acorn.walk); // Plain browser env
 })(function(exports, infer, signal, acorn, walk) {
   "use strict";
 
   var plugins = Object.create(null);
   exports.registerPlugin = function(name, init) { plugins[name] = init; };
 
-  var defaultOptions = {
+  var defaultOptions = exports.defaultOptions = {
     debug: false,
     async: false,
     getFile: function(_f, c) { if (this.async) c(null, null); },
     defs: [],
     plugins: {},
-    fetchTimeout: 1000
+    fetchTimeout: 1000,
+    dependencyBudget: 20000
   };
 
   var queryTypes = {
     completions: {
       takesFile: true,
       run: findCompletions
     },
     properties: {
@@ -58,36 +59,39 @@
     },
     files: {
       run: listFiles
     }
   };
 
   exports.defineQueryType = function(name, desc) { queryTypes[name] = desc; };
 
-  function File(name) {
+  function File(name, parent) {
     this.name = name;
+    this.parent = parent;
     this.scope = this.text = this.ast = this.lineOffsets = null;
   }
   File.prototype.asLineChar = function(pos) { return asLineChar(this, pos); };
 
   function updateText(file, text, srv) {
     file.text = text;
-    file.ast = infer.parse(text, srv.passes, {directSourceFile: file});
+    file.ast = infer.parse(text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
     file.lineOffsets = null;
   }
 
   var Server = exports.Server = function(options) {
     this.cx = null;
     this.options = options || {};
     for (var o in defaultOptions) if (!options.hasOwnProperty(o))
       options[o] = defaultOptions[o];
 
     this.handlers = Object.create(null);
     this.files = [];
+    this.fileMap = Object.create(null);
+    this.budgets = Object.create(null);
     this.uses = 0;
     this.pending = 0;
     this.asyncError = null;
     this.passes = Object.create(null);
 
     this.defs = options.defs.slice(0);
     for (var plugin in options.plugins) if (options.plugins.hasOwnProperty(plugin) && plugin in plugins) {
       var init = plugins[plugin](this, options.plugins[plugin]);
@@ -97,57 +101,61 @@
       }
       if (init && init.passes) for (var type in init.passes) if (init.passes.hasOwnProperty(type))
         (this.passes[type] || (this.passes[type] = [])).push(init.passes[type]);
     }
 
     this.reset();
   };
   Server.prototype = signal.mixin({
-    addFile: function(name, /*optional*/ text) {
-      ensureFile(this, name, text);
+    addFile: function(name, /*optional*/ text, parent) {
+      // Don't crash when sloppy plugins pass non-existent parent ids
+      if (parent && !parent in this.fileMap) parent = null;
+      ensureFile(this, name, parent, text);
     },
     delFile: function(name) {
       for (var i = 0, f; i < this.files.length; ++i) if ((f = this.files[i]).name == name) {
-        clearFile(this, f);
+        clearFile(this, f, null, true);
         this.files.splice(i--, 1);
+        delete this.fileMap[name];
         return;
       }
     },
     reset: function() {
       this.signal("reset");
       this.cx = new infer.Context(this.defs, this);
       this.uses = 0;
+      this.budgets = Object.create(null);
       for (var i = 0; i < this.files.length; ++i) {
         var file = this.files[i];
         file.scope = null;
       }
     },
 
     request: function(doc, c) {
       var inv = invalidDoc(doc);
       if (inv) return c(inv);
 
       var self = this;
       doRequest(this, doc, function(err, data) {
         c(err, data);
         if (self.uses > 40) {
           self.reset();
-          analyzeAll(self, function(){});
+          analyzeAll(self, null, function(){});
         }
       });
     },
 
     findFile: function(name) {
-      return findFile(this.files, name);
+      return this.fileMap[name];
     },
 
     flush: function(c) {
       var cx = this.cx;
-      analyzeAll(this, function(err) {
+      analyzeAll(this, null, function(err) {
         if (err) return c(err);
         infer.withContext(cx, c);
       });
     },
 
     startAsyncAction: function() {
       ++this.pending;
     },
@@ -164,91 +172,100 @@
     var query = doc.query;
     // Respond as soon as possible when this just uploads files
     if (!query) c(null, {});
 
     var files = doc.files || [];
     if (files.length) ++srv.uses;
     for (var i = 0; i < files.length; ++i) {
       var file = files[i];
-      ensureFile(srv, file.name, file.type == "full" ? file.text : null);
+      ensureFile(srv, file.name, null, file.type == "full" ? file.text : null);
     }
 
+    var timeBudget = typeof doc.timeout == "number" ? [doc.timeout] : null;
     if (!query) {
-      analyzeAll(srv, function(){});
+      analyzeAll(srv, timeBudget, function(){});
       return;
     }
 
     var queryType = queryTypes[query.type];
     if (queryType.takesFile) {
       if (typeof query.file != "string") return c(".query.file must be a string");
-      if (!/^#/.test(query.file)) ensureFile(srv, query.file);
+      if (!/^#/.test(query.file)) ensureFile(srv, query.file, null);
     }
 
-    analyzeAll(srv, function(err) {
+    analyzeAll(srv, timeBudget, function(err) {
       if (err) return c(err);
       var file = queryType.takesFile && resolveFile(srv, files, query.file);
       if (queryType.fullFile && file.type == "part")
         return c("Can't run a " + query.type + " query on a file fragment");
 
-      infer.withContext(srv.cx, function() {
+      function run() {
         var result;
         try {
           result = queryType.run(srv, query, file);
         } catch (e) {
           if (srv.options.debug && e.name != "TernError") console.error(e.stack);
           return c(e);
         }
         c(null, result);
-      });
+      }
+      infer.withContext(srv.cx, timeBudget ? function() { infer.withTimeout(timeBudget[0], run); } : run);
     });
   }
 
   function analyzeFile(srv, file) {
     infer.withContext(srv.cx, function() {
       file.scope = srv.cx.topScope;
       srv.signal("beforeLoad", file);
       infer.markVariablesDefinedBy(file.scope, file.name);
       infer.analyze(file.ast, file.name, file.scope, srv.passes);
       infer.purgeMarkedVariables(file.scope);
       srv.signal("afterLoad", file);
     });
     return file;
   }
 
-  function ensureFile(srv, name, text) {
-    var known = findFile(srv.files, name);
+  function ensureFile(srv, name, parent, text) {
+    var known = srv.findFile(name);
     if (known) {
-      if (text) clearFile(srv, known, text);
+      if (text != null) clearFile(srv, known, text);
+      if (parentDepth(known.parent) > parentDepth(parent)) {
+        known.parent = parent;
+        if (known.excluded) known.excluded = null;
+      }
       return;
     }
 
-    var file = new File(name);
+    var file = new File(name, parent);
     srv.files.push(file);
-    if (text) {
+    srv.fileMap[name] = file;
+    if (text != null) {
       updateText(file, text, srv);
     } else if (srv.options.async) {
       srv.startAsyncAction();
       srv.options.getFile(name, function(err, text) {
         updateText(file, text || "", srv);
         srv.finishAsyncAction(err);
       });
     } else {
       updateText(file, srv.options.getFile(name) || "", srv);
     }
   }
 
-  function clearFile(srv, file, newText) {
+  function clearFile(srv, file, newText, purgeVars) {
     if (file.scope) {
       infer.withContext(srv.cx, function() {
         // FIXME try to batch purges into a single pass (each call needs
         // to traverse the whole graph)
         infer.purgeTypes(file.name);
-        infer.markVariablesDefinedBy(file.scope, file.name);
-        infer.purgeMarkedVariables(file.scope);
+        if (purgeVars) {
+          infer.markVariablesDefinedBy(file.scope, file.name);
+          infer.purgeMarkedVariables(file.scope);
+        }
       });
       file.scope = null;
     }
     if (newText != null) updateText(file, newText, srv);
   }
 
   function fetchAll(srv, c) {
     var done = true, returned = false;
@@ -266,47 +283,58 @@
         try {
           updateText(file, srv.options.getFile(file.name) || "", srv);
         } catch (e) { return c(e); }
       }
     }
     if (done) c();
   }
 
-  function waitOnFetch(srv, c) {
+  function waitOnFetch(srv, timeBudget, c) {
     var done = function() {
       srv.off("everythingFetched", done);
       clearTimeout(timeout);
-      analyzeAll(srv, c);
+      analyzeAll(srv, timeBudget, c);
     };
     srv.on("everythingFetched", done);
     var timeout = setTimeout(done, srv.options.fetchTimeout);
   }
 
-  function analyzeAll(srv, c) {
-    if (srv.pending) return waitOnFetch(srv, c);
+  function analyzeAll(srv, timeBudget, c) {
+    if (srv.pending) return waitOnFetch(srv, timeBudget, c);
 
     var e = srv.fetchError;
     if (e) { srv.fetchError = null; return c(e); }
 
     var done = true;
-    for (var i = 0; i < srv.files.length; ++i) {
-      var file = srv.files[i];
-      if (file.text == null) done = false;
-      else if (file.scope == null) analyzeFile(srv, file);
+    // The second inner loop might add new files. The outer loop keeps
+    // repeating both inner loops until all files have been looked at.
+    for (var i = 0; i < srv.files.length;) {
+      var toAnalyze = [];
+      for (; i < srv.files.length; ++i) {
+        var file = srv.files[i];
+        if (file.text == null) done = false;
+        else if (file.scope == null && !file.excluded) toAnalyze.push(file);
+      }
+      toAnalyze.sort(function(a, b) { return parentDepth(a.parent) - parentDepth(b.parent); });
+      for (var j = 0; j < toAnalyze.length; j++) {
+        var file = toAnalyze[j];
+        if (file.parent && !chargeOnBudget(srv, file)) {
+          file.excluded = true;
+        } else if (timeBudget) {
+          var startTime = +new Date;
+          infer.withTimeout(timeBudget[0], function() { analyzeFile(srv, file); });
+          timeBudget[0] -= +new Date - startTime;
+        } else {
+          analyzeFile(srv, file);
+        }
+      }
     }
     if (done) c();
-    else waitOnFetch(srv, c);
-  }
-
-  function findFile(arr, name) {
-    for (var i = 0; i < arr.length; ++i) {
-      var file = arr[i];
-      if (file.name == name && file.type != "part") return file;
-    }
+    else waitOnFetch(srv, timeBudget, c);
   }
 
   function firstLine(str) {
     var end = str.indexOf("\n");
     if (end < 0) return str;
     return str.slice(0, end);
   }
 
@@ -330,25 +358,25 @@
   function ternError(msg) {
     var err = new Error(msg);
     err.name = "TernError";
     return err;
   }
 
   function resolveFile(srv, localFiles, name) {
     var isRef = name.match(/^#(\d+)$/);
-    if (!isRef) return findFile(srv.files, name);
+    if (!isRef) return srv.findFile(name);
 
     var file = localFiles[isRef[1]];
     if (!file) throw ternError("Reference to unknown file " + name);
-    if (file.type == "full") return findFile(srv.files, file.name);
+    if (file.type == "full") return srv.findFile(file.name);
 
     // This is a partial file
 
-    var realFile = file.backing = findFile(srv.files, file.name);
+    var realFile = file.backing = srv.findFile(file.name);
     var offset = file.offset;
     if (file.offsetLines) offset = {line: file.offsetLines, ch: 0};
     file.offset = offset = resolvePos(realFile, file.offsetLines == null ? file.offset : {line: file.offsetLines, ch: 0}, true);
     var line = firstLine(file.text);
     var foundPos = findMatchingPosition(line, realFile.text, offset);
     var pos = foundPos == null ? Math.max(0, realFile.text.lastIndexOf("\n", offset)) : foundPos;
 
     infer.withContext(srv.cx, function() {
@@ -366,17 +394,17 @@
         text = white + text.slice(cut);
         var atFunction = true;
       }
 
       var scopeStart = infer.scopeAt(realFile.ast, pos, realFile.scope);
       var scopeEnd = infer.scopeAt(realFile.ast, pos + text.length, realFile.scope);
       var scope = file.scope = scopeDepth(scopeStart) < scopeDepth(scopeEnd) ? scopeEnd : scopeStart;
       infer.markVariablesDefinedBy(scopeStart, file.name, pos, pos + file.text.length);
-      file.ast = infer.parse(file.text, srv.passes, {directSourceFile: file});
+      file.ast = infer.parse(file.text, srv.passes, {directSourceFile: file, allowReturnOutsideFunction: true});
       infer.analyze(file.ast, file.name, scope, srv.passes);
       infer.purgeMarkedVariables(scopeStart);
 
       // This is a kludge to tie together the function types (if any)
       // outside and inside of the fragment, so that arguments and
       // return values have some information known about them.
       tieTogether: if (inObject || atFunction) {
         var newInner = infer.scopeAt(file.ast, line.length, scopeStart);
@@ -394,16 +422,55 @@
           fOld.self.propagate(fNew.self);
           fNew.retval.propagate(fOld.retval);
         }
       }
     });
     return file;
   }
 
+  // Budget management
+
+  function astSize(node) {
+    var size = 0;
+    walk.simple(node, {Expression: function() { ++size; }});
+    return size;
+  }
+
+  function parentDepth(srv, parent) {
+    var depth = 0;
+    while (parent) {
+      parent = srv.findFile(parent).parent;
+      ++depth;
+    }
+    return depth;
+  }
+
+  function budgetName(srv, file) {
+    for (;;) {
+      var parent = srv.findFile(file.parent);
+      if (!parent.parent) break;
+      file = parent;
+    }
+    return file.name;
+  }
+
+  function chargeOnBudget(srv, file) {
+    var bName = budgetName(srv, file);
+    var size = astSize(file.ast);
+    var known = srv.budgets[bName];
+    if (known == null)
+      known = srv.budgets[bName] = srv.options.dependencyBudget;
+    if (known < size) return false;
+    srv.budgets[bName] = known - size;
+    return true;
+  }
+
+  // Query helpers
+
   function isPosition(val) {
     return typeof val == "number" || typeof val == "object" &&
       typeof val.line == "number" && typeof val.ch == "number";
   }
 
   // Baseline query document validation
   function invalidDoc(doc) {
     if (doc.query) {
@@ -506,16 +573,20 @@
     else return aUp ? 1 : -1;
   }
 
   function isStringAround(node, start, end) {
     return node.type == "Literal" && typeof node.value == "string" &&
       node.start == start - 1 && node.end <= end + 1;
   }
 
+  var jsKeywords = ("break do instanceof typeof case else new var " +
+    "catch finally return void continue for switch while debugger " +
+    "function this with default if throw delete in try").split(" ");
+
   function findCompletions(srv, query, file) {
     if (query.end == null) throw ternError("missing .query.end field");
     var wordStart = resolvePos(file, query.end), wordEnd = wordStart, text = file.text;
     while (wordStart && acorn.isIdentifierChar(text.charCodeAt(wordStart - 1))) --wordStart;
     if (query.expandWordForward !== false)
       while (wordEnd < text.length && acorn.isIdentifierChar(text.charCodeAt(wordEnd))) ++wordEnd;
     var word = text.slice(wordStart, wordEnd), completions = [];
     if (query.caseInsensitive) word = word.toLowerCase();
@@ -547,36 +618,44 @@
           maybeSet(rec, "url", val.url || type && type.url);
         if (query.origins)
           maybeSet(rec, "origin", val.origin || type && type.origin);
       }
       if (query.depths) rec.depth = depth;
     }
 
     var memberExpr = infer.findExpressionAround(file.ast, null, wordStart, file.scope, "MemberExpression");
+    var hookname;
     if (memberExpr &&
         (memberExpr.node.computed ? isStringAround(memberExpr.node.property, wordStart, wordEnd)
                                   : memberExpr.node.object.end < wordStart)) {
       var prop = memberExpr.node.property;
       prop = prop.type == "Literal" ? prop.value.slice(1) : prop.name;
+      srv.cx.completingProperty = prop;
 
       memberExpr.node = memberExpr.node.object;
       var tp = infer.expressionType(memberExpr);
       if (tp) infer.forAllPropertiesOf(tp, gather);
 
       if (!completions.length && query.guess !== false && tp && tp.guessProperties) {
         tp.guessProperties(function(p, o, d) {if (p != prop && p != "✖") gather(p, o, d);});
       }
       if (!completions.length && word.length >= 2 && query.guess !== false)
         for (var prop in srv.cx.props) gather(prop, srv.cx.props[prop][0], 0);
+      hookname = "memberCompletion";
     } else {
       infer.forAllLocalsAt(file.ast, wordStart, file.scope, gather);
+      if (query.includeKeywords) jsKeywords.forEach(function(kw) { gather(kw, null, 0); });
+      hookname = "completion";
     }
+    if (srv.passes[hookname])
+      srv.passes[hookname].forEach(function(hook) {hook(file, wordStart, wordEnd, gather);});
 
     if (query.sort !== false) completions.sort(compareCompletions);
+    srv.cx.completingProperty = null;
 
     return {start: outputPos(query, file, wordStart),
             end: outputPos(query, file, wordEnd),
             completions: completions};
   }
 
   function findProperties(srv, query) {
     var prefix = query.prefix, found = [];
@@ -663,17 +742,17 @@
 
   var storeSpan = exports.storeSpan = function(srv, query, span, target) {
     target.origin = span.origin;
     if (span.span) {
       var m = /^(\d+)\[(\d+):(\d+)\]-(\d+)\[(\d+):(\d+)\]$/.exec(span.span);
       target.start = query.lineCharPositions ? {line: Number(m[2]), ch: Number(m[3])} : Number(m[1]);
       target.end = query.lineCharPositions ? {line: Number(m[5]), ch: Number(m[6])} : Number(m[4]);
     } else {
-      var file = findFile(srv.files, span.origin);
+      var file = srv.findFile(span.origin);
       target.start = outputPos(query, file, span.node.start);
       target.end = outputPos(query, file, span.node.end);
     }
   };
 
   function findDef(srv, query, file) {
     var expr = findExpr(file, query);
     infer.resetGuessing();
@@ -685,17 +764,17 @@
 
     if (type.types) for (var i = type.types.length - 1; i >= 0; --i) {
       var tp = type.types[i];
       storeTypeDocs(tp, result);
       if (!span) span = getSpan(tp);
     }
 
     if (span && span.node) { // refers to a loaded file
-      var spanFile = span.node.sourceFile || findFile(srv.files, span.origin);
+      var spanFile = span.node.sourceFile || srv.findFile(span.origin);
       var start = outputPos(query, spanFile, span.node.start), end = outputPos(query, spanFile, span.node.end);
       result.start = start; result.end = end;
       result.file = span.origin;
       var cxStart = Math.max(0, span.node.start - 50);
       result.contextOffset = span.node.start - cxStart;
       result.context = spanFile.text.slice(cxStart, cxStart + 50);
     } else if (span) { // external
       result.file = span.origin;
@@ -721,27 +800,27 @@
                             (asLineChar(file, exists.name.start).line + 1));
         }
         refs.push({file: file.name,
                    start: outputPos(query, file, node.start),
                    end: outputPos(query, file, node.end)});
       };
     }
 
-    if (scope.node) {
+    if (scope.originNode) {
       type = "local";
       if (checkShadowing) {
         for (var prev = scope.prev; prev; prev = prev.prev)
           if (checkShadowing in prev.props) break;
-        if (prev) infer.findRefs(scope.node, scope, checkShadowing, prev, function(node) {
+        if (prev) infer.findRefs(scope.originNode, scope, checkShadowing, prev, function(node) {
           throw ternError("Renaming `" + name + "` to `" + checkShadowing + "` would shadow the definition used at line " +
                           (asLineChar(file, node.start).line + 1));
         });
       }
-      infer.findRefs(scope.node, scope, name, scope, storeRef(file));
+      infer.findRefs(scope.originNode, scope, name, scope, storeRef(file));
     } else {
       type = "global";
       for (var i = 0; i < srv.files.length; ++i) {
         var cur = srv.files[i];
         infer.findRefs(cur.ast, cur.scope, name, scope, storeRef(cur));
       }
     }
 
@@ -805,10 +884,10 @@
 
     return data;
   }
 
   function listFiles(srv) {
     return {files: srv.files.map(function(f){return f.name;})};
   }
 
-  exports.version = "0.5.1";
+  exports.version = "0.6.2";
 });
--- a/toolkit/modules/Promise-backend.js
+++ b/toolkit/modules/Promise-backend.js
@@ -248,34 +248,37 @@ let PendingErrors = {
   removeAllObservers: function() {
     this._observers.clear();
   }
 };
 PendingErrors.init();
 
 // Default mechanism for displaying errors
 PendingErrors.addObserver(function(details) {
+  const generalDescription = "A promise chain failed to handle a rejection." +
+    " Did you forget to '.catch', or did you forget to 'return'?\nSee" +
+    " https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n\n";
+
   let error = Cc['@mozilla.org/scripterror;1'].createInstance(Ci.nsIScriptError);
   if (!error || !Services.console) {
     // Too late during shutdown to use the nsIConsole
     dump("*************************\n");
-    dump("A promise chain failed to handle a rejection\n\n");
+    dump(generalDescription);
     dump("On: " + details.date + "\n");
     dump("Full message: " + details.message + "\n");
-    dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
     dump("Full stack: " + (details.stack||"not available") + "\n");
     dump("*************************\n");
     return;
   }
   let message = details.message;
   if (details.stack) {
     message += "\nFull Stack: " + details.stack;
   }
   error.init(
-             /*message*/"A promise chain failed to handle a rejection.\n\n" +
+             /*message*/ generalDescription +
              "Date: " + details.date + "\nFull Message: " + details.message,
              /*sourceName*/ details.fileName,
              /*sourceLine*/ details.lineNumber?("" + details.lineNumber):0,
              /*lineNumber*/ details.lineNumber || 0,
              /*columnNumber*/ 0,
              /*flags*/ Ci.nsIScriptError.errorFlag,
              /*category*/ "chrome javascript");
   Services.console.logMessage(error);
@@ -877,19 +880,19 @@ Handler.prototype = {
         // display it using dump().  Note that we do not use Cu.reportError as
         // we assume that this is a programming error, so we do not want end
         // users to see it. Also, if the programmer handles errors correctly,
         // they will either treat the error or log them somewhere.
 
         dump("*************************\n");
         dump("A coding exception was thrown in a Promise " +
              ((nextStatus == STATUS_RESOLVED) ? "resolution":"rejection") +
-             " callback.\n\n");
+             " callback.\n");
+        dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n\n");
         dump("Full message: " + ex + "\n");
-        dump("See https://developer.mozilla.org/Mozilla/JavaScript_code_modules/Promise.jsm/Promise\n");
         dump("Full stack: " + (("stack" in ex)?ex.stack:"not available") + "\n");
         dump("*************************\n");
 
       }
 
       // Additionally, reject the next promise.
       nextStatus = STATUS_REJECTED;
       nextValue = ex;