Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Sun, 19 Jan 2014 15:41:22 -0500
changeset 180268 10030fadbaaf0e147cec3f50a365711d3055d655
parent 180267 f26cb95f5a56ec86d9e90ffd3b7e1dc304e44d92 (current diff)
parent 180243 87bf998d0f3a3a0313983afc568d597fadd926d6 (diff)
child 180269 1ceb8969503e9c787a99eaeff6e11f6b697e23e9
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound.
configure.in
mobile/android/base/db/PerProfileContentProvider.java
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Updates to NSS seem to require a clobber due bug 959928.
+Bug 917896 requires a clobber due to bug 961339.
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fce1a137746dbd354bca1918f02f96d51c40bad2">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.1" path="prebuilts/clang/darwin-x86/3.1" revision="8a10d50e8caab8c18224588f0531f1c9363965b5"/>
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.2" path="prebuilts/clang/darwin-x86/3.2" revision="2d96fcbab6efee560c2004725b21bdc06d090933"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="eda08beb3ba9a159843c70ffde0f9660ec351eb9"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="87aa8679560ce09f6445621d6f370d9de722cdba"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "f1421b9d57e81c3823a32eb02e6ab6e3c74b12f1", 
+    "revision": "34bb6cda673afe1b610c2be5611889c38d180b53", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
@@ -90,17 +90,17 @@
   <project name="platform/system/netd" path="system/netd" revision="2e226e6e636ca0a8cc4c51093e46f4baba1ffcce"/>
   <project name="platform/system/vold" path="system/vold" revision="8ac5eef8ea3a456b96d52ce2091bf6d814782d8c"/>
   <!-- hamachi specific things -->
   <project name="quic/lf/b2g/build" path="device/qcom/b2g_common" revision="2e484664c353fd739200b908dce85500be870d43"/>
   <project name="quic/lf/b2g/external/jsmin" path="external/jsmin" revision="cec896f0affaa0226c02605ad28d42df1bc0e393"/>
   <project name="device/qcom/common" path="device/qcom/common" revision="d13aaf080177b7c48f243d51827db5c7a7873cd0"/>
   <project name="platform/vendor/qcom/msm7627a" path="device/qcom/msm7627a" revision="f06bcacc6f13cec895dc5d4c2385c076396194ec"/>
   <project name="android-device-hamachi" path="device/qcom/hamachi" remote="b2g" revision="9071ac8f0830979fe4a96ce47c7443d8adf0929d"/>
-  <project name="kernel/msm" path="kernel" revision="1f91c1843268af615bf00d7945948653829ac88a"/>
+  <project name="kernel/msm" path="kernel" revision="8072055e7094023e2cac8eea425bb785fe1d4066"/>
   <project name="platform/hardware/qcom/camera" path="hardware/qcom/camera" revision="19933e5d182a4799c6217b19a18562193a419298"/>
   <project name="platform/hardware/qcom/display" path="hardware/qcom/display" revision="5a58382180c70d0c446badc9c9837918ab69ec60"/>
   <project name="platform/hardware/qcom/media" path="hardware/qcom/media" revision="20d83ab382a1f813702421e76c2f9f994585990e"/>
   <project name="platform/hardware/qcom/gps" path="hardware/qcom/gps" revision="1698e6e9ed7cf1d543508845fa05ed86c7e5e241"/>
   <project name="platform/hardware/msm7k" path="hardware/msm7k" revision="693e65da9905d88c23653b45800e6509143f6a78"/>
   <project name="platform/vendor/qcom-opensource/omx/mm-core" path="vendor/qcom/opensource/omx/mm-core" revision="0365db6af2d4df11184a421f97c5043db47a0c0d"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="ec40c0aee736052fc4fe01c1b8dc16929da5dc45"/>
 </manifest>
\ No newline at end of file
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -5,17 +5,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="fce1a137746dbd354bca1918f02f96d51c40bad2">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.1" path="prebuilts/clang/darwin-x86/3.1" revision="8a10d50e8caab8c18224588f0531f1c9363965b5"/>
   <project groups="darwin" name="platform/prebuilts/clang/darwin-x86/3.2" path="prebuilts/clang/darwin-x86/3.2" revision="2d96fcbab6efee560c2004725b21bdc06d090933"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="73a7e0c15969a058964e92fad1925efead38dcfc"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cf19095a08e1279667b3af0df920c0b0107c8cad"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9b6626eddbc85873eaa2a9174a9bd5101e5c05f"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="96d2d00165f4561fbde62d1062706eab74b3a01f"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="221bcaecbbbc9d185f691471b64aed9e75b0c11d"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser-customization.js
+++ b/browser/base/content/browser-customization.js
@@ -16,18 +16,17 @@ let CustomizationHandler = {
         break;
       case "customizationending":
         this._customizationEnding(aEvent.detail);
         break;
     }
   },
 
   isCustomizing: function() {
-    return document.documentElement.hasAttribute("customizing") ||
-           document.documentElement.hasAttribute("customize-exiting");
+    return document.documentElement.hasAttribute("customizing");
   },
 
   _customizationStarting: function() {
     // Disable the toolbar context menu items
     let menubar = document.getElementById("main-menubar");
     for (let childNode of menubar.childNodes)
       childNode.setAttribute("disabled", true);
 
--- a/browser/base/content/browser-fullZoom.js
+++ b/browser/base/content/browser-fullZoom.js
@@ -18,21 +18,20 @@ var FullZoom = {
 
   // browser.zoom.updateBackgroundTabs preference cache
   updateBackgroundTabs: undefined,
 
   // One of the possible values for the mousewheel.* preferences.
   // From nsEventStateManager.h.
   ACTION_ZOOM: 3,
 
-  // This maps browser outer window IDs to monotonically increasing integer
-  // tokens.  _browserTokenMap[outerID] is increased each time the zoom is
-  // changed in the browser whose outer window ID is outerID.  See
-  // _getBrowserToken and _ignorePendingZoomAccesses.
-  _browserTokenMap: new Map(),
+  // 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(),
 
   get siteSpecific() {
     return this._siteSpecificPref;
   },
 
   //**************************************************************************//
   // nsISupports
 
@@ -41,48 +40,37 @@ var FullZoom = {
                                          Ci.nsIContentPrefObserver,
                                          Ci.nsISupportsWeakReference,
                                          Ci.nsISupports]),
 
   //**************************************************************************//
   // Initialization & Destruction
 
   init: function FullZoom_init() {
-    // Bug 691614 - zooming support for electrolysis
-    if (gMultiProcessBrowser)
-      return;
-
     // Listen for scrollwheel events so we can save scrollwheel-based changes.
     window.addEventListener("DOMMouseScroll", this, false);
 
     // Register ourselves with the service so we know when our pref changes.
     this._cps2 = Cc["@mozilla.org/content-pref/service;1"].
                  getService(Ci.nsIContentPrefService2);
     this._cps2.addObserverForName(this.name, this);
 
     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);
-
-    Services.obs.addObserver(this, "outer-window-destroyed", false);
   },
 
   destroy: function FullZoom_destroy() {
-    // Bug 691614 - zooming support for electrolysis
-    if (gMultiProcessBrowser)
-      return;
-
     gPrefService.removeObserver("browser.zoom.", this);
     this._cps2.removeObserverForName(this.name, this);
     window.removeEventListener("DOMMouseScroll", this, false);
-    Services.obs.removeObserver(this, "outer-window-destroyed");
   },
 
 
   //**************************************************************************//
   // Event Handlers
 
   // nsIDOMEventListener
 
@@ -151,20 +139,16 @@ var FullZoom = {
               gPrefService.getBoolPref("browser.zoom.siteSpecific");
             break;
           case "browser.zoom.updateBackgroundTabs":
             this.updateBackgroundTabs =
               gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
             break;
         }
         break;
-      case "outer-window-destroyed":
-        let outerID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
-        this._browserTokenMap.delete(outerID);
-        break;
     }
   },
 
   // nsIContentPrefObserver
 
   onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) {
     this._onContentPrefChanged(aGroup, aValue);
   },
@@ -203,17 +187,17 @@ var FullZoom = {
 
     this._globalValue = aValue === undefined ? aValue :
                           this._ensureValid(aValue);
 
     // If the current page doesn't have a site-specific preference, then its
     // zoom should be set to the new global preference now that the global
     // preference has changed.
     let hasPref = false;
-    let ctxt = this._loadContextFromWindow(browser.contentWindow);
+    let ctxt = this._loadContextFromBrowser(browser);
     let token = this._getBrowserToken(browser);
     this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
       handleResult: function () hasPref = true,
       handleCompletion: function () {
         if (!hasPref && token.isCurrent)
           this._applyPrefToZoom(undefined, browser);
       }.bind(this)
     });
@@ -228,20 +212,16 @@ 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) {
-    // Bug 691614 - zooming support for electrolysis
-    if (gMultiProcessBrowser)
-      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();
@@ -251,25 +231,25 @@ var FullZoom = {
     // Avoid the cps roundtrip and apply the default/global pref.
     if (aURI.spec == "about:blank") {
       this._applyPrefToZoom(undefined, browser,
                             this._notifyOnLocationChange.bind(this));
       return;
     }
 
     // Media documents should always start at 1, and are not affected by prefs.
-    if (!aIsTabSwitch && browser.contentDocument.mozSyntheticDocument) {
+    if (!aIsTabSwitch && browser.isSyntheticDocument) {
       ZoomManager.setZoomForBrowser(browser, 1);
       // _ignorePendingZoomAccesses already called above, so no need here.
       this._notifyOnLocationChange();
       return;
     }
 
     // See if the zoom pref is cached.
-    let ctxt = this._loadContextFromWindow(browser.contentWindow);
+    let ctxt = this._loadContextFromBrowser(browser);
     let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
     if (pref) {
       this._applyPrefToZoom(pref.value, browser,
                             this._notifyOnLocationChange.bind(this));
       return;
     }
 
     // It's not cached, so we have to asynchronously fetch it.
@@ -321,17 +301,17 @@ var FullZoom = {
 
   /**
    * Sets the zoom level of the page in the current browser to the global zoom
    * level.
    */
   reset: function FullZoom_reset() {
     let browser = gBrowser.selectedBrowser;
     let token = this._getBrowserToken(browser);
-    this._getGlobalValue(browser.contentWindow, function (value) {
+    this._getGlobalValue(browser, function (value) {
       if (token.isCurrent) {
         ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
         this._ignorePendingZoomAccesses(browser);
       }
     });
     this._removePref(browser);
   },
 
@@ -359,34 +339,33 @@ var FullZoom = {
    * @param aCallback  If given, it's asynchronously called when complete.
    */
   _applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) {
     if (!this.siteSpecific || gInPrintPreviewMode) {
       this._executeSoon(aCallback);
       return;
     }
 
-    // aBrowser.contentDocument is sometimes gone because this method is called
+    // The browser is sometimes half-destroyed because this method is called
     // by content pref service callbacks, which themselves can be called at any
     // time, even after browsers are closed.
-    if (!aBrowser.contentDocument ||
-        aBrowser.contentDocument.mozSyntheticDocument) {
+    if (!aBrowser.parentNode || aBrowser.isSyntheticDocument) {
       this._executeSoon(aCallback);
       return;
     }
 
     if (aValue !== undefined) {
       ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
       this._ignorePendingZoomAccesses(aBrowser);
       this._executeSoon(aCallback);
       return;
     }
 
     let token = this._getBrowserToken(aBrowser);
-    this._getGlobalValue(aBrowser.contentWindow, function (value) {
+    this._getGlobalValue(aBrowser, function (value) {
       if (token.isCurrent) {
         ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
         this._ignorePendingZoomAccesses(aBrowser);
       }
       this._executeSoon(aCallback);
     });
   },
 
@@ -395,38 +374,38 @@ var FullZoom = {
    * prefs store.
    *
    * @param browser  The zoom of this browser will be saved.  Required.
    */
   _applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
     Services.obs.notifyObservers(null, "browser-fullZoom:zoomChange", "");
     if (!this.siteSpecific ||
         gInPrintPreviewMode ||
-        browser.contentDocument.mozSyntheticDocument)
+        browser.isSyntheticDocument)
       return;
 
     this._cps2.set(browser.currentURI.spec, this.name,
                    ZoomManager.getZoomForBrowser(browser),
-                   this._loadContextFromWindow(browser.contentWindow), {
+                   this._loadContextFromBrowser(browser), {
       handleCompletion: function () {
         this._isNextContentPrefChangeInternal = true;
       }.bind(this),
     });
   },
 
   /**
    * Removes from the content prefs store the zoom level of the given browser.
    *
    * @param browser  The zoom of this browser will be removed.  Required.
    */
   _removePref: function FullZoom__removePref(browser) {
     Services.obs.notifyObservers(null, "browser-fullZoom:zoomReset", "");
-    if (browser.contentDocument.mozSyntheticDocument)
+    if (browser.isSyntheticDocument)
       return;
-    let ctxt = this._loadContextFromWindow(browser.contentWindow);
+    let ctxt = this._loadContextFromBrowser(browser);
     this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
       handleCompletion: function () {
         this._isNextContentPrefChangeInternal = true;
       }.bind(this),
     });
   },
 
   //**************************************************************************//
@@ -440,52 +419,42 @@ var FullZoom = {
    * after the async operation started, either the browser's zoom was changed or
    * the browser was destroyed, and depending on what the operation is doing, it
    * may no longer be safe to set and get its zoom.
    *
    * @param browser  The token of this browser will be returned.
    * @return  An object with an "isCurrent" getter.
    */
   _getBrowserToken: function FullZoom__getBrowserToken(browser) {
-    let outerID = this._browserOuterID(browser);
     let map = this._browserTokenMap;
-    if (!map.has(outerID))
-      map.set(outerID, 0);
+    if (!map.has(browser))
+      map.set(browser, 0);
     return {
-      token: map.get(outerID),
+      token: map.get(browser),
       get isCurrent() {
         // At this point, the browser may have been destructed and unbound but
         // its outer ID not removed from the map because outer-window-destroyed
         // hasn't been received yet.  In that case, the browser is unusable, it
         // has no properties, so return false.  Check for this case by getting a
         // property, say, docShell.
-        return map.get(outerID) === this.token && browser.docShell;
+        return map.get(browser) === this.token && browser.parentNode;
       },
     };
   },
 
   /**
    * Increments the zoom change token for the given browser so that pending
    * async operations know that it may be unsafe to access they zoom when they
    * finish.
    *
    * @param browser  Pending accesses in this browser will be ignored.
    */
   _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
-    let outerID = this._browserOuterID(browser);
     let map = this._browserTokenMap;
-    map.set(outerID, (map.get(outerID) || 0) + 1);
-  },
-
-  _browserOuterID: function FullZoom__browserOuterID(browser) {
-    return browser.
-           contentWindow.
-           QueryInterface(Ci.nsIInterfaceRequestor).
-           getInterface(Ci.nsIDOMWindowUtils).
-           outerWindowID;
+    map.set(browser, (map.get(browser) || 0) + 1);
   },
 
   _ensureValid: function FullZoom__ensureValid(aValue) {
     // Note that undefined is a valid value for aValue that indicates a known-
     // not-to-exist value.
     if (isNaN(aValue))
       return 1;
 
@@ -502,50 +471,47 @@ var FullZoom = {
    * Gets the global browser.content.full-zoom content preference.
    *
    * WARNING: callback may be called synchronously or asynchronously.  The
    * reason is that it's usually desirable to avoid turns of the event loop
    * where possible, since they can lead to visible, jarring jumps in zoom
    * level.  It's not always possible to avoid them, though.  As a convenience,
    * then, this method takes a callback and returns nothing.
    *
-   * @param window    The content window pertaining to the zoom.
+   * @param browser   The browser pertaining to the zoom.
    * @param callback  Synchronously or asynchronously called when done.  It's
    *                  bound to this object (FullZoom) and called as:
    *                    callback(prefValue)
    */
-  _getGlobalValue: function FullZoom__getGlobalValue(window, callback) {
+  _getGlobalValue: function FullZoom__getGlobalValue(browser, callback) {
     // * !("_globalValue" in this) => global value not yet cached.
     // * this._globalValue === undefined => global value known not to exist.
     // * Otherwise, this._globalValue is a number, the global value.
     if ("_globalValue" in this) {
       callback.call(this, this._globalValue, true);
       return;
     }
     let value = undefined;
-    this._cps2.getGlobal(this.name, this._loadContextFromWindow(window), {
+    this._cps2.getGlobal(this.name, this._loadContextFromBrowser(browser), {
       handleResult: function (pref) value = pref.value,
       handleCompletion: function (reason) {
         this._globalValue = this._ensureValid(value);
         callback.call(this, this._globalValue);
       }.bind(this)
     });
   },
 
   /**
-   * Gets the load context from the given window.
+   * Gets the load context from the given Browser.
    *
-   * @param window  The window whose load context will be returned.
-   * @return        The nsILoadContext of the given window.
+   * @param Browser  The Browser whose load context will be returned.
+   * @return        The nsILoadContext of the given Browser.
    */
-  _loadContextFromWindow: function FullZoom__loadContextFromWindow(window) {
-    return window.
-           QueryInterface(Ci.nsIInterfaceRequestor).
-           getInterface(Ci.nsIWebNavigation).
-           QueryInterface(Ci.nsILoadContext);
+  _loadContextFromBrowser: function FullZoom__loadContextFromBrowser(browser) {
+    return browser.loadContext;
   },
 
   /**
    * Asynchronously broadcasts "FullZoom:TESTS:location-change" so that tests
    * can select tabs, load pages, etc. and be notified when the zoom levels on
    * those pages change.  The notification is always asynchronous so that
    * observers are guaranteed a consistent behavior.
    */
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3705,21 +3705,21 @@ var XULBrowserWindow = {
           }
         }
       } else
         disableFindCommands(false);
 
       // Try not to instantiate gCustomizeMode as much as possible,
       // so don't use CustomizeMode.jsm to check for URI or customizing.
       let customizingURI = "about:customizing";
-      if (location == customizingURI &&
-          !CustomizationHandler.isCustomizing()) {
+      if (location == customizingURI) {
         gCustomizeMode.enter();
       } else if (location != customizingURI &&
-                 CustomizationHandler.isCustomizing()) {
+                 (CustomizationHandler.isEnteringCustomizeMode ||
+                  CustomizationHandler.isCustomizing())) {
         gCustomizeMode.exit();
       }
     }
     UpdateBackForwardCommands(gBrowser.webNavigation);
 
     gGestureSupport.restoreRotationState();
 
     // See bug 358202, when tabs are switched during a drag operation,
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2824,16 +2824,28 @@
       <property name="contentPrincipal"
                 onget="return this.mCurrentBrowser.contentPrincipal;"
                 readonly="true"/>
 
       <property name="securityUI"
                 onget="return this.mCurrentBrowser.securityUI;"
                 readonly="true"/>
 
+      <property name="fullZoom"
+                onget="return this.mCurrentBrowser.fullZoom;"
+                onset="this.mCurrentBrowser.fullZoom = val;"/>
+
+      <property name="textZoom"
+                onget="return this.mCurrentBrowser.textZoom;"
+                onset="this.mCurrentBrowser.textZoom = val;"/>
+
+      <property name="isSyntheticDocument"
+                onget="return this.mCurrentBrowser.isSyntheticDocument;"
+                readonly="true"/>
+
       <method name="_handleKeyEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (!aEvent.isTrusted) {
             // Don't let untrusted events mess with tabs.
             return;
           }
 
@@ -2943,17 +2955,17 @@
         <parameter name="aMessage"/>
         <body><![CDATA[
           let json = aMessage.json;
           let browser = aMessage.target;
 
           switch (aMessage.name) {
             case "DOMTitleChanged": {
               let tab = this._getTabForBrowser(browser);
-              if (!tab)
+              if (!tab || tab.hasAttribute("pending"))
                 return;
               let titleChanged = this.setTabTitle(tab);
               if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
                 tab.setAttribute("titlechanged", "true");
               break;
             }
             case "contextmenu": {
               gContextMenuContentData = { event: aMessage.objects.event,
@@ -2965,18 +2977,17 @@
                               true, false, null);
               break;
             }
             case "DOMWebNotificationClicked": {
               let tab = this._getTabForBrowser(browser);
               if (!tab)
                 return;
               this.selectedTab = tab;
-              let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-              focusManager.activeWindow = window;
+              window.focus();
               break;
             }
           }
         ]]></body>
       </method>
 
       <constructor>
         <![CDATA[
@@ -3186,16 +3197,19 @@
           if (!event.isTrusted)
             return;
 
           var contentWin = event.target.defaultView;
           if (contentWin != contentWin.top)
             return;
 
           var tab = this._getTabForContentWindow(contentWin);
+          if (tab.hasAttribute("pending"))
+            return;
+
           var titleChanged = this.setTabTitle(tab);
           if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
             tab.setAttribute("titlechanged", "true");
         ]]>
       </handler>
       <handler event="oop-browser-crashed">
         <![CDATA[
           if (!event.isTrusted)
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -3,16 +3,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <hbox id="customization-container" flex="1" hidden="true">
   <vbox flex="1" id="customization-palette-container">
     <label id="customization-header">
       &customizeMode.menuAndToolbars.header;
     </label>
     <vbox id="customization-palette" flex="1"/>
+    <spacer flex="1"/>
     <hbox>
       <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
         <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
       </button>
       <spacer flex="1"/>
       <button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
     </hbox>
   </vbox>
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -360,19 +360,19 @@ const CustomizableWidgets = [{
 
       // The middle node is the 'Reset Zoom' button.
       let zoomResetButton = node.childNodes[2];
       let window = aDocument.defaultView;
       function updateZoomResetButton() {
         //XXXgijs in some tests we get called very early, and there's no docShell on the
         // tabbrowser. This breaks the zoom toolkit code (see bug 897410). Don't let that happen:
         let zoomFactor = 100;
-        if (window.gBrowser.docShell) {
+        try {
           zoomFactor = Math.floor(window.ZoomManager.zoom * 100);
-        }
+        } catch (e) {}
         zoomResetButton.setAttribute("label", CustomizableUI.getLocalizedProperty(
           buttons[1], "label", [zoomFactor]
         ));
       };
 
       // Register ourselves with the service so we know when the zoom prefs change.
       Services.obs.addObserver(updateZoomResetButton, "browser-fullZoom:zoomChange", false);
       Services.obs.addObserver(updateZoomResetButton, "browser-fullZoom:zoomReset", false);
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -69,42 +69,59 @@ CustomizeMode.prototype = {
   _customizing: false,
   _skipSourceNodeCheck: null,
   _mainViewContext: null,
 
   get panelUIContents() {
     return this.document.getElementById("PanelUI-contents");
   },
 
+  get _handler() {
+    return this.window.CustomizationHandler;
+  },
+
   toggle: function() {
-    if (this._transitioning) {
+    if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
+      this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
       return;
     }
     if (this._customizing) {
       this.exit();
     } else {
       this.enter();
     }
   },
 
   enter: function() {
-    if (this._customizing || this._transitioning) {
+    this._wantToBeInCustomizeMode = true;
+
+    if (this._customizing || this._handler.isEnteringCustomizeMode) {
       return;
     }
 
+    // Exiting; want to re-enter once we've done that.
+    if (this._handler.isExitingCustomizeMode) {
+      LOG("Attempted to enter while we're in the middle of exiting. " +
+          "We'll exit after we've entered");
+      return;
+    }
+
+
     // We don't need to switch to kAboutURI, or open a new tab at
     // kAboutURI if we're already on it.
     if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) {
       this.window.switchToTabHavingURI(kAboutURI, true);
       return;
     }
 
     let window = this.window;
     let document = this.document;
 
+    this._handler.isEnteringCustomizeMode = true;
+
     Task.spawn(function() {
       // We shouldn't start customize mode until after browser-delayed-startup has finished:
       if (!this.window.gBrowserInit.delayedStartupFinished) {
         let delayedStartupDeferred = Promise.defer();
         let delayedStartupObserver = function(aSubject) {
           if (aSubject == this.window) {
             Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
             delayedStartupDeferred.resolve();
@@ -207,29 +224,45 @@ CustomizeMode.prototype = {
       CustomizableUI.addListener(this);
       window.PanelUI.endBatchUpdate();
       this._customizing = true;
       this._transitioning = false;
 
       // Show the palette now that the transition has finished.
       this.visiblePalette.hidden = false;
 
+      this._handler.isEnteringCustomizeMode = false;
       this.dispatchToolboxEvent("customizationready");
+      if (!this._wantToBeInCustomizeMode) {
+        this.exit();
+      }
     }.bind(this)).then(null, function(e) {
       ERROR(e);
       // We should ensure this has been called, and calling it again doesn't hurt:
       window.PanelUI.endBatchUpdate();
-    });
+      this._handler.isEnteringCustomizeMode = false;
+    }.bind(this));
   },
 
   exit: function() {
-    if (!this._customizing || this._transitioning) {
+    this._wantToBeInCustomizeMode = false;
+
+    if (!this._customizing || this._handler.isExitingCustomizeMode) {
       return;
     }
 
+    // Entering; want to exit once we've done that.
+    if (this._handler.isEnteringCustomizeMode) {
+      LOG("Attempted to exit while we're in the middle of entering. " +
+          "We'll exit after we've entered");
+      return;
+    }
+
+    this._handler.isExitingCustomizeMode = true;
+
     CustomizableUI.removeListener(this);
 
     this.document.removeEventListener("keypress", this);
     this.window.PanelUI.menuButton.removeEventListener("mousedown", this);
     this.window.PanelUI.menuButton.open = false;
 
     this.window.PanelUI.beginBatchUpdate();
 
@@ -302,17 +335,23 @@ CustomizeMode.prototype = {
       if (this._mainViewContext) {
         mainView.setAttribute("context", this._mainViewContext);
       }
 
       if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) {
         let custBrowser = this.browser.selectedBrowser;
         if (custBrowser.canGoBack) {
           // If there's history to this tab, just go back.
-          custBrowser.goBack();
+          // Note that this throws an exception if the previous document has a
+          // problematic URL (e.g. about:idontexist)
+          try {
+            custBrowser.goBack();
+          } catch (ex) {
+            ERROR(ex);
+          }
         } else {
           // If we can't go back, we're removing the about:customization tab.
           // We only do this if we're the top window for this window (so not
           // a dialog window, for example).
           if (window.getTopWin(true) == window) {
             let customizationTab = this.browser.selectedTab;
             if (this.browser.browsers.length == 1) {
               window.BrowserOpenTab();
@@ -327,23 +366,29 @@ CustomizeMode.prototype = {
 
       let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
       for (let toolbar of customizableToolbars)
         toolbar.removeAttribute("customizing");
 
       this.window.PanelUI.endBatchUpdate();
       this._changed = false;
       this._transitioning = false;
+      this._handler.isExitingCustomizeMode = false;
       this.dispatchToolboxEvent("aftercustomization");
       CustomizableUI.notifyEndCustomizing(this.window);
+
+      if (this._wantToBeInCustomizeMode) {
+        this.enter();
+      }
     }.bind(this)).then(null, function(e) {
       ERROR(e);
       // We should ensure this has been called, and calling it again doesn't hurt:
       window.PanelUI.endBatchUpdate();
-    });
+      this._handler.isExitingCustomizeMode = false;
+    }.bind(this));
   },
 
   /**
    * The customize mode transition has 3 phases when entering:
    * 1) Pre-customization mode
    *    This is the starting phase of the browser.
    * 2) customize-entering
    *    This phase is a transition, optimized for smoothness.
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
 [browser_885052_customize_mode_observers_disabed.js]
 # Bug 951403 - Disabled on OSX for frequent failures
 skip-if = os == "mac"
 
 [browser_885530_showInPrivateBrowsing.js]
 [browser_886323_buildArea_removable_nodes.js]
 [browser_887438_currentset_shim.js]
 [browser_888817_currentset_updating.js]
+[browser_889120_customize_tab_merging.js]
 [browser_890140_orphaned_placeholders.js]
 [browser_890262_destroyWidget_after_add_to_panel.js]
 [browser_892955_isWidgetRemovable_for_removed_widgets.js]
 [browser_892956_destroyWidget_defaultPlacements.js]
 [browser_909779_overflow_toolbars_new_window.js]
 [browser_901207_searchbar_in_panel.js]
 [browser_913972_currentset_overflow.js]
 
--- a/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js
+++ b/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js
@@ -105,17 +105,16 @@ function checkPalette(id, method) {
   is(expectedChild.firstChild.id, id, "Widget " + id + " was moved using " + method + " and should now be wrapped in palette in customizing window.");
   if (id == kXULWidgetId) {
     ok(otherWin.gNavToolbox.palette.querySelector("#" + id), "Widget " + id + " should be in invisible palette in other window.");
   }
   checkWrapper(id);
 }
 
 let otherWin;
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
 
 // Moving widgets in two windows, one with customize mode and one without, should work.
 add_task(function MoveWidgetsInTwoWindows() {
   yield startCustomizing();
   otherWin = yield openAndLoadWindow(null, true);
   yield otherWin.PanelUI.ensureReady();
   ok(CustomizableUI.inDefaultState, "Should start in default state");
 
@@ -134,11 +133,10 @@ add_task(function MoveWidgetsInTwoWindow
   otherWin = null;
   if (otherWin) {
     otherWin.close();
   }
   yield endCustomizing();
 });
 
 add_task(function asyncCleanup() {
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
-});
\ No newline at end of file
+});
--- a/browser/components/customizableui/test/browser_878452_drag_to_panel.js
+++ b/browser/components/customizableui/test/browser_878452_drag_to_panel.js
@@ -1,16 +1,14 @@
 /* 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";
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
-
 // Dragging an item from the palette to another button in the panel should work.
 add_task(function() {
   yield startCustomizing();
   let btn = document.getElementById("feed-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   let lastButtonIndex = placements.length - 1;
@@ -59,11 +57,10 @@ add_task(function() {
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   simulateItemDrag(btn, palette);
   assertAreaPlacements(panel.id, []);
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
+++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
@@ -1,15 +1,14 @@
 /* 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";
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
 requestLongerTimeout(5);
 
 // Dragging the zoom controls to be before the print button should not move any controls.
 add_task(function() {
   yield startCustomizing();
   let zoomControls = document.getElementById("zoom-controls");
   let printButton = document.getElementById("print-button");
   let placementsAfterMove = ["edit-controls",
@@ -457,11 +456,10 @@ add_task(function() {
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(button, palette);
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_889120_customize_tab_merging.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const kTestToolbarId = "test-empty-drag";
+
+// Attempting to switch quickly from one tab to another to see whether the state changes
+// correctly.
+add_task(function CheckBasicCustomizeMode() {
+  yield startCustomizing();
+  ok(CustomizationHandler.isCustomizing(), "We should be in customize mode");
+  yield endCustomizing();
+  ok(!CustomizationHandler.isCustomizing(), "We should not be in customize mode");
+});
+add_task(function CheckQuickCustomizeModeSwitch() {
+  let tab1 = gBrowser.addTab("about:newtab");
+  gBrowser.selectedTab = tab1;
+  let tab2 = gBrowser.addTab("about:customizing");
+  let tab3 = gBrowser.addTab("about:newtab");
+  gBrowser.selectedTab = tab2;
+  try {
+    yield waitForCondition(() => CustomizationHandler.isEnteringCustomizeMode);
+  } catch (ex) {
+    Cu.reportError(ex);
+  }
+  ok(CustomizationHandler.isEnteringCustomizeMode, "Should be entering customize mode");
+  gBrowser.selectedTab = tab3;
+  try {
+    yield waitForCondition(() => !CustomizationHandler.isEnteringCustomizeMode && !CustomizationHandler.isCustomizing());
+  } catch (ex) {
+    Cu.reportError(ex);
+  }
+  ok(!CustomizationHandler.isCustomizing(), "Should not be entering customize mode");
+  gBrowser.removeTab(tab1);
+  gBrowser.removeTab(tab2);
+  gBrowser.removeTab(tab3);
+});
+
+add_task(function asyncCleanup() {
+  yield endCustomizing();
+});
+
--- a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
+++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
@@ -1,16 +1,14 @@
 /* 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";
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
-
 // One orphaned item should have two placeholders next to it.
 add_task(function() {
   yield startCustomizing();
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   if (isInWin8()) {
@@ -153,16 +151,15 @@ add_task(function() {
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering");
 
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
 
 function getVisiblePlaceholderCount(aPanel) {
   let visiblePlaceholders = aPanel.querySelectorAll(".panel-customization-placeholder:not([hidden=true])");
   return visiblePlaceholders.length;
 }
--- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
+++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js
@@ -47,18 +47,18 @@ add_task(function() {
 
 // Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.
 add_task(function() {
   this.originalWindowWidth = window.outerWidth;
   let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
   ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
   ok(CustomizableUI.inDefaultState, "Should start in default state.");
 
-  window.resizeTo(380, window.outerHeight);
-  yield waitForCondition(() => navbar.hasAttribute("overflowing"));
+  window.resizeTo(360, window.outerHeight);
+  yield waitForCondition(() => navbar.getAttribute("overflowing") == "true");
   ok(!navbar.querySelector("#search-container"), "Search container should be overflowing");
   let searchbar = document.getElementById("searchbar");
 
   let shownPanelPromise = promiseOverflowShown(window);
   sendWebSearchKeyCommand();
   yield shownPanelPromise;
 
   let chevron = document.getElementById("nav-bar-overflow-button");
--- a/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js
+++ b/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js
@@ -103,18 +103,18 @@ add_task(function() {
   window.resizeTo(originalWindowWidth, window.outerHeight);
 });
 
 // Overflow everything that can, then reorganize that list
 add_task(function() {
   ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
   ok(CustomizableUI.inDefaultState, "Should start in default state.");
 
-  window.resizeTo(380, window.outerHeight);
-  yield waitForCondition(() => navbar.hasAttribute("overflowing"));
+  window.resizeTo(360, window.outerHeight);
+  yield waitForCondition(() => navbar.getAttribute("overflowing") == "true");
   ok(!navbar.querySelector("#" + kSearchBox), "Search container should be overflowing");
   let placements = CustomizableUI.getWidgetIdsInArea(navbar.id);
   let searchboxPlacement = placements.indexOf(kSearchBox);
   CustomizableUI.moveWidgetWithinArea(kHomeBtn, searchboxPlacement);
   yield waitForCondition(() => navbar.querySelector("#" + kHomeBtn));
   ok(navbar.querySelector("#" + kHomeBtn), "Home button should have moved back");
   let inc = 15;
   window.resizeTo(640, window.outerHeight);
--- a/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js
+++ b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js
@@ -2,18 +2,16 @@
  * 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 navbar;
 let skippedItem;
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
-
 // Attempting to drag a skipintoolbarset item should work.
 add_task(function() {
   navbar = document.getElementById("nav-bar");
   skippedItem = document.createElement("toolbarbutton");
   skippedItem.id = "test-skipintoolbarset-item";
   skippedItem.setAttribute("label", "Test");
   skippedItem.setAttribute("skipintoolbarset", "true");
   navbar.customizationTarget.appendChild(skippedItem);
@@ -30,11 +28,10 @@ add_task(function() {
   is(downloadWrapper.nextSibling && downloadWrapper.nextSibling.id,
      skippedItem.parentNode.id, "Should be next to skipintoolbarset item");
   ok(CustomizableUI.inDefaultState, "Should still be in default state");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
   skippedItem.remove();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js
+++ b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js
@@ -1,16 +1,14 @@
 /* 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";
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
-
 // Customize mode reset button should revert correctly
 add_task(function() {
   yield startCustomizing();
   let devButton = document.getElementById("developer-button");
   let downloadsButton = document.getElementById("downloads-button");
   let searchBox = document.getElementById("search-container");
   let palette = document.getElementById("customization-palette");
   ok(devButton && downloadsButton && searchBox && palette, "Stuff should exist");
@@ -18,11 +16,10 @@ add_task(function() {
   simulateItemDrag(searchBox, palette);
   gCustomizeMode.reset();
   yield waitForCondition(function() !gCustomizeMode.resetting);
   ok(CustomizableUI.inDefaultState, "Should be back in default state");
   yield endCustomizing();
 });
 
 add_task(function asyncCleanup() {
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js
+++ b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js
@@ -1,16 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const kTestToolbarId = "test-empty-drag";
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
 
 // Attempting to drag an item to an empty container should work.
 add_task(function() {
   yield createToolbarWithPlacements(kTestToolbarId, []);
   yield startCustomizing();
   let downloadButton = document.getElementById("downloads-button");
   let customToolbar = document.getElementById(kTestToolbarId);
   simulateItemDrag(downloadButton, customToolbar);
@@ -18,11 +17,10 @@ add_task(function() {
   ok(downloadButton.parentNode && downloadButton.parentNode.parentNode == customToolbar,
      "Button should really be in toolbar");
   yield endCustomizing();
   removeCustomToolbars();
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_934113_menubar_removable.js
+++ b/browser/components/customizableui/test/browser_934113_menubar_removable.js
@@ -1,16 +1,14 @@
 /* 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";
 
-Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
-
 // Attempting to drag the menubar to the navbar shouldn't work.
 add_task(function() {
   yield startCustomizing();
   let menuItems = document.getElementById("menubar-items");
   let navbar = document.getElementById("nav-bar");
   let menubar = document.getElementById("toolbar-menubar");
   simulateItemDrag(menuItems, navbar.customizationTarget);
   is(getAreaWidgetIds("nav-bar").indexOf("menubar-items"), -1, "Menu bar shouldn't be in the navbar.");
@@ -18,11 +16,10 @@ add_task(function() {
   ok(menubar.querySelector("#menubar-items"), "Should find menubar items in the menubar.");
   isnot(getAreaWidgetIds("toolbar-menubar").indexOf("menubar-items"), -1,
         "Menubar items shouldn't be missing from the navbar.");
   yield endCustomizing();
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
-  Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck");
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -9,16 +9,19 @@ let tmp = {};
 Cu.import("resource://gre/modules/Promise.jsm", tmp);
 Cu.import("resource:///modules/CustomizableUI.jsm", tmp);
 let {Promise, CustomizableUI} = tmp;
 
 let ChromeUtils = {};
 let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader);
 scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils);
 
+Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true);
+registerCleanupFunction(() => Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck"));
+
 let {synthesizeDragStart, synthesizeDrop} = ChromeUtils;
 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 function createDummyXULButton(id, label) {
   let btn = document.createElementNS(kNSXUL, "toolbarbutton");
   btn.id = id;
   btn.setAttribute("label", label || id);
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -474,21 +474,32 @@ nsBrowserContentHandler.prototype = {
       }
     }
     if (cmdLine.handleFlag("preferences", false)) {
       openPreferences();
       cmdLine.preventDefault = true;
     }
     if (cmdLine.handleFlag("silent", false))
       cmdLine.preventDefault = true;
-    if (cmdLine.handleFlag("private-window", false)) {
-      openWindow(null, this.chromeURL, "_blank",
-        "chrome,dialog=no,private,all" + this.getFeatures(cmdLine),
-        "about:privatebrowsing");
-      cmdLine.preventDefault = true;
+
+    try {
+      var privateWindowParam = cmdLine.handleFlagWithParam("private-window", false);
+      if (privateWindowParam) {
+        var uri = resolveURIInternal(cmdLine, privateWindowParam);
+        handURIToExistingBrowser(uri, nsIBrowserDOMWindow.OPEN_NEWTAB, cmdLine, true);
+        cmdLine.preventDefault = true;
+      }
+    } catch (e if e.result == Components.results.NS_ERROR_INVALID_ARG) {
+      // NS_ERROR_INVALID_ARG is thrown when flag exists, but has no param.
+      if (cmdLine.handleFlag("private-window", false)) {
+        openWindow(null, this.chromeURL, "_blank",
+          "chrome,dialog=no,private,all" + this.getFeatures(cmdLine),
+          "about:privatebrowsing");
+        cmdLine.preventDefault = true;
+      }
     }
 
     var searchParam = cmdLine.handleFlagWithParam("search", false);
     if (searchParam) {
       doSearch(searchParam, cmdLine);
       cmdLine.preventDefault = true;
     }
 
@@ -680,29 +691,32 @@ nsBrowserContentHandler.prototype = {
       if (cmdLine.length != urlFlagIdx + 2 || /firefoxurl:/.test(urlParam))
         throw NS_ERROR_ABORT;
       cmdLine.handleFlag("osint", false)
     }
   },
 };
 var gBrowserContentHandler = new nsBrowserContentHandler();
 
-function handURIToExistingBrowser(uri, location, cmdLine)
+function handURIToExistingBrowser(uri, location, cmdLine, forcePrivate)
 {
   if (!shouldLoadURI(uri))
     return;
 
-  // Do not open external links in private windows, unless we're in perma-private mode
-  var allowPrivate = PrivateBrowsingUtils.permanentPrivateBrowsing;
+  // Unless using a private window is forced, open external links in private
+  // windows only if we're in perma-private mode.
+  var allowPrivate = forcePrivate || PrivateBrowsingUtils.permanentPrivateBrowsing;
   var navWin = RecentWindow.getMostRecentBrowserWindow({private: allowPrivate});
   if (!navWin) {
     // if we couldn't load it in an existing window, open a new one
-    openWindow(null, gBrowserContentHandler.chromeURL, "_blank",
-               "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine),
-               uri.spec);
+    var features = "chrome,dialog=no,all" + gBrowserContentHandler.getFeatures(cmdLine);
+    if (forcePrivate) {
+      features += ",private";
+    }
+    openWindow(null, gBrowserContentHandler.chromeURL, "_blank", features, uri.spec);
     return;
   }
 
   var navNav = navWin.QueryInterface(nsIInterfaceRequestor)
                      .getInterface(nsIWebNavigation);
   var rootItem = navNav.QueryInterface(nsIDocShellTreeItem).rootTreeItem;
   var rootWin = rootItem.QueryInterface(nsIInterfaceRequestor)
                         .getInterface(nsIDOMWindow);
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -337,21 +337,26 @@ let FormDataListener = {
  * Causes a SessionStore:update message to be sent that contains the currently
  * selected pageStyle for all reachable frames.
  *
  * Example:
  *   {pageStyle: "Dusk", children: [null, {pageStyle: "Mozilla"}]}
  */
 let PageStyleListener = {
   init: function () {
-    Services.obs.addObserver(this, "author-style-disabled-changed", true);
-    Services.obs.addObserver(this, "style-sheet-applicable-state-changed", true);
+    Services.obs.addObserver(this, "author-style-disabled-changed", false);
+    Services.obs.addObserver(this, "style-sheet-applicable-state-changed", false);
     gFrameTree.addObserver(this);
   },
 
+  uninit: function () {
+    Services.obs.removeObserver(this, "author-style-disabled-changed");
+    Services.obs.removeObserver(this, "style-sheet-applicable-state-changed");
+  },
+
   observe: function (subject, topic) {
     let frame = subject.defaultView;
 
     if (frame && gFrameTree.contains(frame)) {
       MessageQueue.push("pageStyle", () => this.collect());
     }
   },
 
@@ -362,18 +367,17 @@ let PageStyleListener = {
   onFrameTreeCollected: function () {
     MessageQueue.push("pageStyle", () => this.collect());
   },
 
   onFrameTreeReset: function () {
     MessageQueue.push("pageStyle", () => null);
   },
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference])
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
 };
 
 /**
  * Listens for changes to docShell capabilities. Whenever a new load is started
  * we need to re-check the list of capabilities and send message when it has
  * changed.
  *
  * Causes a SessionStore:update message to be sent that contains the currently
@@ -414,20 +418,24 @@ let DocShellCapabilitiesListener = {
  *
  * Causes a SessionStore:update message to be sent that contains the current
  * DOMSessionStorage contents. The data is a nested object using host names
  * as keys and per-host DOMSessionStorage data as values.
  */
 let SessionStorageListener = {
   init: function () {
     addEventListener("MozStorageChanged", this);
-    Services.obs.addObserver(this, "browser:purge-domain-data", true);
+    Services.obs.addObserver(this, "browser:purge-domain-data", false);
     gFrameTree.addObserver(this);
   },
 
+  uninit: function () {
+    Services.obs.removeObserver(this, "browser:purge-domain-data");
+  },
+
   handleEvent: function (event) {
     // Ignore events triggered by localStorage or globalStorage changes.
     if (gFrameTree.contains(event.target) && isSessionStorageEvent(event)) {
       this.collect();
     }
   },
 
   observe: function () {
@@ -445,18 +453,17 @@ let SessionStorageListener = {
   onFrameTreeCollected: function () {
     this.collect();
   },
 
   onFrameTreeReset: function () {
     this.collect();
   },
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference])
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
 };
 
 /**
  * Listen for changes to the privacy status of the tab.
  * By definition, tabs start in non-private mode.
  *
  * Causes a SessionStore:update message to be sent for
  * field "isPrivate". This message contains
@@ -651,8 +658,18 @@ MessageListener.init();
 FormDataListener.init();
 SyncHandler.init();
 ProgressListener.init();
 PageStyleListener.init();
 SessionStorageListener.init();
 ScrollPositionListener.init();
 DocShellCapabilitiesListener.init();
 PrivacyListener.init();
+
+addEventListener("unload", () => {
+  // Remove all registered nsIObservers.
+  PageStyleListener.uninit();
+  SessionStorageListener.uninit();
+
+  // We don't need to take care of any gFrameTree observers as the gFrameTree
+  // will die with the content script. The same goes for the privacy transition
+  // observer that will die with the docShell when the tab is closed.
+});
--- a/browser/components/sessionstore/src/ContentRestore.jsm
+++ b/browser/components/sessionstore/src/ContentRestore.jsm
@@ -137,17 +137,16 @@ ContentRestoreInternal.prototype = {
     let uri = activePageData.url || null;
     if (uri) {
       webNavigation.setCurrentURI(Utils.makeURI(uri));
     }
 
     SessionHistory.restore(this.docShell, tabData);
 
     // Add a listener to watch for reloads.
-    let webNavigation = this.docShell.QueryInterface(Ci.nsIWebNavigation);
     let listener = new HistoryListener(this.docShell, reloadCallback);
     webNavigation.sessionHistory.addSHistoryListener(listener);
     this._historyListener = listener;
 
     // Make sure to reset the capabilities and attributes in case this tab gets
     // reused.
     let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
     DocShellCapabilities.restore(this.docShell, disallow);
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -624,16 +624,40 @@ let SessionStoreInternal = {
       case "SessionStore:load":
         TabStateCache.delete(browser);
         this.onTabLoad(win, browser);
         break;
       case "SessionStore:restoreHistoryComplete":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           // Notify the tabbrowser that the tab chrome has been restored.
           let tab = this._getTabForBrowser(browser);
+          let tabData = browser.__SS_data;
+
+          // wall-paper fix for bug 439675: make sure that the URL to be loaded
+          // is always visible in the address bar
+          let activePageData = tabData.entries[tabData.index - 1] || null;
+          let uri = activePageData ? activePageData.url || null : null;
+          browser.userTypedValue = uri;
+
+          // If the page has a title, set it.
+          if (activePageData) {
+            if (activePageData.title) {
+              tab.label = activePageData.title;
+              tab.crop = "end";
+            } else if (activePageData.url != "about:blank") {
+              tab.label = activePageData.url;
+              tab.crop = "center";
+            }
+          }
+
+          // Restore the tab icon.
+          if ("image" in tabData) {
+            win.gBrowser.setIcon(tab, tabData.image);
+          }
+
           let event = win.document.createEvent("Events");
           event.initEvent("SSTabRestoring", true, false);
           tab.dispatchEvent(event);
         }
         break;
       case "SessionStore:restoreTabContentStarted":
         if (this.isCurrentEpoch(browser, aMessage.data.epoch)) {
           // If the user was typing into the URL bar when we crashed, but hadn't hit
@@ -2717,43 +2741,21 @@ let SessionStoreInternal = {
         formdata: tabData.formdata || null,
         disallow: tabData.disallow || null,
         pageStyle: tabData.pageStyle || null
       });
 
       browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
                                               {tabData: tabData, epoch: epoch});
 
-      // wall-paper fix for bug 439675: make sure that the URL to be loaded
-      // is always visible in the address bar
-      let activePageData = tabData.entries[activeIndex] || null;
-      let uri = activePageData ? activePageData.url || null : null;
-      browser.userTypedValue = uri;
-
-      // If the page has a title, set it.
-      if (activePageData) {
-        if (activePageData.title) {
-          tab.label = activePageData.title;
-          tab.crop = "end";
-        } else if (activePageData.url != "about:blank") {
-          tab.label = activePageData.url;
-          tab.crop = "center";
-        }
-      }
-
       // Restore tab attributes.
       if ("attributes" in tabData) {
         TabAttributes.set(tab, tabData.attributes);
       }
 
-      // Restore the tab icon.
-      if ("image" in tabData) {
-        tabbrowser.setIcon(tab, tabData.image);
-      }
-
       // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
       // it ensures each window will have its selected tab loaded.
       if (aRestoreImmediately || tabbrowser.selectedBrowser == browser) {
         this.restoreTabContent(tab);
       } else {
         TabRestoreQueue.add(tab);
         this.restoreNextTab();
       }
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -57,16 +57,17 @@ support-files =
 [browser_capabilities.js]
 [browser_dying_cache.js]
 [browser_form_restore_events.js]
 [browser_formdata.js]
 [browser_formdata_format.js]
 [browser_formdata_xpath.js]
 [browser_frametree.js]
 [browser_global_store.js]
+[browser_label_and_icon.js]
 [browser_merge_closed_tabs.js]
 [browser_pageshow.js]
 [browser_pageStyle.js]
 [browser_privatetabs.js]
 [browser_scrollPositions.js]
 [browser_sessionStorage.js]
 [browser_swapDocShells.js]
 [browser_tabStateCache.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_label_and_icon.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Make sure that tabs are restored on demand as otherwise the tab will start
+ * loading immediately and we can't check its icon and label.
+ */
+add_task(function setup() {
+  Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", true);
+
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+  });
+});
+
+/**
+ * Ensure that a pending tab has label and icon correctly set.
+ */
+add_task(function test_label_and_icon() {
+  // Create a new tab.
+  let tab = gBrowser.addTab("about:robots");
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
+
+  // Retrieve the tab state.
+  SyncHandlers.get(browser).flush();
+  let state = ss.getTabState(tab);
+  gBrowser.removeTab(tab);
+  browser = null;
+
+  // Open a new tab to restore into.
+  let tab = gBrowser.addTab("about:blank");
+  ss.setTabState(tab, state);
+  yield promiseTabRestoring(tab);
+
+  // Check that label and icon are set for the restoring tab.
+  ok(gBrowser.getIcon(tab).startsWith("data:image/png;"), "icon is set");
+  is(tab.label, "Gort! Klaatu barada nikto!", "label is set");
+
+  // Cleanup.
+  gBrowser.removeTab(tab);
+});
+
+function promiseTabRestoring(tab) {
+  let deferred = Promise.defer();
+
+  tab.addEventListener("SSTabRestoring", function onRestoring() {
+    tab.removeEventListener("SSTabRestoring", onRestoring);
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -207,16 +207,17 @@ support-files =
 [browser_dbg_tracing-05.js]
 [browser_dbg_variables-view-01.js]
 [browser_dbg_variables-view-02.js]
 [browser_dbg_variables-view-03.js]
 [browser_dbg_variables-view-04.js]
 [browser_dbg_variables-view-05.js]
 [browser_dbg_variables-view-accessibility.js]
 [browser_dbg_variables-view-data.js]
+[browser_dbg_variables-view-edit-cancel.js]
 [browser_dbg_variables-view-edit-getset-01.js]
 [browser_dbg_variables-view-edit-getset-02.js]
 [browser_dbg_variables-view-edit-value.js]
 [browser_dbg_variables-view-edit-watch.js]
 [browser_dbg_variables-view-filter-01.js]
 [browser_dbg_variables-view-filter-02.js]
 [browser_dbg_variables-view-filter-03.js]
 [browser_dbg_variables-view-filter-04.js]
--- a/browser/devtools/debugger/test/browser_dbg_editor-contextmenu.js
+++ b/browser/devtools/debugger/test/browser_dbg_editor-contextmenu.js
@@ -34,17 +34,17 @@ function test() {
     is(gSources.selectedValue, gSources.values[1],
       "The correct source is selected.");
 
     is(gEditor.getText().indexOf("\u263a"), 162,
       "Unicode characters are converted correctly.");
 
     ok(gContextMenu,
       "The source editor's context menupopup is available.");
-    ok(gEditor.isReadOnly(),
+    ok(gEditor.getOption("readOnly"),
       "The source editor is read only.");
 
     gEditor.focus();
     gEditor.setSelection({ line: 1, ch: 0 }, { line: 1, ch: 10 });
 
     once(gContextMenu, "popupshown").then(testContextMenu).then(null, info);
     gContextMenu.openPopup(gEditor.container, "overlap", 0, 0, true, false);
   }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_variables-view-edit-cancel.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that canceling a name change correctly unhides the separator and
+ * value elements.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_watch-expressions.html";
+
+function test() {
+  Task.spawn(function*() {
+    let [tab, debuggee, panel] = yield initDebugger(TAB_URL);
+    let win = panel.panelWin;
+    let vars = win.DebuggerView.Variables;
+
+    win.DebuggerView.WatchExpressions.addExpression("this");
+
+    // Allow this generator function to yield first.
+    executeSoon(() => debuggee.ermahgerd());
+    yield waitForDebuggerEvents(panel, win.EVENTS.FETCHED_WATCH_EXPRESSIONS);
+
+    let exprScope = vars.getScopeAtIndex(0);
+    let {target} = exprScope.get("this");
+
+    let name = target.querySelector(".title > .name");
+    let separator = target.querySelector(".separator");
+    let value = target.querySelector(".value");
+
+    is(separator.hidden, false,
+      "The separator element should not be hidden.");
+    is(value.hidden, false,
+      "The value element should not be hidden.");
+
+    for (let key of ["ESCAPE", "ENTER"]) {
+      EventUtils.sendMouseEvent({ type: "dblclick" }, name, win);
+
+      is(separator.hidden, true,
+        "The separator element should be hidden.");
+      is(value.hidden, true,
+        "The value element should be hidden.");
+
+      EventUtils.sendKey(key, win);
+
+      is(separator.hidden, false,
+        "The separator element should not be hidden.");
+      is(value.hidden, false,
+        "The value element should not be hidden.");
+    }
+
+    yield resumeDebuggerThenCloseAndFinish(panel);
+  });
+}
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -5,16 +5,17 @@ support-files =
   browser_inspector_bug_831693_search_suggestions.html
   browser_inspector_cmd_inspect.html
   browser_inspector_dead_node_exception.html
   browser_inspector_destroyselection.html
   browser_inspector_menu.html
   browser_inspector_select_last_selected.html
   browser_inspector_select_last_selected2.html
   browser_inspector_bug_848731_reset_selection_on_delete.html
+  browser_inspector_bug_958456_highlight_comments.html
   head.js
 
 [browser_inspector_basic_highlighter.js]
 [browser_inspector_breadcrumbs.js]
 [browser_inspector_bug_650804_search.js]
 [browser_inspector_bug_665880.js]
 [browser_inspector_bug_672902_keyboard_shortcuts.js]
 [browser_inspector_bug_674871.js]
@@ -40,9 +41,10 @@ support-files =
 [browser_inspector_pseudoclass_lock.js]
 [browser_inspector_reload.js]
 [browser_inspector_scrolling.js]
 [browser_inspector_select_last_selected.js]
 [browser_inspector_sidebarstate.js]
 [browser_inspector_bug_848731_reset_selection_on_delete.js]
 [browser_inspector_bug_922125_destroy_on_navigate.js]
 [browser_inspector_bug_952294_tooltips_dimensions.js]
+[browser_inspector_bug_958456_highlight_comments.js]
 [browser_inspector_bug_958169_switch_to_inspector_on_pick.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Inspector Highlighter Test</title>
+</head>
+<body>
+  <p></p>
+  <div id="id1">Visible div 1</div>
+  <!-- Invisible comment node -->
+  <div id="id2">Visible div 2</div>
+  <script type="text/javascript">/*Invisible script node*/</script>
+  <div id="id3">Visible div 3</div>
+  <div id="id4" style="display:none;">Invisible div node</div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.js
@@ -0,0 +1,107 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// Test that hovering over the markup-view's containers doesn't always show the
+// highlighter, depending on the type of node hovered over.
+
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let promise = devtools.require("sdk/core/promise");
+let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+
+const TEST_PAGE = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_958456_highlight_comments.html";
+let inspector, markupView, doc;
+
+function test() {
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+
+    waitForFocus(function() {
+      openInspector((aInspector, aToolbox) => {
+        inspector = aInspector;
+        markupView = inspector.markup;
+        inspector.once("inspector-updated", startTests);
+      });
+    }, content);
+  }, true);
+  content.location = TEST_PAGE;
+}
+
+function startTests() {
+  Task.spawn(function() {
+    yield prepareHighlighter();
+
+    yield hoverElement("#id1");
+    assertHighlighterShownOn("#id1");
+
+    yield hoverComment();
+    assertHighlighterHidden();
+
+    yield hoverElement("#id2");
+    assertHighlighterShownOn("#id2");
+
+    yield hoverElement("script");
+    assertHighlighterHidden();
+
+    yield hoverElement("#id3");
+    assertHighlighterShownOn("#id3");
+
+    yield hoverElement("#id4");
+    assertHighlighterHidden();
+  }).then(null, Cu.reportError).then(finishTest);
+}
+
+function finishTest() {
+  doc = inspector = markupView = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function prepareHighlighter() {
+  // Make sure the highlighter doesn't have transitions enabled
+  let deferred = promise.defer();
+  inspector.selection.setNode(doc.querySelector("p"), null);
+  inspector.once("inspector-updated", () => {
+    getHighlighterOutline().setAttribute("disable-transitions", "true");
+    deferred.resolve();
+  });
+  return deferred.promise;
+}
+
+function hoverContainer(container) {
+  let deferred = promise.defer();
+  EventUtils.synthesizeMouse(container.tagLine, 2, 2, {type: "mousemove"},
+      markupView.doc.defaultView);
+  inspector.markup.once("node-highlight", deferred.resolve);
+  return deferred.promise;
+}
+
+function hoverElement(selector) {
+  info("Hovering node " + selector + " in the markup view");
+  let container = getContainerForRawNode(markupView, doc.querySelector(selector));
+  return hoverContainer(container);
+}
+
+function hoverComment() {
+  info("Hovering the comment node in the markup view");
+  for (let [node, container] of markupView._containers) {
+    if (node.nodeType === Ci.nsIDOMNode.COMMENT_NODE) {
+      return hoverContainer(container);
+    }
+  }
+}
+
+function assertHighlighterShownOn(selector) {
+  let node = doc.querySelector(selector);
+  let highlightNode = getHighlitNode();
+  is(node, highlightNode, "Highlighter is shown on the right node: " + selector);
+}
+
+function assertHighlighterHidden() {
+  ok(!isHighlighting(), "Highlighter is hidden");
+}
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -49,17 +49,17 @@ function createDocument()
 }
 
 function selectNode(aInspector)
 {
   inspector = aInspector;
   inspector.sidebar.once("ruleview-ready", function() {
     ruleview = inspector.sidebar.getWindowForTab("ruleview").ruleview.view;
     inspector.sidebar.select("ruleview");
-    inspector.selection.setNode(div);
+    inspector.selection.setNode(div, "test");
     inspector.once("inspector-updated", performTests);
   });
 }
 
 function performTests()
 {
   // toggle the class
   inspector.togglePseudoClass(pseudo);
@@ -90,31 +90,31 @@ function performTests()
         });
       });
     });
   });
 }
 
 function testNavigate(callback)
 {
-  inspector.selection.setNode(parentDiv);
+  inspector.selection.setNode(parentDiv, "test");
   inspector.once("inspector-updated", () => {
 
     // make sure it's still on after naving to parent
     is(DOMUtils.hasPseudoClassLock(div, pseudo), true,
       "pseudo-class lock is still applied after inspecting ancestor");
 
-    inspector.selection.setNode(div2);
+    inspector.selection.setNode(div2, "test");
     inspector.selection.once("pseudoclass", () => {
       // make sure it's removed after naving to a non-hierarchy node
       is(DOMUtils.hasPseudoClassLock(div, pseudo), false,
         "pseudo-class lock is removed after inspecting sibling node");
 
       // toggle it back on
-      inspector.selection.setNode(div);
+      inspector.selection.setNode(div, "test");
       inspector.once("inspector-updated", () => {
         inspector.togglePseudoClass(pseudo);
         inspector.once("computed-view-refreshed", callback);
       });
     });
   });
 }
 
@@ -139,17 +139,17 @@ function testAdded(cb)
   is(rules.length, 3, "rule view is showing 3 rules for pseudo-class locked div");
   is(rules[1]._ruleEditor.rule.selectorText, "div:hover", "rule view is showing " + pseudo + " rule");
 
   // Show the highlighter by starting the pick mode and hovering over the div
   showPickerOn(div, () => {
     // infobar selector contains pseudo-class
     let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
     is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
-    cb();
+    inspector.toolbox.highlighter.hideBoxModel().then(cb);
   });
 }
 
 function testRemoved()
 {
   // lock removed from node and ancestors
   let node = div;
   do {
@@ -163,17 +163,17 @@ function testRemovedFromUI(cb)
 {
   // ruleview no longer contains pseudo-class rule
   let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   showPickerOn(div, () => {
     let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
     is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
-    cb();
+    inspector.toolbox.highlighter.hideBoxModel().then(cb);
   });
 }
 
 function finishUp()
 {
   gDevTools.once("toolbox-destroyed", function() {
     testRemoved();
     inspector = ruleview = null;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -278,26 +278,42 @@ MarkupView.prototype = {
     if (container) {
       // With the newly found container, delegate the tooltip content creation
       // and decision to show or not the tooltip
       return container._buildTooltipContent(target, this.tooltip);
     }
   },
 
   /**
+   * Given the known reason, should the current selection be briefly highlighted
+   * In a few cases, we don't want to highlight the node:
+   * - If the reason is null (used to reset the selection),
+   * - if it's "inspector-open" (when the inspector opens up, let's not highlight
+   * the default node)
+   * - if it's "navigateaway" (since the page is being navigated away from)
+   * - if it's "test" (this is a special case for mochitest. In tests, we often
+   * need to select elements but don't necessarily want the highlighter to come
+   * and go after a delay as this might break test scenarios)
+   */
+  _shouldNewSelectionBeHighlighted: function() {
+    let reason = this._inspector.selection.reason;
+    let unwantedReasons = ["inspector-open", "navigateaway", "test"];
+    return reason && unwantedReasons.indexOf(reason) === -1;
+  },
+
+  /**
    * Highlight the inspector selected node.
    */
   _onNewSelection: function() {
     let selection = this._inspector.selection;
 
     this.htmlEditor.hide();
     let done = this._inspector.updating("markup-view");
     if (selection.isNode()) {
-      let reason = selection.reason;
-      if (reason && reason !== "inspector-open" && reason !== "navigateaway") {
+      if (this._shouldNewSelectionBeHighlighted()) {
         this._brieflyShowBoxModel(selection.nodeFront, {
           scrollIntoView: true
         });
       }
 
       this.showNode(selection.nodeFront, true).then(() => {
         if (selection.reason !== "treepanel") {
           this.markNodeAsSelected(selection.nodeFront);
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -1485,24 +1485,20 @@ var Scratchpad = {
       this._instanceId = ScratchpadManager.createUid();
     }
 
     let config = {
       mode: Editor.modes.js,
       value: initialText,
       lineNumbers: true,
       showTrailingSpace: Services.prefs.getBoolPref(SHOW_TRAILING_SPACE),
+      enableCodeFolding: Services.prefs.getBoolPref(ENABLE_CODE_FOLDING),
       contextMenu: "scratchpad-text-popup"
     };
 
-    if (Services.prefs.getBoolPref(ENABLE_CODE_FOLDING)) {
-      config.foldGutter = true;
-      config.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
-    }
-
     this.editor = new Editor(config);
     this.editor.appendTo(document.querySelector("#scratchpad-editor")).then(() => {
       var lines = initialText.split("\n");
 
       this.editor.on("change", this._onChanged);
       this.editor.on("save", () => this.saveFile());
       this.editor.focus();
       this.editor.setCursor({ line: lines.length, ch: lines.pop().length });
--- a/browser/devtools/shared/widgets/VariablesView.jsm
+++ b/browser/devtools/shared/widgets/VariablesView.jsm
@@ -3677,16 +3677,17 @@ Editable.prototype = {
    * Activate this editable by replacing the input box it overlays and
    * initialize the handlers.
    *
    * @param Event e [optional]
    *        Optionally, the Event object that was used to activate the Editable.
    */
   activate: function(e) {
     if (!this.shouldActivate) {
+      this._onCleanup && this._onCleanup();
       return;
     }
 
     let { label } = this;
     let initialString = label.getAttribute("value");
 
     if (e) {
       e.preventDefault();
@@ -3734,16 +3735,17 @@ Editable.prototype = {
     this._input.parentNode.replaceChild(this.label, this._input);
     this._input = null;
 
     let { boxObject } = this._variable._variablesView;
     boxObject.scrollBy(-this._variable._target, 0);
     this._variable.locked = false;
     this._variable.twisty = this._prevExpandable;
     this._variable.expanded = this._prevExpanded;
+    this._onCleanup && this._onCleanup();
   },
 
   /**
    * Save the current value and deactivate the Editable.
    */
   _save: function() {
     let initial = this.label.getAttribute("value");
     let current = this._input.value.trim();
@@ -3862,19 +3864,16 @@ EditableNameAndValue.prototype = Heritag
   _next: function(e) {
     // Override _next so as to set both key and value at the same time.
     let key = this._input.value;
     this.label.setAttribute("value", key);
 
     let valueEditable = EditableValue.create(this._variable, {
       onSave: aValue => {
         this._onSave([key, aValue]);
-      },
-      onCleanup: () => {
-        this._onCleanup();
       }
     });
     valueEditable._reset = () => {
       this._variable.remove();
       valueEditable.deactivate();
     };
   },
 
--- a/browser/devtools/sourceeditor/codemirror/README
+++ b/browser/devtools/sourceeditor/codemirror/README
@@ -12,61 +12,73 @@ CSS files inside the codemirror director
 To confirm the functionality run mochitests for the following components:
 
  * sourceeditor
  * scratchpad
  * debugger
  * styleditor
  * netmonitor
 
-The sourceeditor component contains imported CodeMirror tests [3]. Some
-tests were commented out because we don't use that functionality within
-Firefox (for example Ruby editing mode). The search addon (search.js)
-was slightly modified to make search UI localizable. Other than that,
-we don't have any Mozilla-specific patches applied to CodeMirror itself.
+The sourceeditor component contains imported CodeMirror tests [3].
+
+ * Some tests were commented out because we don't use that functionality
+   within Firefox (for example Ruby editing mode). Be careful when updating
+   files test/codemirror.html and test/vimemacs.html; they were modified to
+   co-exist with Mozilla's testing infrastructure.
+ * In cm_comment_test.js comment out fallbackToBlock and fallbackToLine
+   tests.
+ * The search addon (search.js) was slightly modified to make search
+   UI localizable.
+
+Other than that, we don't have any Mozilla-specific patches applied to
+CodeMirror itself.
 
 # Addons
 
 To install a new CodeMirror addon add it to the codemirror directory,
 jar.mn [4] file and editor.js [5]. Also, add it to the License section
 below.
 
 # License
 
 The following files in this directory are licensed according to the contents
 in the LICENSE file:
 
  * codemirror.css
  * codemirror.js
  * comment.js
+ * activeline.js
  * dialog/dialog.css
  * dialog/dialog.js
  * keymap/emacs.js
  * keymap/vim.js
  * fold/foldcode.js
  * fold/brace-fold.js
  * fold/comment-fold.js
  * fold/xml-fold.js
  * fold/foldgutter.js
  * xml.js
  * css.js
  * javascript.js
  * clike.js
+ * htmlmixed.js
  * matchbrackets.js
  * closebrackets.js
  * trailingspace.js
  * search/match-highlighter.js
  * search/search.js
  * search/searchcursor.js
  * test/codemirror.html
  * test/cm_comment_test.js
  * test/cm_driver.js
  * test/cm_mode_javascript_test.js
  * test/cm_mode_test.css
  * test/cm_mode_test.js
+ * test/cm_vim_test.js
+ * test/cm_emacs_test.js
  * test/cm_test.js
 
 # Footnotes
 
 [1] http://codemirror.net
 [2] browser/devtools/sourceeditor/codemirror
 [3] browser/devtools/sourceeditor/test/browser_codemirror.js
 [4] browser/devtools/jar.mn
--- a/browser/devtools/sourceeditor/codemirror/activeline.js
+++ b/browser/devtools/sourceeditor/codemirror/activeline.js
@@ -1,40 +1,45 @@
-/* vim:set ts=2 sw=2 sts=2 et tw=80:
- * 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/. */
+// Because sometimes you need to style the cursor's line.
+//
+// Adds an option 'styleActiveLine' which, when enabled, gives the
+// active line's wrapping <div> the CSS class "CodeMirror-activeline",
+// and gives its background <div> the class "CodeMirror-activeline-background".
 
-(function () {
+(function() {
   "use strict";
-
-  const WRAP_CLASS = "CodeMirror-activeline";
-  const BACK_CLASS = "CodeMirror-activeline-background";
+  var WRAP_CLASS = "CodeMirror-activeline";
+  var BACK_CLASS = "CodeMirror-activeline-background";
 
   CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
     var prev = old && old != CodeMirror.Init;
-
     if (val && !prev) {
-      updateActiveLine(cm);
-      cm.on("cursorActivity", updateActiveLine);
+      updateActiveLine(cm, cm.getCursor().line);
+      cm.on("beforeSelectionChange", selectionChange);
     } else if (!val && prev) {
-      cm.off("cursorActivity", updateActiveLine);
+      cm.off("beforeSelectionChange", selectionChange);
       clearActiveLine(cm);
       delete cm.state.activeLine;
     }
   });
 
   function clearActiveLine(cm) {
     if ("activeLine" in cm.state) {
       cm.removeLineClass(cm.state.activeLine, "wrap", WRAP_CLASS);
       cm.removeLineClass(cm.state.activeLine, "background", BACK_CLASS);
     }
   }
 
-  function updateActiveLine(cm) {
-    var line = cm.getLineHandleVisualStart(cm.getCursor().line);
+  function updateActiveLine(cm, selectedLine) {
+    var line = cm.getLineHandleVisualStart(selectedLine);
     if (cm.state.activeLine == line) return;
-    clearActiveLine(cm);
-    cm.addLineClass(line, "wrap", WRAP_CLASS);
-    cm.addLineClass(line, "background", BACK_CLASS);
-    cm.state.activeLine = line;
+    cm.operation(function() {
+      clearActiveLine(cm);
+      cm.addLineClass(line, "wrap", WRAP_CLASS);
+      cm.addLineClass(line, "background", BACK_CLASS);
+      cm.state.activeLine = line;
+    });
   }
-})();
\ No newline at end of file
+
+  function selectionChange(cm, sel) {
+    updateActiveLine(cm, sel.head.line);
+  }
+})();
--- a/browser/devtools/sourceeditor/codemirror/clike.js
+++ b/browser/devtools/sourceeditor/codemirror/clike.js
@@ -198,52 +198,70 @@ CodeMirror.defineMode("clike", function(
       if (next == '"' && !stream.eat('"')) {
         state.tokenize = null;
         break;
       }
     }
     return "string";
   }
 
-  function mimes(ms, mode) {
-    for (var i = 0; i < ms.length; ++i) CodeMirror.defineMIME(ms[i], mode);
+  function def(mimes, mode) {
+    var words = [];
+    function add(obj) {
+      if (obj) for (var prop in obj) if (obj.hasOwnProperty(prop))
+        words.push(prop);
+    }
+    add(mode.keywords);
+    add(mode.builtin);
+    add(mode.atoms);
+    if (words.length) {
+      mode.helperType = mimes[0];
+      CodeMirror.registerHelper("hintWords", mimes[0], words);
+    }
+
+    for (var i = 0; i < mimes.length; ++i)
+      CodeMirror.defineMIME(mimes[i], mode);
   }
 
-  mimes(["text/x-csrc", "text/x-c", "text/x-chdr"], {
+  def(["text/x-csrc", "text/x-c", "text/x-chdr"], {
     name: "clike",
     keywords: words(cKeywords),
     blockKeywords: words("case do else for if switch while struct"),
     atoms: words("null"),
-    hooks: {"#": cppHook}
+    hooks: {"#": cppHook},
+    modeProps: {fold: ["brace", "include"]}
   });
-  mimes(["text/x-c++src", "text/x-c++hdr"], {
+
+  def(["text/x-c++src", "text/x-c++hdr"], {
     name: "clike",
     keywords: words(cKeywords + " asm dynamic_cast namespace reinterpret_cast try bool explicit new " +
                     "static_cast typeid catch operator template typename class friend private " +
                     "this using const_cast inline public throw virtual delete mutable protected " +
                     "wchar_t"),
     blockKeywords: words("catch class do else finally for if struct switch try while"),
     atoms: words("true false null"),
-    hooks: {"#": cppHook}
+    hooks: {"#": cppHook},
+    modeProps: {fold: ["brace", "include"]}
   });
   CodeMirror.defineMIME("text/x-java", {
     name: "clike",
     keywords: words("abstract assert boolean break byte case catch char class const continue default " +
                     "do double else enum extends final finally float for goto if implements import " +
                     "instanceof int interface long native new package private protected public " +
                     "return short static strictfp super switch synchronized this throw throws transient " +
                     "try void volatile while"),
     blockKeywords: words("catch class do else finally for if switch try while"),
     atoms: words("true false null"),
     hooks: {
       "@": function(stream) {
         stream.eatWhile(/[\w\$_]/);
         return "meta";
       }
-    }
+    },
+    modeProps: {fold: ["brace", "import"]}
   });
   CodeMirror.defineMIME("text/x-csharp", {
     name: "clike",
     keywords: words("abstract as base break case catch checked class const continue" +
                     " default delegate do else enum event explicit extern finally fixed for" +
                     " foreach goto if implicit in interface internal is lock namespace new" +
                     " operator out override params private protected public readonly ref return sealed" +
                     " sizeof stackalloc static struct switch this throw try typeof unchecked" +
@@ -298,17 +316,17 @@ CodeMirror.defineMode("clike", function(
     atoms: words("true false null"),
     hooks: {
       "@": function(stream) {
         stream.eatWhile(/[\w\$_]/);
         return "meta";
       }
     }
   });
-  mimes(["x-shader/x-vertex", "x-shader/x-fragment"], {
+  def(["x-shader/x-vertex", "x-shader/x-fragment"], {
     name: "clike",
     keywords: words("float int bool void " +
                     "vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 " +
                     "mat2 mat3 mat4 " +
                     "sampler1D sampler2D sampler3D samplerCube " +
                     "sampler1DShadow sampler2DShadow" +
                     "const attribute uniform varying " +
                     "break continue discard return " +
@@ -352,11 +370,12 @@ CodeMirror.defineMode("clike", function(
                 "gl_FrontLightModelProduct gl_BackLightModelProduct " +
                 "gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ " +
                 "gl_FogParameters " +
                 "gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords " +
                 "gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats " +
                 "gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits " +
                 "gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits " +
                 "gl_MaxDrawBuffers"),
-    hooks: {"#": cppHook}
+    hooks: {"#": cppHook},
+    modeProps: {fold: ["brace", "include"]}
   });
 }());
--- a/browser/devtools/sourceeditor/codemirror/closebrackets.js
+++ b/browser/devtools/sourceeditor/codemirror/closebrackets.js
@@ -23,17 +23,17 @@
                           CodeMirror.Pos(pos.line, pos.ch + 1));
     return str.length == 2 ? str : null;
   }
 
   function buildKeymap(pairs) {
     var map = {
       name : "autoCloseBrackets",
       Backspace: function(cm) {
-        if (cm.somethingSelected()) return CodeMirror.Pass;
+        if (cm.somethingSelected() || cm.getOption("disableInput")) return CodeMirror.Pass;
         var cur = cm.getCursor(), around = charsAround(cm, cur);
         if (around && pairs.indexOf(around) % 2 == 0)
           cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1));
         else
           return CodeMirror.Pass;
       }
     };
     var closingBrackets = "";
@@ -44,17 +44,18 @@
         cm.replaceSelection(left + selection + right);
       }
       function maybeOverwrite(cm) {
         var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1));
         if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass;
         else cm.execCommand("goCharRight");
       }
       map["'" + left + "'"] = function(cm) {
-        if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment")
+        if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment" ||
+            cm.getOption("disableInput"))
           return CodeMirror.Pass;
         if (cm.somethingSelected()) return surround(cm);
         if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return;
         var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1);
         var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : "";
         if (left == right && CodeMirror.isWordChar(curChar))
           return CodeMirror.Pass;
         if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar))
@@ -65,17 +66,18 @@
       if (left != right) map["'" + right + "'"] = maybeOverwrite;
     })(pairs.charAt(i), pairs.charAt(i + 1));
     return map;
   }
 
   function buildExplodeHandler(pairs) {
     return function(cm) {
       var cur = cm.getCursor(), around = charsAround(cm, cur);
-      if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+      if (!around || pairs.indexOf(around) % 2 != 0 || cm.getOption("disableInput"))
+        return CodeMirror.Pass;
       cm.operation(function() {
         var newPos = CodeMirror.Pos(cur.line + 1, 0);
         cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input");
         cm.indentLine(cur.line + 1, null, true);
         cm.indentLine(cur.line + 2, null, true);
       });
     };
   }
--- a/browser/devtools/sourceeditor/codemirror/codemirror.css
+++ b/browser/devtools/sourceeditor/codemirror/codemirror.css
@@ -56,17 +56,17 @@
 }
 /* Can style cursor different in overwrite (non-insert) mode */
 .CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {}
 
 .cm-tab { display: inline-block; }
 
 /* DEFAULT THEME */
 
-.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default     .cm-keyword {color: #708;}
 .cm-s-default .cm-atom {color: #219;}
 .cm-s-default .cm-number {color: #164;}
 .cm-s-default .cm-def {color: #00f;}
 .cm-s-default .cm-variable {color: black;}
 .cm-s-default .cm-variable-2 {color: #05a;}
 .cm-s-default .cm-variable-3 {color: #085;}
 .cm-s-default .cm-property {color: black;}
 .cm-s-default .cm-operator {color: black;}
--- a/browser/devtools/sourceeditor/codemirror/codemirror.js
+++ b/browser/devtools/sourceeditor/codemirror/codemirror.js
@@ -1,26 +1,27 @@
-// CodeMirror version 3.20
+// CodeMirror version 3.21
 //
 // CodeMirror is the only global var we claim
 window.CodeMirror = (function() {
   "use strict";
 
   // BROWSER SNIFFING
 
   // Crude, but necessary to handle a number of hard-to-feature-detect
   // bugs and behavior differences.
   var gecko = /gecko\/\d/i.test(navigator.userAgent);
   // IE11 currently doesn't count as 'ie', since it has almost none of
   // the same bugs as earlier versions. Use ie_gt10 to handle
   // incompatibilities in that version.
-  var ie = /MSIE \d/.test(navigator.userAgent);
-  var ie_lt8 = ie && (document.documentMode == null || document.documentMode < 8);
-  var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
+  var old_ie = /MSIE \d/.test(navigator.userAgent);
+  var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8);
+  var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9);
   var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent);
+  var ie = old_ie || ie_gt10;
   var webkit = /WebKit\//.test(navigator.userAgent);
   var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent);
   var chrome = /Chrome\//.test(navigator.userAgent);
   var opera = /Opera\//.test(navigator.userAgent);
   var safari = /Apple Computer/.test(navigator.vendor);
   var khtml = /KHTML\//.test(navigator.userAgent);
   var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent);
   var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent);
@@ -32,17 +33,17 @@ window.CodeMirror = (function() {
   var mac = ios || /Mac/.test(navigator.platform);
   var windows = /win/i.test(navigator.platform);
 
   var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/);
   if (opera_version) opera_version = Number(opera_version[1]);
   if (opera_version && opera_version >= 15) { opera = false; webkit = true; }
   // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
   var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11));
-  var captureMiddleClick = gecko || (ie && !ie_lt9);
+  var captureMiddleClick = gecko || (old_ie && !ie_lt9);
 
   // Optimize some code when these features are not used
   var sawReadOnlySpans = false, sawCollapsedSpans = false;
 
   // CONSTRUCTOR
 
   function CodeMirror(place, options) {
     if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
@@ -58,31 +59,32 @@ window.CodeMirror = (function() {
     display.wrapper.CodeMirror = this;
     updateGutters(this);
     if (options.autofocus && !mobile) focusInput(this);
 
     this.state = {keyMaps: [],
                   overlays: [],
                   modeGen: 0,
                   overwrite: false, focused: false,
-                  suppressEdits: false, pasteIncoming: false,
+                  suppressEdits: false,
+                  pasteIncoming: false, cutIncoming: false,
                   draggingText: false,
                   highlight: new Delayed()};
 
     themeChanged(this);
     if (options.lineWrapping)
       this.display.wrapper.className += " CodeMirror-wrap";
 
     var doc = options.value;
     if (typeof doc == "string") doc = new Doc(options.value, options.mode);
     operation(this, attachDoc)(this, doc);
 
     // Override magic textarea content restore that IE sometimes does
     // on our hidden textarea on reload
-    if (ie) setTimeout(bind(resetInput, this, true), 20);
+    if (old_ie) setTimeout(bind(resetInput, this, true), 20);
 
     registerEventHandlers(this);
     // IE throws unspecified error in certain cases, when
     // trying to access activeElement before onload
     var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { }
     if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20);
     else onBlur(this);
 
@@ -192,16 +194,20 @@ window.CodeMirror = (function() {
   }
 
   // STATE UPDATES
 
   // Used to get the editor into a consistent state again when options change.
 
   function loadMode(cm) {
     cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption);
+    resetModeState(cm);
+  }
+
+  function resetModeState(cm) {
     cm.doc.iter(function(line) {
       if (line.stateAfter) line.stateAfter = null;
       if (line.styles) line.styles = null;
     });
     cm.doc.frontier = cm.doc.first;
     startWorker(cm, 100);
     cm.state.modeGen++;
     if (cm.curOp) regChange(cm);
@@ -241,17 +247,16 @@ window.CodeMirror = (function() {
       if (estHeight != line.height) updateLineHeight(line, estHeight);
     });
   }
 
   function keyMapChanged(cm) {
     var map = keyMap[cm.options.keyMap], style = map.style;
     cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
       (style ? " cm-keymap-" + style : "");
-    cm.state.disableInput = map.disableInput;
   }
 
   function themeChanged(cm) {
     cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") +
       cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-");
     clearCaches(cm);
   }
 
@@ -447,17 +452,17 @@ window.CodeMirror = (function() {
     return updated;
   }
 
   // Uses a set of changes plus the current scroll position to
   // determine which DOM updates have to be made, and makes the
   // updates.
   function updateDisplayInner(cm, changes, visible, forced) {
     var display = cm.display, doc = cm.doc;
-    if (!display.wrapper.clientWidth) {
+    if (!display.wrapper.offsetWidth) {
       display.showingFrom = display.showingTo = doc.first;
       display.viewOffset = 0;
       return;
     }
 
     // Bail out if the visible area is already rendered and nothing changed.
     if (!forced && changes.length == 0 &&
         visible.from > display.showingFrom && visible.to < display.showingTo)
@@ -532,16 +537,17 @@ window.CodeMirror = (function() {
     // This is just a bogus formula that detects when the editor is
     // resized or the font size changes.
     if (different) {
       display.lastSizeC = display.wrapper.clientHeight;
       startWorker(cm, 400);
     }
     display.showingFrom = from; display.showingTo = to;
 
+    display.gutters.style.height = "";
     updateHeightsInViewport(cm);
     updateViewOffset(cm);
 
     return true;
   }
 
   function updateHeightsInViewport(cm) {
     var display = cm.display;
@@ -714,19 +720,19 @@ window.CodeMirror = (function() {
     if (!wrap) {
       wrap = elt("div", null, line.wrapClass, "position: relative");
       wrap.appendChild(lineElement);
     }
     // Kludge to make sure the styled element lies behind the selection (by z-index)
     if (bgClass)
       wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild);
     if (cm.options.lineNumbers || markers) {
-      var gutterWrap = wrap.insertBefore(elt("div", null, null, "position: absolute; left: " +
+      var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " +
                                              (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"),
-                                         wrap.firstChild);
+                                         lineElement);
       if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap);
       if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
         wrap.lineNumber = gutterWrap.appendChild(
           elt("div", lineNumberFor(cm.options, lineNo),
               "CodeMirror-linenumber CodeMirror-gutter-elt",
               "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
               + display.lineNumInnerWidth + "px"));
       if (markers)
@@ -1050,17 +1056,17 @@ window.CodeMirror = (function() {
     // do an amount of layout work quadratic to the number of
     // characters. When line wrapping is off, we try to improve things
     // by first subdividing the line into a bunch of inline blocks, so
     // that IE can reuse most of the layout information from caches
     // for those blocks. This does interfere with line wrapping, so it
     // doesn't work when wrapping is on, but in that case the
     // situation is slightly better, since IE does cache line-wrapping
     // information and only recomputes per-line.
-    if (ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
+    if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) {
       var fragment = document.createDocumentFragment();
       var chunk = 10, n = pre.childNodes.length;
       for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) {
         var wrap = elt("div", null, null, "display: inline-block");
         for (var j = 0; j < chunk && n; ++j) {
           wrap.appendChild(pre.firstChild);
           --n;
         }
@@ -1110,17 +1116,17 @@ window.CodeMirror = (function() {
         if (cur.firstChild.nodeType == 1) node = cur.firstChild;
         var rects = node.getClientRects();
         if (rects.length > 1) {
           rect = data[i] = measureRect(rects[0]);
           rect.rightSide = measureRect(rects[rects.length - 1]);
         }
       }
       if (!rect) rect = data[i] = measureRect(getRect(node));
-      if (cur.measureRight) rect.right = getRect(cur.measureRight).left;
+      if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left;
       if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide));
     }
     removeChildren(cm.display.measure);
     for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) {
       finishRect(cur);
       if (cur.leftSide) finishRect(cur.leftSide);
       if (cur.rightSide) finishRect(cur.rightSide);
     }
@@ -1286,17 +1292,17 @@ window.CodeMirror = (function() {
     var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine;
 
     if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1);
     // Do a binary search between these bounds.
     for (;;) {
       if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) {
         var ch = x < fromX || x - fromX <= toX - x ? from : to;
         var xDiff = x - (ch == from ? fromX : toX);
-        while (isExtendingChar.test(lineObj.text.charAt(ch))) ++ch;
+        while (isExtendingChar(lineObj.text.charAt(ch))) ++ch;
         var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside,
                               xDiff < 0 ? -1 : xDiff ? 1 : 0);
         return pos;
       }
       var step = Math.ceil(dist / 2), middle = from + step;
       if (bidi) {
         middle = from;
         for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1);
@@ -1478,17 +1484,17 @@ window.CodeMirror = (function() {
 
   // prevInput is a hack to work with IME. If we reset the textarea
   // on every change, that breaks IME. So we look for changes
   // compared to the previous content instead. (Modern browsers have
   // events that indicate IME taking place, but these are not widely
   // supported or compatible enough yet to rely on.)
   function readInput(cm) {
     var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel;
-    if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.state.disableInput) return false;
+    if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false;
     if (cm.state.pasteIncoming && cm.state.fakedLastChar) {
       input.value = input.value.substring(0, input.value.length - 1);
       cm.state.fakedLastChar = false;
     }
     var text = input.value;
     if (text == prevInput && posEq(sel.from, sel.to)) return false;
     if (ie && !ie_lt9 && cm.display.inputHasSelection === text) {
       resetInput(cm, true);
@@ -1496,32 +1502,42 @@ window.CodeMirror = (function() {
     }
 
     var withOp = !cm.curOp;
     if (withOp) startOperation(cm);
     sel.shift = false;
     var same = 0, l = Math.min(prevInput.length, text.length);
     while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
     var from = sel.from, to = sel.to;
+    var inserted = text.slice(same);
     if (same < prevInput.length)
       from = Pos(from.line, from.ch - (prevInput.length - same));
     else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming)
-      to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + (text.length - same)));
+      to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + inserted.length));
 
     var updateInput = cm.curOp.updateInput;
-    var changeEvent = {from: from, to: to, text: splitLines(text.slice(same)),
-                       origin: cm.state.pasteIncoming ? "paste" : "+input"};
+    var changeEvent = {from: from, to: to, text: splitLines(inserted),
+                       origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"};
     makeChange(cm.doc, changeEvent, "end");
     cm.curOp.updateInput = updateInput;
     signalLater(cm, "inputRead", cm, changeEvent);
+    if (inserted && !cm.state.pasteIncoming && cm.options.electricChars &&
+        cm.options.smartIndent && sel.head.ch < 100) {
+      var electric = cm.getModeAt(sel.head).electricChars;
+      if (electric) for (var i = 0; i < electric.length; i++)
+        if (inserted.indexOf(electric.charAt(i)) > -1) {
+          indentLine(cm, sel.head.line, "smart");
+          break;
+        }
+    }
 
     if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = "";
     else cm.display.prevInput = text;
     if (withOp) endOperation(cm);
-    cm.state.pasteIncoming = false;
+    cm.state.pasteIncoming = cm.state.cutIncoming = false;
     return true;
   }
 
   function resetInput(cm, user) {
     var minimal, selected, doc = cm.doc;
     if (!posEq(doc.sel.from, doc.sel.to)) {
       cm.display.prevInput = "";
       minimal = hasCopyEvent &&
@@ -1546,17 +1562,17 @@ window.CodeMirror = (function() {
     return cm.options.readOnly || cm.doc.cantEdit;
   }
 
   // EVENT HANDLERS
 
   function registerEventHandlers(cm) {
     var d = cm.display;
     on(d.scroller, "mousedown", operation(cm, onMouseDown));
-    if (ie)
+    if (old_ie)
       on(d.scroller, "dblclick", operation(cm, function(e) {
         if (signalDOMEvent(cm, e)) return;
         var pos = posFromMouse(cm, e);
         if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return;
         e_preventDefault(e);
         var word = findWordAt(getLine(cm.doc, pos.line).text, pos);
         extendSelection(cm.doc, word.from, word.to);
       }));
@@ -1652,31 +1668,32 @@ window.CodeMirror = (function() {
         d.input.selectionStart = start;
         d.input.selectionEnd = end;
         cm.state.fakedLastChar = true;
       }
       cm.state.pasteIncoming = true;
       fastPoll(cm);
     });
 
-    function prepareCopy() {
+    function prepareCopy(e) {
       if (d.inaccurateSelection) {
         d.prevInput = "";
         d.inaccurateSelection = false;
         d.input.value = cm.getSelection();
         selectInput(d.input);
       }
+      if (e.type == "cut") cm.state.cutIncoming = true;
     }
     on(d.input, "cut", prepareCopy);
     on(d.input, "copy", prepareCopy);
 
     // Needed to handle Tab key in KHTML
     if (khtml) on(d.sizer, "mouseup", function() {
-        if (document.activeElement == d.input) d.input.blur();
-        focusInput(cm);
+      if (document.activeElement == d.input) d.input.blur();
+      focusInput(cm);
     });
   }
 
   function eventInWidget(display, e) {
     for (var n = e_target(e); n != display.wrapper; n = n.parentNode) {
       if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true;
     }
   }
@@ -1750,16 +1767,19 @@ window.CodeMirror = (function() {
         if (webkit) display.scroller.draggable = false;
         cm.state.draggingText = false;
         off(document, "mouseup", dragEnd);
         off(display.scroller, "drop", dragEnd);
         if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
           e_preventDefault(e2);
           extendSelection(cm.doc, start);
           focusInput(cm);
+          // Work around unexplainable focus problem in IE9 (#2127)
+          if (old_ie && !ie_lt9)
+            setTimeout(function() {document.body.focus(); focusInput(cm);}, 20);
         }
       });
       // Let the drag handler handle this.
       if (webkit) display.scroller.draggable = true;
       cm.state.draggingText = dragEnd;
       // IE's approach to draggable
       if (display.scroller.dragDrop) display.scroller.dragDrop();
       on(document, "mouseup", dragEnd);
@@ -1824,17 +1844,17 @@ window.CodeMirror = (function() {
       counter = Infinity;
       e_preventDefault(e);
       focusInput(cm);
       off(document, "mousemove", move);
       off(document, "mouseup", up);
     }
 
     var move = operation(cm, function(e) {
-      if (!ie && !e_button(e)) done(e);
+      if (!old_ie && !e_button(e)) done(e);
       else extend(e);
     });
     var up = operation(cm, done);
     on(document, "mousemove", move);
     on(document, "mouseup", up);
   }
 
   function gutterEvent(cm, e, type, prevent, signalfn) {
@@ -1969,17 +1989,17 @@ window.CodeMirror = (function() {
   // is that it gives us a chance to update the display before the
   // actual scrolling happens, reducing flickering.
 
   var wheelSamples = 0, wheelPixelsPerUnit = null;
   // Fill in a browser-detected starting value on browsers where we
   // know one. These don't have to be accurate -- the result of them
   // being wrong would just be a slight flicker on the first wheel
   // scroll (if it is large enough).
-  if (ie) wheelPixelsPerUnit = -.53;
+  if (old_ie) wheelPixelsPerUnit = -.53;
   else if (gecko) wheelPixelsPerUnit = 15;
   else if (chrome) wheelPixelsPerUnit = -.7;
   else if (safari) wheelPixelsPerUnit = -1/3;
 
   function onScrollWheel(cm, e) {
     var dx = e.wheelDeltaX, dy = e.wheelDeltaY;
     if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
     if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail;
@@ -2123,17 +2143,17 @@ window.CodeMirror = (function() {
     return handled;
   }
 
   var lastStoppedKey = null;
   function onKeyDown(e) {
     var cm = this;
     if (!cm.state.focused) onFocus(cm);
     if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
-    if (ie && e.keyCode == 27) e.returnValue = false;
+    if (old_ie && e.keyCode == 27) e.returnValue = false;
     var code = e.keyCode;
     // IE does strange things with escape.
     cm.doc.sel.shift = code == 16 || e.shiftKey;
     // First give onKeyEvent option a chance to handle this.
     var handled = handleKeyBinding(cm, e);
     if (opera) {
       lastStoppedKey = handled ? code : null;
       // Opera has no cut event... we try to at least catch the key combo
@@ -2144,20 +2164,16 @@ window.CodeMirror = (function() {
 
   function onKeyPress(e) {
     var cm = this;
     if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return;
     var keyCode = e.keyCode, charCode = e.charCode;
     if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
     if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return;
     var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
-    if (this.options.electricChars && this.doc.mode.electricChars &&
-        this.options.smartIndent && !isReadOnly(this) &&
-        this.doc.mode.electricChars.indexOf(ch) > -1)
-      setTimeout(operation(cm, function() {indentLine(cm, cm.doc.sel.to.line, "smart");}), 75);
     if (handleCharBinding(cm, e, ch)) return;
     if (ie && !ie_lt9) cm.display.inputHasSelection = null;
     fastPoll(cm);
   }
 
   function onFocus(cm) {
     if (cm.options.readOnly == "nocursor") return;
     if (!cm.state.focused) {
@@ -2196,17 +2212,17 @@ window.CodeMirror = (function() {
     // and 'resetSelectionOnContextMenu' option is true.
     var reset = cm.options.resetSelectionOnContextMenu;
     if (reset && (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to)))
       operation(cm, setSelection)(cm.doc, pos, pos);
 
     var oldCSS = display.input.style.cssText;
     display.inputDiv.style.position = "absolute";
     display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
-      "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; outline: none;" +
+      "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" +
       "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);";
     focusInput(cm);
     resetInput(cm, true);
     // Adds "Select all" to context menu in FF
     if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " ";
 
     function prepareSelectAllHack() {
       if (display.input.selectionStart != null) {
@@ -2218,29 +2234,29 @@ window.CodeMirror = (function() {
     function rehide() {
       display.inputDiv.style.position = "relative";
       display.input.style.cssText = oldCSS;
       if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos;
       slowPoll(cm);
 
       // Try to detect the user choosing select-all
       if (display.input.selectionStart != null) {
-        if (!ie || ie_lt9) prepareSelectAllHack();
+        if (!old_ie || ie_lt9) prepareSelectAllHack();
         clearTimeout(detectingSelectAll);
         var i = 0, poll = function(){
-          if (display.prevInput == " " && display.input.selectionStart == 0)
+          if (display.prevInput == "\u200b" && display.input.selectionStart == 0)
             operation(cm, commands.selectAll)(cm);
           else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
           else resetInput(cm);
         };
         detectingSelectAll = setTimeout(poll, 200);
       }
     }
 
-    if (ie && !ie_lt9) prepareSelectAllHack();
+    if (old_ie && !ie_lt9) prepareSelectAllHack();
     if (captureMiddleClick) {
       e_stop(e);
       var mouseup = function() {
         off(window, "mouseup", mouseup);
         setTimeout(rehide, 20);
       };
       on(window, "mouseup", mouseup);
     } else {
@@ -2503,16 +2519,17 @@ window.CodeMirror = (function() {
   function Pos(line, ch) {
     if (!(this instanceof Pos)) return new Pos(line, ch);
     this.line = line; this.ch = ch;
   }
   CodeMirror.Pos = Pos;
 
   function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
   function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
+  function cmp(a, b) {return a.line - b.line || a.ch - b.ch;}
   function copyPos(x) {return Pos(x.line, x.ch);}
 
   // SELECTION
 
   function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));}
   function clipPos(doc, pos) {
     if (pos.line < doc.first) return Pos(doc.first, 0);
     var last = doc.first + doc.size - 1;
@@ -2647,24 +2664,23 @@ window.CodeMirror = (function() {
 
   function scrollCursorIntoView(cm) {
     var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin);
     if (!cm.state.focused) return;
     var display = cm.display, box = getRect(display.sizer), doScroll = null;
     if (coords.top + box.top < 0) doScroll = true;
     else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
     if (doScroll != null && !phantom) {
-      var hidden = display.cursor.style.display == "none";
-      if (hidden) {
-        display.cursor.style.display = "";
-        display.cursor.style.left = coords.left + "px";
-        display.cursor.style.top = (coords.top - display.viewOffset) + "px";
-      }
-      display.cursor.scrollIntoView(doScroll);
-      if (hidden) display.cursor.style.display = "none";
+      var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " +
+                           (coords.top - display.viewOffset) + "px; height: " +
+                           (coords.bottom - coords.top + scrollerCutOff) + "px; left: " +
+                           coords.left + "px; width: 2px;");
+      cm.display.lineSpace.appendChild(scrollNode);
+      scrollNode.scrollIntoView(doScroll);
+      cm.display.lineSpace.removeChild(scrollNode);
     }
   }
 
   function scrollPosIntoView(cm, pos, end, margin) {
     if (margin == null) margin = 0;
     for (;;) {
       var changed = false, coords = cursorCoords(cm, pos);
       var endCoords = !end || end == pos ? coords : cursorCoords(cm, end);
@@ -2737,17 +2753,20 @@ window.CodeMirror = (function() {
     if (how == "smart") {
       if (!cm.doc.mode.indent) how = "prev";
       else var state = getStateBefore(cm, n);
     }
 
     var tabSize = cm.options.tabSize;
     var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize);
     var curSpaceString = line.text.match(/^\s*/)[0], indentation;
-    if (how == "smart") {
+    if (!aggressive && !/\S/.test(line.text)) {
+      indentation = 0;
+      how = "not";
+    } else if (how == "smart") {
       indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text);
       if (indentation == Pass) {
         if (!aggressive) return;
         how = "prev";
       }
     }
     if (how == "prev") {
       if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize);
@@ -2919,17 +2938,17 @@ window.CodeMirror = (function() {
       if (typeof dir != "string" && typeof dir != "number") {
         if (dir == null) dir = this.options.smartIndent ? "smart" : "prev";
         else dir = dir ? "add" : "subtract";
       }
       if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive);
     }),
     indentSelection: operation(null, function(how) {
       var sel = this.doc.sel;
-      if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how);
+      if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how, true);
       var e = sel.to.line - (sel.to.ch ? 0 : 1);
       for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how);
     }),
 
     // Fetch the parser token for a given character. Useful for hacks
     // that want to inspect the mode state (say, for completion).
     getTokenAt: function(pos, precise) {
       var doc = this.doc;
@@ -2964,21 +2983,41 @@ window.CodeMirror = (function() {
 
     getModeAt: function(pos) {
       var mode = this.doc.mode;
       if (!mode.innerMode) return mode;
       return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode;
     },
 
     getHelper: function(pos, type) {
-      if (!helpers.hasOwnProperty(type)) return;
+      return this.getHelpers(pos, type)[0];
+    },
+
+    getHelpers: function(pos, type) {
+      var found = [];
+      if (!helpers.hasOwnProperty(type)) return helpers;
       var help = helpers[type], mode = this.getModeAt(pos);
-      return mode[type] && help[mode[type]] ||
-        mode.helperType && help[mode.helperType] ||
-        help[mode.name];
+      if (typeof mode[type] == "string") {
+        if (help[mode[type]]) found.push(help[mode[type]]);
+      } else if (mode[type]) {
+        for (var i = 0; i < mode[type].length; i++) {
+          var val = help[mode[type][i]];
+          if (val) found.push(val);
+        }
+      } else if (mode.helperType && help[mode.helperType]) {
+        found.push(help[mode.helperType]);
+      } else if (help[mode.name]) {
+        found.push(help[mode.name]);
+      }
+      for (var i = 0; i < help._global.length; i++) {
+        var cur = help._global[i];
+        if (cur.pred(mode, this) && indexOf(found, cur.val) == -1)
+          found.push(cur.val);
+      }
+      return found;
     },
 
     getStateAfter: function(line, precise) {
       var doc = this.doc;
       line = clipLine(doc, line == null ? doc.first + doc.size - 1: line);
       return getStateBefore(this, line + 1, precise);
     },
 
@@ -3115,17 +3154,20 @@ window.CodeMirror = (function() {
         node.style.left = left + "px";
       }
       if (scroll)
         scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight);
     },
 
     triggerOnKeyDown: operation(null, onKeyDown),
 
-    execCommand: function(cmd) {return commands[cmd](this);},
+    execCommand: function(cmd) {
+      if (commands.hasOwnProperty(cmd))
+        return commands[cmd](this);
+    },
 
     findPosH: function(from, amount, unit, visually) {
       var dir = 1;
       if (amount < 0) { dir = -1; amount = -amount; }
       for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) {
         cur = findPosH(this.doc, cur, dir, unit, visually);
         if (cur.hitSide) break;
       }
@@ -3157,24 +3199,28 @@ window.CodeMirror = (function() {
         else coords.left = x;
         cur = findPosV(this, coords, dir, unit);
         if (cur.hitSide) break;
       }
       return cur;
     },
 
     moveV: operation(null, function(dir, unit) {
-      var sel = this.doc.sel;
-      var pos = cursorCoords(this, sel.head, "div");
-      if (sel.goalColumn != null) pos.left = sel.goalColumn;
-      var target = findPosV(this, pos, dir, unit);
-
-      if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
+      var sel = this.doc.sel, target, goal;
+      if (sel.shift || sel.extend || posEq(sel.from, sel.to)) {
+        var pos = cursorCoords(this, sel.head, "div");
+        if (sel.goalColumn != null) pos.left = sel.goalColumn;
+        target = findPosV(this, pos, dir, unit);
+        if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top);
+        goal = pos.left;
+      } else {
+        target = dir < 0 ? sel.from : sel.to;
+      }
       extendSelection(this.doc, target, target, dir);
-      sel.goalColumn = pos.left;
+      if (goal != null) sel.goalColumn = goal;
     }),
 
     toggleOverwrite: function(value) {
       if (value != null && value == this.state.overwrite) return;
       if (this.state.overwrite = !this.state.overwrite)
         this.display.cursor.className += " CodeMirror-overwrite";
       else
         this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", "");
@@ -3274,17 +3320,17 @@ window.CodeMirror = (function() {
     cm.doc.modeOption = val;
     loadMode(cm);
   }, true);
 
   option("indentUnit", 2, loadMode, true);
   option("indentWithTabs", false);
   option("smartIndent", true);
   option("tabSize", 4, function(cm) {
-    loadMode(cm);
+    resetModeState(cm);
     clearCaches(cm);
     regChange(cm);
   }, true);
   option("specialChars", /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g, function(cm, val) {
     cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
     cm.refresh();
   }, true);
   option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true);
@@ -3327,29 +3373,31 @@ window.CodeMirror = (function() {
       onBlur(cm);
       cm.display.input.blur();
       cm.display.disabled = true;
     } else {
       cm.display.disabled = false;
       if (!val) resetInput(cm, true);
     }
   });
+  option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true);
   option("dragDrop", true);
 
   option("cursorBlinkRate", 530);
   option("cursorScrollMargin", 0);
   option("cursorHeight", 1);
   option("workTime", 100);
   option("workDelay", 100);
-  option("flattenSpans", true);
+  option("flattenSpans", true, resetModeState, true);
+  option("addModeClass", false, resetModeState, true);
   option("pollInterval", 100);
   option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;});
   option("historyEventDelay", 500);
   option("viewportMargin", 10, function(cm){cm.refresh();}, true);
-  option("maxHighlightLength", 10000, function(cm){loadMode(cm); cm.refresh();}, true);
+  option("maxHighlightLength", 10000, resetModeState, true);
   option("crudeMeasuringFrom", 10000);
   option("moveInputWithCursor", true, function(cm, val) {
     if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0;
   });
 
   option("tabindex", null, function(cm, val) {
     cm.display.input.tabIndex = val || "";
   });
@@ -3396,16 +3444,19 @@ window.CodeMirror = (function() {
       var exts = modeExtensions[spec.name];
       for (var prop in exts) {
         if (!exts.hasOwnProperty(prop)) continue;
         if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop];
         modeObj[prop] = exts[prop];
       }
     }
     modeObj.name = spec.name;
+    if (spec.helperType) modeObj.helperType = spec.helperType;
+    if (spec.modeProps) for (var prop in spec.modeProps)
+      modeObj[prop] = spec.modeProps[prop];
 
     return modeObj;
   };
 
   CodeMirror.defineMode("null", function() {
     return {token: function(stream) {stream.skipToEnd();}};
   });
   CodeMirror.defineMIME("text/plain", "null");
@@ -3426,19 +3477,23 @@ window.CodeMirror = (function() {
   };
   CodeMirror.defineOption = option;
 
   var initHooks = [];
   CodeMirror.defineInitHook = function(f) {initHooks.push(f);};
 
   var helpers = CodeMirror.helpers = {};
   CodeMirror.registerHelper = function(type, name, value) {
-    if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {};
+    if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []};
     helpers[type][name] = value;
   };
+  CodeMirror.registerGlobalHelper = function(type, name, predicate, value) {
+    CodeMirror.registerHelper(type, name, value);
+    helpers[type]._global.push({pred: predicate, val: value});
+  };
 
   // UTILITIES
 
   CodeMirror.isWordChar = isWordChar;
 
   // MODE STATE HANDLING
 
   // Utility functions for working with state. Exported because modes
@@ -3533,17 +3588,19 @@ window.CodeMirror = (function() {
     delCharAfter: function(cm) {cm.deleteH(1, "char");},
     delWordBefore: function(cm) {cm.deleteH(-1, "word");},
     delWordAfter: function(cm) {cm.deleteH(1, "word");},
     delGroupBefore: function(cm) {cm.deleteH(-1, "group");},
     delGroupAfter: function(cm) {cm.deleteH(1, "group");},
     indentAuto: function(cm) {cm.indentSelection("smart");},
     indentMore: function(cm) {cm.indentSelection("add");},
     indentLess: function(cm) {cm.indentSelection("subtract");},
-    insertTab: function(cm) {cm.replaceSelection("\t", "end", "+input");},
+    insertTab: function(cm) {
+      cm.replaceSelection("\t", "end", "+input");
+    },
     defaultTab: function(cm) {
       if (cm.somethingSelected()) cm.indentSelection("add");
       else cm.replaceSelection("\t", "end", "+input");
     },
     transposeChars: function(cm) {
       var cur = cm.getCursor(), line = cm.getLine(cur.line);
       if (cur.ch > 0 && cur.ch < line.length - 1)
         cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
@@ -3706,21 +3763,22 @@ window.CodeMirror = (function() {
   // parsers more succinct.
 
   // The character stream used by a mode's parser.
   function StringStream(string, tabSize) {
     this.pos = this.start = 0;
     this.string = string;
     this.tabSize = tabSize || 8;
     this.lastColumnPos = this.lastColumnValue = 0;
+    this.lineStart = 0;
   }
 
   StringStream.prototype = {
     eol: function() {return this.pos >= this.string.length;},
-    sol: function() {return this.pos == 0;},
+    sol: function() {return this.pos == this.lineStart;},
     peek: function() {return this.string.charAt(this.pos) || undefined;},
     next: function() {
       if (this.pos < this.string.length)
         return this.string.charAt(this.pos++);
     },
     eat: function(match) {
       var ch = this.string.charAt(this.pos);
       if (typeof match == "string") var ok = ch == match;
@@ -3743,35 +3801,43 @@ window.CodeMirror = (function() {
       if (found > -1) {this.pos = found; return true;}
     },
     backUp: function(n) {this.pos -= n;},
     column: function() {
       if (this.lastColumnPos < this.start) {
         this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue);
         this.lastColumnPos = this.start;
       }
-      return this.lastColumnValue;
+      return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
     },
-    indentation: function() {return countColumn(this.string, null, this.tabSize);},
+    indentation: function() {
+      return countColumn(this.string, null, this.tabSize) -
+        (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0);
+    },
     match: function(pattern, consume, caseInsensitive) {
       if (typeof pattern == "string") {
         var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
         var substr = this.string.substr(this.pos, pattern.length);
         if (cased(substr) == cased(pattern)) {
           if (consume !== false) this.pos += pattern.length;
           return true;
         }
       } else {
         var match = this.string.slice(this.pos).match(pattern);
         if (match && match.index > 0) return null;
         if (match && consume !== false) this.pos += match[0].length;
         return match;
       }
     },
-    current: function(){return this.string.slice(this.start, this.pos);}
+    current: function(){return this.string.slice(this.start, this.pos);},
+    hideFirstChars: function(n, inner) {
+      this.lineStart += n;
+      try { return inner(); }
+      finally { this.lineStart -= n; }
+    }
   };
   CodeMirror.StringStream = StringStream;
 
   // TEXTMARKERS
 
   function TextMarker(doc, type) {
     this.lines = [];
     this.type = type;
@@ -3813,28 +3879,28 @@ window.CodeMirror = (function() {
     this.explicitlyCleared = true;
     if (this.atomic && this.doc.cantEdit) {
       this.doc.cantEdit = false;
       if (cm) reCheckSelection(cm);
     }
     if (withOp) endOperation(cm);
   };
 
-  TextMarker.prototype.find = function() {
+  TextMarker.prototype.find = function(bothSides) {
     var from, to;
     for (var i = 0; i < this.lines.length; ++i) {
       var line = this.lines[i];
       var span = getMarkedSpanFor(line.markedSpans, this);
       if (span.from != null || span.to != null) {
         var found = lineNo(line);
         if (span.from != null) from = Pos(found, span.from);
         if (span.to != null) to = Pos(found, span.to);
       }
     }
-    if (this.type == "bookmark") return from;
+    if (this.type == "bookmark" && !bothSides) return from;
     return from && {from: from, to: to};
   };
 
   TextMarker.prototype.changed = function() {
     var pos = this.find(), cm = this.doc.cm;
     if (!pos || !cm) return;
     if (this.type != "bookmark") pos = pos.from;
     var line = getLine(this.doc, pos.line);
@@ -3861,67 +3927,66 @@ window.CodeMirror = (function() {
   TextMarker.prototype.detachLine = function(line) {
     this.lines.splice(indexOf(this.lines, line), 1);
     if (!this.lines.length && this.doc.cm) {
       var op = this.doc.cm.curOp;
       (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this);
     }
   };
 
+  var nextMarkerId = 0;
+
   function markText(doc, from, to, options, type) {
     if (options && options.shared) return markTextShared(doc, from, to, options, type);
     if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type);
 
     var marker = new TextMarker(doc, type);
-    if (posLess(to, from) || posEq(from, to) && type == "range" &&
-        !(options.inclusiveLeft && options.inclusiveRight))
+    if (options) copyObj(options, marker);
+    if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false)
       return marker;
-    if (options) copyObj(options, marker);
     if (marker.replacedWith) {
       marker.collapsed = true;
       marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget");
       if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true;
     }
-    if (marker.collapsed) sawCollapsedSpans = true;
+    if (marker.collapsed) {
+      if (conflictingCollapsedRange(doc, from.line, from, to, marker) ||
+          from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker))
+        throw new Error("Inserting collapsed marker partially overlapping an existing one");
+      sawCollapsedSpans = true;
+    }
 
     if (marker.addToHistory)
       addToHistory(doc, {from: from, to: to, origin: "markText"},
                    {head: doc.sel.head, anchor: doc.sel.anchor}, NaN);
 
-    var curLine = from.line, size = 0, collapsedAtStart, collapsedAtEnd, cm = doc.cm, updateMaxLine;
+    var curLine = from.line, cm = doc.cm, updateMaxLine;
     doc.iter(curLine, to.line + 1, function(line) {
       if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine)
         updateMaxLine = true;
       var span = {from: null, to: null, marker: marker};
-      size += line.text.length;
-      if (curLine == from.line) {span.from = from.ch; size -= from.ch;}
-      if (curLine == to.line) {span.to = to.ch; size -= line.text.length - to.ch;}
-      if (marker.collapsed) {
-        if (curLine == to.line) collapsedAtEnd = collapsedSpanAt(line, to.ch);
-        if (curLine == from.line) collapsedAtStart = collapsedSpanAt(line, from.ch);
-        else updateLineHeight(line, 0);
-      }
+      if (curLine == from.line) span.from = from.ch;
+      if (curLine == to.line) span.to = to.ch;
+      if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0);
       addMarkedSpan(line, span);
       ++curLine;
     });
     if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) {
       if (lineIsHidden(doc, line)) updateLineHeight(line, 0);
     });
 
     if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); });
 
     if (marker.readOnly) {
       sawReadOnlySpans = true;
       if (doc.history.done.length || doc.history.undone.length)
         doc.clearHistory();
     }
     if (marker.collapsed) {
-      if (collapsedAtStart != collapsedAtEnd)
-        throw new Error("Inserting collapsed marker overlapping an existing one");
-      marker.size = size;
+      marker.id = ++nextMarkerId;
       marker.atomic = true;
     }
     if (cm) {
       if (updateMaxLine) cm.curOp.updateMaxLine = true;
       if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed)
         regChange(cm, from.line, to.line + 1);
       if (marker.atomic) reCheckSelection(cm);
     }
@@ -3984,33 +4049,31 @@ window.CodeMirror = (function() {
     line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span];
     span.marker.attachLine(line);
   }
 
   function markedSpansBefore(old, startCh, isInsert) {
     if (old) for (var i = 0, nw; i < old.length; ++i) {
       var span = old[i], marker = span.marker;
       var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
-      if (startsBefore ||
-          (marker.inclusiveLeft && marker.inclusiveRight || marker.type == "bookmark") &&
-          span.from == startCh && (!isInsert || !span.marker.insertLeft)) {
+      if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) {
         var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
         (nw || (nw = [])).push({from: span.from,
                                 to: endsAfter ? null : span.to,
                                 marker: marker});
       }
     }
     return nw;
   }
 
   function markedSpansAfter(old, endCh, isInsert) {
     if (old) for (var i = 0, nw; i < old.length; ++i) {
       var span = old[i], marker = span.marker;
       var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
-      if (endsAfter || marker.type == "bookmark" && span.from == endCh && (!isInsert || span.marker.insertLeft)) {
+      if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) {
         var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
         (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
                                 to: span.to == null ? null : span.to - endCh,
                                 marker: marker});
       }
     }
     return nw;
   }
@@ -4050,39 +4113,45 @@ window.CodeMirror = (function() {
             if (sameLine) (first || (first = [])).push(span);
           }
         } else {
           span.from += offset;
           if (sameLine) (first || (first = [])).push(span);
         }
       }
     }
-    if (sameLine && first) {
-      // Make sure we didn't create any zero-length spans
-      for (var i = 0; i < first.length; ++i)
-        if (first[i].from != null && first[i].from == first[i].to && first[i].marker.type != "bookmark")
-          first.splice(i--, 1);
-      if (!first.length) first = null;
-    }
+    // Make sure we didn't create any zero-length spans
+    if (first) first = clearEmptySpans(first);
+    if (last && last != first) last = clearEmptySpans(last);
 
     var newMarkers = [first];
     if (!sameLine) {
       // Fill gap with whole-line-spans
       var gap = change.text.length - 2, gapMarkers;
       if (gap > 0 && first)
         for (var i = 0; i < first.length; ++i)
           if (first[i].to == null)
             (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
       for (var i = 0; i < gap; ++i)
         newMarkers.push(gapMarkers);
       newMarkers.push(last);
     }
     return newMarkers;
   }
 
+  function clearEmptySpans(spans) {
+    for (var i = 0; i < spans.length; ++i) {
+      var span = spans[i];
+      if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false)
+        spans.splice(i--, 1);
+    }
+    if (!spans.length) return null;
+    return spans;
+  }
+
   function mergeOldSpans(doc, change) {
     var old = getOldSpans(doc, change);
     var stretched = stretchSpansOverChange(doc, change);
     if (!old) return stretched;
     if (!stretched) return old;
 
     for (var i = 0; i < old.length; ++i) {
       var oldCur = old[i], stretchCur = stretched[i];
@@ -4123,30 +4192,58 @@ window.CodeMirror = (function() {
           newParts.push({from: m.to, to: p.to});
         parts.splice.apply(parts, newParts);
         j += newParts.length - 1;
       }
     }
     return parts;
   }
 
-  function collapsedSpanAt(line, ch) {
+  function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; }
+  function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; }
+
+  function compareCollapsedMarkers(a, b) {
+    var lenDiff = a.lines.length - b.lines.length;
+    if (lenDiff != 0) return lenDiff;
+    var aPos = a.find(), bPos = b.find();
+    var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b);
+    if (fromCmp) return -fromCmp;
+    var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b);
+    if (toCmp) return toCmp;
+    return b.id - a.id;
+  }
+
+  function collapsedSpanAtSide(line, start) {
     var sps = sawCollapsedSpans && line.markedSpans, found;
     if (sps) for (var sp, i = 0; i < sps.length; ++i) {
       sp = sps[i];
-      if (!sp.marker.collapsed) continue;
-      if ((sp.from == null || sp.from < ch) &&
-          (sp.to == null || sp.to > ch) &&
-          (!found || found.width < sp.marker.width))
+      if (sp.marker.collapsed && (start ? sp.from : sp.to) == null &&
+          (!found || compareCollapsedMarkers(found, sp.marker) < 0))
         found = sp.marker;
     }
     return found;
   }
-  function collapsedSpanAtStart(line) { return collapsedSpanAt(line, -1); }
-  function collapsedSpanAtEnd(line) { return collapsedSpanAt(line, line.text.length + 1); }
+  function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); }
+  function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); }
+
+  function conflictingCollapsedRange(doc, lineNo, from, to, marker) {
+    var line = getLine(doc, lineNo);
+    var sps = sawCollapsedSpans && line.markedSpans;
+    if (sps) for (var i = 0; i < sps.length; ++i) {
+      var sp = sps[i];
+      if (!sp.marker.collapsed) continue;
+      var found = sp.marker.find(true);
+      var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker);
+      var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker);
+      if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue;
+      if (fromCmp <= 0 && (cmp(found.to, from) || extraRight(sp.marker) - extraLeft(marker)) > 0 ||
+          fromCmp >= 0 && (cmp(found.from, to) || extraLeft(sp.marker) - extraRight(marker)) < 0)
+        return true;
+    }
+  }
 
   function visualLine(doc, line) {
     var merged;
     while (merged = collapsedSpanAtStart(line))
       line = getLine(doc, merged.find().from.line);
     return line;
   }
 
@@ -4166,16 +4263,17 @@ window.CodeMirror = (function() {
       var end = span.marker.find().to, endLine = getLine(doc, end.line);
       return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker));
     }
     if (span.marker.inclusiveRight && span.to == line.text.length)
       return true;
     for (var sp, i = 0; i < line.markedSpans.length; ++i) {
       sp = line.markedSpans[i];
       if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to &&
+          (sp.to == null || sp.to != span.from) &&
           (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
           lineIsHiddenInner(doc, line, sp)) return true;
     }
   }
 
   function detachMarkedSpans(line) {
     var spans = line.markedSpans;
     if (!spans) return;
@@ -4295,16 +4393,20 @@ window.CodeMirror = (function() {
       if (stream.pos > cm.options.maxHighlightLength) {
         flattenSpans = false;
         if (forceToEnd) processLine(cm, text, state, stream.pos);
         stream.pos = text.length;
         style = null;
       } else {
         style = mode.token(stream, state);
       }
+      if (cm.options.addModeClass) {
+        var mName = CodeMirror.innerMode(mode, state).mode.name;
+        if (mName) style = "m-" + (style ? mName + " " + style : mName);
+      }
       if (!flattenSpans || curStyle != style) {
         if (curStart < stream.start) f(stream.start, curStyle);
         curStart = stream.start; curStyle = style;
       }
       stream.start = stream.pos;
     }
     while (curStart < stream.pos) {
       // Webkit seems to refuse to render text nodes longer than 57444 characters
@@ -4366,48 +4468,49 @@ window.CodeMirror = (function() {
     stream.start = stream.pos = startAt || 0;
     if (text == "" && mode.blankLine) mode.blankLine(state);
     while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) {
       mode.token(stream, state);
       stream.start = stream.pos;
     }
   }
 
-  var styleToClassCache = {};
+  var styleToClassCache = {}, styleToClassCacheWithMode = {};
   function interpretTokenStyle(style, builder) {
     if (!style) return null;
     for (;;) {
       var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/);
       if (!lineClass) break;
       style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length);
       var prop = lineClass[1] ? "bgClass" : "textClass";
       if (builder[prop] == null)
         builder[prop] = lineClass[2];
       else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop]))
         builder[prop] += " " + lineClass[2];
     }
-    return styleToClassCache[style] ||
-      (styleToClassCache[style] = "cm-" + style.replace(/ +/g, " cm-"));
+    var cache = builder.cm.options.addModeClass ? styleToClassCacheWithMode : styleToClassCache;
+    return cache[style] ||
+      (cache[style] = "cm-" + style.replace(/ +/g, " cm-"));
   }
 
   function buildLineContent(cm, realLine, measure, copyWidgets) {
     var merged, line = realLine, empty = true;
     while (merged = collapsedSpanAtStart(line))
       line = getLine(cm.doc, merged.find().from.line);
 
     var builder = {pre: elt("pre"), col: 0, pos: 0,
                    measure: null, measuredSomething: false, cm: cm,
                    copyWidgets: copyWidgets};
 
     do {
       if (line.text) empty = false;
       builder.measure = line == realLine && measure;
       builder.pos = 0;
       builder.addToken = builder.measure ? buildTokenMeasure : buildToken;
-      if ((ie || webkit) && cm.getOption("lineWrapping"))
+      if ((old_ie || webkit) && cm.getOption("lineWrapping"))
         builder.addToken = buildTokenSplitSpaces(builder.addToken);
       var next = insertLineContent(line, builder, getLineStyles(cm, line));
       if (measure && line == realLine && !builder.measuredSomething) {
         measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure));
         builder.measuredSomething = true;
       }
       if (next) line = getLine(cm.doc, next.to.line);
     } while (next);
@@ -4416,17 +4519,17 @@ window.CodeMirror = (function() {
       measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure));
     if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine))
       builder.pre.appendChild(document.createTextNode("\u00a0"));
 
     var order;
     // Work around problem with the reported dimensions of single-char
     // direction spans on IE (issue #1129). See also the comment in
     // cursorCoords.
-    if (measure && (ie || ie_gt10) && (order = getOrder(line))) {
+    if (measure && ie && (order = getOrder(line))) {
       var l = order.length - 1;
       if (order[l].from == order[l].to) --l;
       var last = order[l], prev = order[l - 1];
       if (last.from + 1 == last.to && prev && last.level < prev.level) {
         var span = measure[builder.pos - 1];
         if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure),
                                                span.nextSibling);
       }
@@ -4483,32 +4586,31 @@ window.CodeMirror = (function() {
       return builder.pre.appendChild(token);
     }
     builder.pre.appendChild(content);
   }
 
   function buildTokenMeasure(builder, text, style, startStyle, endStyle) {
     var wrapping = builder.cm.options.lineWrapping;
     for (var i = 0; i < text.length; ++i) {
-      var ch = text.charAt(i), start = i == 0;
-      if (ch >= "\ud800" && ch < "\udbff" && i < text.length - 1) {
-        ch = text.slice(i, i + 2);
-        ++i;
-      } else if (i && wrapping && spanAffectsWrapping(text, i)) {
+      var start = i == 0, to = i + 1;
+      while (to < text.length && isExtendingChar(text.charAt(to))) ++to;
+      var ch = text.slice(i, to);
+      i = to - 1;
+      if (i && wrapping && spanAffectsWrapping(text, i))
         builder.pre.appendChild(elt("wbr"));
-      }
       var old = builder.measure[builder.pos];
       var span = builder.measure[builder.pos] =
         buildToken(builder, ch, style,
                    start && startStyle, i == text.length - 1 && endStyle);
       if (old) span.leftSide = old.leftSide || old;
       // In IE single-space nodes wrap differently than spaces
       // embedded in larger text nodes, except when set to
       // white-space: normal (issue #1268).
-      if (ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
+      if (old_ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) &&
           i < text.length - 1 && !/\s/.test(text.charAt(i + 1)))
         span.style.whiteSpace = "normal";
       builder.pos += ch.length;
     }
     if (text.length) builder.measuredSomething = true;
   }
 
   function buildTokenSplitSpaces(inner) {
@@ -4566,17 +4668,17 @@ window.CodeMirror = (function() {
         for (var j = 0; j < spans.length; ++j) {
           var sp = spans[j], m = sp.marker;
           if (sp.from <= pos && (sp.to == null || sp.to > pos)) {
             if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; }
             if (m.className) spanStyle += " " + m.className;
             if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle;
             if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle;
             if (m.title && !title) title = m.title;
-            if (m.collapsed && (!collapsed || collapsed.marker.size < m.size))
+            if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
               collapsed = sp;
           } else if (sp.from > pos && nextChange > sp.from) {
             nextChange = sp.from;
           }
           if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m);
         }
         if (collapsed && (collapsed.from || 0) == pos) {
           buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos,
@@ -4899,20 +5001,21 @@ window.CodeMirror = (function() {
 
     historySize: function() {
       var hist = this.history;
       return {undo: hist.done.length, redo: hist.undone.length};
     },
     clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);},
 
     markClean: function() {
-      this.cleanGeneration = this.changeGeneration();
+      this.cleanGeneration = this.changeGeneration(true);
     },
-    changeGeneration: function() {
-      this.history.lastOp = this.history.lastOrigin = null;
+    changeGeneration: function(forceSplit) {
+      if (forceSplit)
+        this.history.lastOp = this.history.lastOrigin = null;
       return this.history.generation;
     },
     isClean: function (gen) {
       return this.history.generation == (gen || this.cleanGeneration);
     },
 
     getHistory: function() {
       return {done: copyHistoryArray(this.history.done),
@@ -4924,17 +5027,18 @@ window.CodeMirror = (function() {
       hist.undone = histData.undone.slice(0);
     },
 
     markText: function(from, to, options) {
       return markText(this, clipPos(this, from), clipPos(this, to), options, "range");
     },
     setBookmark: function(pos, options) {
       var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options),
-                      insertLeft: options && options.insertLeft};
+                      insertLeft: options && options.insertLeft,
+                      clearWhenEmpty: false};
       pos = clipPos(this, pos);
       return markText(this, pos, pos, realOpts, "bookmark");
     },
     findMarksAt: function(pos) {
       pos = clipPos(this, pos);
       var markers = [], spans = getLine(this, pos.line).markedSpans;
       if (spans) for (var i = 0; i < spans.length; ++i) {
         var span = spans[i];
@@ -5205,20 +5309,20 @@ window.CodeMirror = (function() {
       cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head;
     } else {
       // Can not be merged, start a new event.
       cur = {changes: [historyChangeFromChange(doc, change)],
              generation: hist.generation,
              anchorBefore: doc.sel.anchor, headBefore: doc.sel.head,
              anchorAfter: selAfter.anchor, headAfter: selAfter.head};
       hist.done.push(cur);
-      hist.generation = ++hist.maxGeneration;
       while (hist.done.length > hist.undoDepth)
         hist.done.shift();
     }
+    hist.generation = ++hist.maxGeneration;
     hist.lastTime = time;
     hist.lastOp = opId;
     hist.lastOrigin = change.origin;
   }
 
   function removeClearedSpans(spans) {
     if (!spans) return null;
     for (var i = 0, out; i < spans.length; ++i) {
@@ -5504,17 +5608,18 @@ window.CodeMirror = (function() {
       (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch));
   }
 
   function isEmpty(obj) {
     for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false;
     return true;
   }
 
-  var isExtendingChar = /[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\uA66F\u1DC0–\u1DFF\u20D0–\u20FF\uA670-\uA672\uA674-\uA67D\uA69F\udc00-\udfff\uFE20–\uFE2F]/;
+  var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;
+  function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); }
 
   // DOM UTILITIES
 
   function elt(tag, content, className, style) {
     var e = document.createElement(tag);
     if (className) e.className = className;
     if (style) e.style.cssText = style;
     if (typeof content == "string") setTextContent(e, content);
@@ -5577,17 +5682,17 @@ window.CodeMirror = (function() {
       return code >= 8208 && code <= 8212;
     };
   else if (webkit)
     spanAffectsWrapping = function(str, i) {
       if (i > 1 && str.charCodeAt(i - 1) == 45) {
         if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true;
         if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false;
       }
-      return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|…[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
+      return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1));
     };
 
   var knownScrollbarWidth;
   function scrollbarWidth(measure) {
     if (knownScrollbarWidth != null) return knownScrollbarWidth;
     var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll");
     removeChildrenAndAdd(measure, test);
     if (test.offsetWidth)
@@ -5645,24 +5750,24 @@ window.CodeMirror = (function() {
     return typeof e.oncopy == 'function';
   })();
 
   // KEY NAMING
 
   var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
                   19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
                   36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
-                  46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete",
-                  186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
-                  221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home",
-                  63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"};
+                  46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete",
+                  173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
+                  221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete",
+                  63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"};
   CodeMirror.keyNames = keyNames;
   (function() {
     // Number keys
-    for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
+    for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i);
     // Alphabetic keys
     for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
     // Function keys
     for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
   })();
 
   // BIDI HELPERS
 
@@ -5709,39 +5814,39 @@ window.CodeMirror = (function() {
   function compareBidiLevel(order, a, b) {
     var linedir = order[0].level;
     if (a == linedir) return true;
     if (b == linedir) return false;
     return a < b;
   }
   var bidiOther;
   function getBidiPartAt(order, pos) {
+    bidiOther = null;
     for (var i = 0, found; i < order.length; ++i) {
       var cur = order[i];
-      if (cur.from < pos && cur.to > pos) { bidiOther = null; return i; }
-      if (cur.from == pos || cur.to == pos) {
+      if (cur.from < pos && cur.to > pos) return i;
+      if ((cur.from == pos || cur.to == pos)) {
         if (found == null) {
           found = i;
         } else if (compareBidiLevel(order, cur.level, order[found].level)) {
-          bidiOther = found;
+          if (cur.from != cur.to) bidiOther = found;
           return i;
         } else {
-          bidiOther = i;
+          if (cur.from != cur.to) bidiOther = i;
           return found;
         }
       }
     }
-    bidiOther = null;
     return found;
   }
 
   function moveInLine(line, pos, dir, byUnit) {
     if (!byUnit) return pos + dir;
     do pos += dir;
-    while (pos > 0 && isExtendingChar.test(line.text.charAt(pos)));
+    while (pos > 0 && isExtendingChar(line.text.charAt(pos)));
     return pos;
   }
 
   // This is somewhat involved. It is needed in order to move
   // 'visually' through bi-directional text -- i.e., pressing left
   // should make the cursor go left, even when in RTL text. The
   // tricky part is the 'jumps', where RTL and LTR text touch each
   // other. This often requires the cursor offset to move more than
@@ -5766,17 +5871,17 @@ window.CodeMirror = (function() {
         else
           target = moveInLine(line, part.from, 1, byUnit);
       }
     }
   }
 
   function moveLogically(line, start, dir, byUnit) {
     var target = start + dir;
-    if (byUnit) while (target > 0 && isExtendingChar.test(line.text.charAt(target))) target += dir;
+    if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir;
     return target < 0 || target > line.text.length ? null : target;
   }
 
   // Bidirectional ordering algorithm
   // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm
   // that this (partially) implements.
 
   // One-char codes used for character types:
@@ -5858,17 +5963,17 @@ window.CodeMirror = (function() {
       // numbers changes to all European numbers.
       // W6. Otherwise, separators and terminators change to Other
       // Neutral.
       for (var i = 0; i < len; ++i) {
         var type = types[i];
         if (type == ",") types[i] = "N";
         else if (type == "%") {
           for (var end = i + 1; end < len && types[end] == "%"; ++end) {}
-          var replace = (i && types[i-1] == "!") || (end < len - 1 && types[end] == "1") ? "1" : "N";
+          var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N";
           for (var j = i; j < end; ++j) types[j] = replace;
           i = end - 1;
         }
       }
 
       // W7. Search backwards from each instance of a European number
       // until the first strong type (R, L, or sor) is found. If an L is
       // found, then change the type of the European number to L.
@@ -5883,17 +5988,17 @@ window.CodeMirror = (function() {
       // direction. European and Arabic numbers act as if they were R in
       // terms of their influence on neutrals. Start-of-level-run (sor)
       // and end-of-level-run (eor) are used at level run boundaries.
       // N2. Any remaining neutrals take the embedding direction.
       for (var i = 0; i < len; ++i) {
         if (isNeutral.test(types[i])) {
           for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {}
           var before = (i ? types[i-1] : outerType) == "L";
-          var after = (end < len - 1 ? types[end] : outerType) == "L";
+          var after = (end < len ? types[end] : outerType) == "L";
           var replace = before || after ? "L" : "R";
           for (var j = i; j < end; ++j) types[j] = replace;
           i = end - 1;
         }
       }
 
       // Here we depart from the documented algorithm, in order to avoid
       // building up an actual levels array. Since there are only three
@@ -5933,12 +6038,12 @@ window.CodeMirror = (function() {
         order.push({from: len, to: len, level: order[0].level});
 
       return order;
     };
   })();
 
   // THE END
 
-  CodeMirror.version = "3.20.0";
+  CodeMirror.version = "3.21.0";
 
   return CodeMirror;
 })();
--- a/browser/devtools/sourceeditor/codemirror/comment.js
+++ b/browser/devtools/sourceeditor/codemirror/comment.js
@@ -91,18 +91,19 @@
     // Try finding line comments
     var lineString = options.lineComment || mode.lineComment, lines = [];
     var pad = options.padding == null ? " " : options.padding, didSomething;
     lineComment: {
       if (!lineString) break lineComment;
       for (var i = start; i <= end; ++i) {
         var line = self.getLine(i);
         var found = line.indexOf(lineString);
+        if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
         if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
-        if (i != start && found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+        if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
         lines.push(line);
       }
       self.operation(function() {
         for (var i = start; i <= end; ++i) {
           var line = lines[i - start];
           var pos = line.indexOf(lineString), endPos = pos + lineString.length;
           if (pos < 0) continue;
           if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
@@ -119,17 +120,20 @@
     if (!startString || !endString) return false;
     var lead = options.blockCommentLead || mode.blockCommentLead;
     var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
     var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
     if (close == -1 && start != end) {
       endLine = self.getLine(--end);
       close = endLine.lastIndexOf(endString);
     }
-    if (open == -1 || close == -1) return false;
+    if (open == -1 || close == -1 ||
+        !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+        !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+      return false;
 
     self.operation(function() {
       self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
                         Pos(end, close + endString.length));
       var openEnd = open + startString.length;
       if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
       self.replaceRange("", Pos(start, open), Pos(start, openEnd));
       if (lead) for (var i = start + 1; i <= end; ++i) {
--- a/browser/devtools/sourceeditor/codemirror/css.js
+++ b/browser/devtools/sourceeditor/codemirror/css.js
@@ -1,313 +1,326 @@
 CodeMirror.defineMode("css", function(config, parserConfig) {
   "use strict";
 
   if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css");
 
-  var indentUnit = config.indentUnit || config.tabSize || 2,
-      hooks = parserConfig.hooks || {},
-      atMediaTypes = parserConfig.atMediaTypes || {},
-      atMediaFeatures = parserConfig.atMediaFeatures || {},
+  var indentUnit = config.indentUnit,
+      tokenHooks = parserConfig.tokenHooks,
+      mediaTypes = parserConfig.mediaTypes || {},
+      mediaFeatures = parserConfig.mediaFeatures || {},
       propertyKeywords = parserConfig.propertyKeywords || {},
       colorKeywords = parserConfig.colorKeywords || {},
       valueKeywords = parserConfig.valueKeywords || {},
-      allowNested = !!parserConfig.allowNested,
-      type = null;
+      fontProperties = parserConfig.fontProperties || {},
+      allowNested = parserConfig.allowNested;
 
+  var type, override;
   function ret(style, tp) { type = tp; return style; }
 
+  // Tokenizers
+
   function tokenBase(stream, state) {
     var ch = stream.next();
-    if (hooks[ch]) {
-      // result[0] is style and result[1] is type
-      var result = hooks[ch](stream, state);
+    if (tokenHooks[ch]) {
+      var result = tokenHooks[ch](stream, state);
       if (result !== false) return result;
     }
-    if (ch == "@") {stream.eatWhile(/[\w\\\-]/); return ret("def", stream.current());}
-    else if (ch == "=") ret(null, "compare");
-    else if ((ch == "~" || ch == "|") && stream.eat("=")) return ret(null, "compare");
-    else if (ch == "\"" || ch == "'") {
+    if (ch == "@") {
+      stream.eatWhile(/[\w\\\-]/);
+      return ret("def", stream.current());
+    } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) {
+      return ret(null, "compare");
+    } else if (ch == "\"" || ch == "'") {
       state.tokenize = tokenString(ch);
       return state.tokenize(stream, state);
-    }
-    else if (ch == "#") {
+    } else if (ch == "#") {
       stream.eatWhile(/[\w\\\-]/);
       return ret("atom", "hash");
-    }
-    else if (ch == "!") {
+    } else if (ch == "!") {
       stream.match(/^\s*\w*/);
       return ret("keyword", "important");
-    }
-    else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
+    } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) {
       stream.eatWhile(/[\w.%]/);
       return ret("number", "unit");
-    }
-    else if (ch === "-") {
-      if (/\d/.test(stream.peek())) {
+    } else if (ch === "-") {
+      if (/[\d.]/.test(stream.peek())) {
         stream.eatWhile(/[\w.%]/);
         return ret("number", "unit");
       } else if (stream.match(/^[^-]+-/)) {
         return ret("meta", "meta");
       }
-    }
-    else if (/[,+>*\/]/.test(ch)) {
+    } else if (/[,+>*\/]/.test(ch)) {
       return ret(null, "select-op");
-    }
-    else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
+    } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
       return ret("qualifier", "qualifier");
-    }
-    else if (ch == ":") {
-      return ret("operator", ch);
-    }
-    else if (/[;{}\[\]\(\)]/.test(ch)) {
+    } else if (/[:;{}\[\]\(\)]/.test(ch)) {
       return ret(null, ch);
-    }
-    else if (ch == "u" && stream.match("rl(")) {
+    } else if (ch == "u" && stream.match("rl(")) {
       stream.backUp(1);
       state.tokenize = tokenParenthesized;
-      return ret("property", "variable");
-    }
-    else {
+      return ret("property", "word");
+    } else if (/[\w\\\-]/.test(ch)) {
       stream.eatWhile(/[\w\\\-]/);
-      return ret("property", "variable");
+      return ret("property", "word");
+    } else {
+      return ret(null, null);
     }
   }
 
-  function tokenString(quote, nonInclusive) {
+  function tokenString(quote) {
     return function(stream, state) {
       var escaped = false, ch;
       while ((ch = stream.next()) != null) {
-        if (ch == quote && !escaped)
+        if (ch == quote && !escaped) {
+          if (quote == ")") stream.backUp(1);
           break;
+        }
         escaped = !escaped && ch == "\\";
       }
-      if (!escaped) {
-        if (nonInclusive) stream.backUp(1);
-        state.tokenize = tokenBase;
-      }
+      if (ch == quote || !escaped && quote != ")") state.tokenize = null;
       return ret("string", "string");
     };
   }
 
   function tokenParenthesized(stream, state) {
     stream.next(); // Must be '('
     if (!stream.match(/\s*[\"\']/, false))
-      state.tokenize = tokenString(")", true);
+      state.tokenize = tokenString(")");
     else
-      state.tokenize = tokenBase;
+      state.tokenize = null;
     return ret(null, "(");
   }
 
+  // Context management
+
+  function Context(type, indent, prev) {
+    this.type = type;
+    this.indent = indent;
+    this.prev = prev;
+  }
+
+  function pushContext(state, stream, type) {
+    state.context = new Context(type, stream.indentation() + indentUnit, state.context);
+    return type;
+  }
+
+  function popContext(state) {
+    state.context = state.context.prev;
+    return state.context.type;
+  }
+
+  function pass(type, stream, state) {
+    return states[state.context.type](type, stream, state);
+  }
+  function popAndPass(type, stream, state, n) {
+    for (var i = n || 1; i > 0; i--)
+      state.context = state.context.prev;
+    return pass(type, stream, state);
+  }
+
+  // Parser
+
+  function wordAsValue(stream) {
+    var word = stream.current().toLowerCase();
+    if (valueKeywords.hasOwnProperty(word))
+      override = "atom";
+    else if (colorKeywords.hasOwnProperty(word))
+      override = "keyword";
+    else
+      override = "variable";
+  }
+
+  var states = {};
+
+  states.top = function(type, stream, state) {
+    if (type == "{") {
+      return pushContext(state, stream, "block");
+    } else if (type == "}" && state.context.prev) {
+      return popContext(state);
+    } else if (type == "@media") {
+      return pushContext(state, stream, "media");
+    } else if (type == "@font-face") {
+      return "font_face_before";
+    } else if (type && type.charAt(0) == "@") {
+      return pushContext(state, stream, "at");
+    } else if (type == "hash") {
+      override = "builtin";
+    } else if (type == "word") {
+      override = "tag";
+    } else if (type == "variable-definition") {
+      return "maybeprop";
+    } else if (type == "interpolation") {
+      return pushContext(state, stream, "interpolation");
+    } else if (type == ":") {
+      return "pseudo";
+    } else if (allowNested && type == "(") {
+      return pushContext(state, stream, "params");
+    }
+    return state.context.type;
+  };
+
+  states.block = function(type, stream, state) {
+    if (type == "word") {
+      if (propertyKeywords.hasOwnProperty(stream.current().toLowerCase())) {
+        override = "property";
+        return "maybeprop";
+      } else if (allowNested) {
+        override = stream.match(/^\s*:/, false) ? "property" : "tag";
+        return "block";
+      } else {
+        override += " error";
+        return "maybeprop";
+      }
+    } else if (type == "meta") {
+      return "block";
+    } else if (!allowNested && (type == "hash" || type == "qualifier")) {
+      override = "error";
+      return "block";
+    } else {
+      return states.top(type, stream, state);
+    }
+  };
+
+  states.maybeprop = function(type, stream, state) {
+    if (type == ":") return pushContext(state, stream, "prop");
+    return pass(type, stream, state);
+  };
+
+  states.prop = function(type, stream, state) {
+    if (type == ";") return popContext(state);
+    if (type == "{" && allowNested) return pushContext(state, stream, "propBlock");
+    if (type == "}" || type == "{") return popAndPass(type, stream, state);
+    if (type == "(") return pushContext(state, stream, "parens");
+
+    if (type == "hash" && !/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
+      override += " error";
+    } else if (type == "word") {
+      wordAsValue(stream);
+    } else if (type == "interpolation") {
+      return pushContext(state, stream, "interpolation");
+    }
+    return "prop";
+  };
+
+  states.propBlock = function(type, _stream, state) {
+    if (type == "}") return popContext(state);
+    if (type == "word") { override = "property"; return "maybeprop"; }
+    return state.context.type;
+  };
+
+  states.parens = function(type, stream, state) {
+    if (type == "{" || type == "}") return popAndPass(type, stream, state);
+    if (type == ")") return popContext(state);
+    return "parens";
+  };
+
+  states.pseudo = function(type, stream, state) {
+    if (type == "word") {
+      override = "variable-3";
+      return state.context.type;
+    }
+    return pass(type, stream, state);
+  };
+
+  states.media = function(type, stream, state) {
+    if (type == "(") return pushContext(state, stream, "media_parens");
+    if (type == "}") return popAndPass(type, stream, state);
+    if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top");
+
+    if (type == "word") {
+      var word = stream.current().toLowerCase();
+      if (word == "only" || word == "not" || word == "and")
+        override = "keyword";
+      else if (mediaTypes.hasOwnProperty(word))
+        override = "attribute";
+      else if (mediaFeatures.hasOwnProperty(word))
+        override = "property";
+      else
+        override = "error";
+    }
+    return state.context.type;
+  };
+
+  states.media_parens = function(type, stream, state) {
+    if (type == ")") return popContext(state);
+    if (type == "{" || type == "}") return popAndPass(type, stream, state, 2);
+    return states.media(type, stream, state);
+  };
+
+  states.font_face_before = function(type, stream, state) {
+    if (type == "{")
+      return pushContext(state, stream, "font_face");
+    return pass(type, stream, state);
+  };
+
+  states.font_face = function(type, stream, state) {
+    if (type == "}") return popContext(state);
+    if (type == "word") {
+      if (!fontProperties.hasOwnProperty(stream.current().toLowerCase()))
+        override = "error";
+      else
+        override = "property";
+      return "maybeprop";
+    }
+    return "font_face";
+  };
+
+  states.at = function(type, stream, state) {
+    if (type == ";") return popContext(state);
+    if (type == "{" || type == "}") return popAndPass(type, stream, state);
+    if (type == "word") override = "tag";
+    else if (type == "hash") override = "builtin";
+    return "at";
+  };
+
+  states.interpolation = function(type, stream, state) {
+    if (type == "}") return popContext(state);
+    if (type == "{" || type == ";") return popAndPass(type, stream, state);
+    if (type != "variable") override = "error";
+    return "interpolation";
+  };
+
+  states.params = function(type, stream, state) {
+    if (type == ")") return popContext(state);
+    if (type == "{" || type == "}") return popAndPass(type, stream, state);
+    if (type == "word") wordAsValue(stream);
+    return "params";
+  };
+
   return {
     startState: function(base) {
-      return {tokenize: tokenBase,
-              baseIndent: base || 0,
-              stack: [],
-              lastToken: null};
+      return {tokenize: null,
+              state: "top",
+              context: new Context("top", base || 0, null)};
     },
 
     token: function(stream, state) {
-
-      // Use these terms when applicable (see http://www.xanthir.com/blog/b4E50)
-      //
-      // rule** or **ruleset:
-      // A selector + braces combo, or an at-rule.
-      //
-      // declaration block:
-      // A sequence of declarations.
-      //
-      // declaration:
-      // A property + colon + value combo.
-      //
-      // property value:
-      // The entire value of a property.
-      //
-      // component value:
-      // A single piece of a property value. Like the 5px in
-      // text-shadow: 0 0 5px blue;. Can also refer to things that are
-      // multiple terms, like the 1-4 terms that make up the background-size
-      // portion of the background shorthand.
-      //
-      // term:
-      // The basic unit of author-facing CSS, like a single number (5),
-      // dimension (5px), string ("foo"), or function. Officially defined
-      //  by the CSS 2.1 grammar (look for the 'term' production)
-      //
-      //
-      // simple selector:
-      // A single atomic selector, like a type selector, an attr selector, a
-      // class selector, etc.
-      //
-      // compound selector:
-      // One or more simple selectors without a combinator. div.example is
-      // compound, div > .example is not.
-      //
-      // complex selector:
-      // One or more compound selectors chained with combinators.
-      //
-      // combinator:
-      // The parts of selectors that express relationships. There are four
-      // currently - the space (descendant combinator), the greater-than
-      // bracket (child combinator), the plus sign (next sibling combinator),
-      // and the tilda (following sibling combinator).
-      //
-      // sequence of selectors:
-      // One or more of the named type of selector chained with commas.
-
-      state.tokenize = state.tokenize || tokenBase;
-      if (state.tokenize == tokenBase && stream.eatSpace()) return null;
-      var style = state.tokenize(stream, state);
-      if (style && typeof style != "string") style = ret(style[0], style[1]);
-
-      // Changing style returned based on context
-      var context = state.stack[state.stack.length-1];
-      if (style == "variable") {
-        if (type == "variable-definition") state.stack.push("propertyValue");
-        return state.lastToken = "variable-2";
-      } else if (style == "property") {
-        var word = stream.current().toLowerCase();
-        if (context == "propertyValue") {
-          if (valueKeywords.hasOwnProperty(word)) {
-            style = "string-2";
-          } else if (colorKeywords.hasOwnProperty(word)) {
-            style = "keyword";
-          } else {
-            style = "variable-2";
-          }
-        } else if (context == "rule") {
-          if (!propertyKeywords.hasOwnProperty(word)) {
-            style += " error";
-          }
-        } else if (context == "block") {
-          // if a value is present in both property, value, or color, the order
-          // of preference is property -> color -> value
-          if (propertyKeywords.hasOwnProperty(word)) {
-            style = "property";
-          } else if (colorKeywords.hasOwnProperty(word)) {
-            style = "keyword";
-          } else if (valueKeywords.hasOwnProperty(word)) {
-            style = "string-2";
-          } else {
-            style = "tag";
-          }
-        } else if (!context || context == "@media{") {
-          style = "tag";
-        } else if (context == "@media") {
-          if (atMediaTypes[stream.current()]) {
-            style = "attribute"; // Known attribute
-          } else if (/^(only|not)$/.test(word)) {
-            style = "keyword";
-          } else if (word == "and") {
-            style = "error"; // "and" is only allowed in @mediaType
-          } else if (atMediaFeatures.hasOwnProperty(word)) {
-            style = "error"; // Known property, should be in @mediaType(
-          } else {
-            // Unknown, expecting keyword or attribute, assuming attribute
-            style = "attribute error";
-          }
-        } else if (context == "@mediaType") {
-          if (atMediaTypes.hasOwnProperty(word)) {
-            style = "attribute";
-          } else if (word == "and") {
-            style = "operator";
-          } else if (/^(only|not)$/.test(word)) {
-            style = "error"; // Only allowed in @media
-          } else {
-            // Unknown attribute or property, but expecting property (preceded
-            // by "and"). Should be in parentheses
-            style = "error";
-          }
-        } else if (context == "@mediaType(") {
-          if (propertyKeywords.hasOwnProperty(word)) {
-            // do nothing, remains "property"
-          } else if (atMediaTypes.hasOwnProperty(word)) {
-            style = "error"; // Known property, should be in parentheses
-          } else if (word == "and") {
-            style = "operator";
-          } else if (/^(only|not)$/.test(word)) {
-            style = "error"; // Only allowed in @media
-          } else {
-            style += " error";
-          }
-        } else if (context == "@import") {
-          style = "tag";
-        } else {
-          style = "error";
-        }
-      } else if (style == "atom") {
-        if(!context || context == "@media{" || context == "block") {
-          style = "builtin";
-        } else if (context == "propertyValue") {
-          if (!/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) {
-            style += " error";
-          }
-        } else {
-          style = "error";
-        }
-      } else if (context == "@media" && type == "{") {
-        style = "error";
+      if (!state.tokenize && stream.eatSpace()) return null;
+      var style = (state.tokenize || tokenBase)(stream, state);
+      if (style && typeof style == "object") {
+        type = style[1];
+        style = style[0];
       }
-
-      // Push/pop context stack
-      if (type == "{") {
-        if (context == "@media" || context == "@mediaType") {
-          state.stack[state.stack.length-1] = "@media{";
-        }
-        else {
-          var newContext = allowNested ? "block" : "rule";
-          state.stack.push(newContext);
-        }
-      }
-      else if (type == "}") {
-        if (context == "interpolation") style = "operator";
-        // Pop off end of array until { is reached
-        while(state.stack.length){
-          var removed = state.stack.pop();
-          if(removed.indexOf("{") > -1 || removed == "block" || removed == "rule"){
-            break;
-          }
-        }
-      }
-      else if (type == "interpolation") state.stack.push("interpolation");
-      else if (type == "@media") state.stack.push("@media");
-      else if (type == "@import") state.stack.push("@import");
-      else if (context == "@media" && /\b(keyword|attribute)\b/.test(style))
-        state.stack[state.stack.length-1] = "@mediaType";
-      else if (context == "@mediaType" && stream.current() == ",")
-        state.stack[state.stack.length-1] = "@media";
-      else if (type == "(") {
-        if (context == "@media" || context == "@mediaType") {
-          // Make sure @mediaType is used to avoid error on {
-          state.stack[state.stack.length-1] = "@mediaType";
-          state.stack.push("@mediaType(");
-        }
-        else state.stack.push("(");
-      }
-      else if (type == ")") {
-        // Pop off end of array until ( is reached
-        while(state.stack.length){
-          var removed = state.stack.pop();
-          if(removed.indexOf("(") > -1){
-            break;
-          }
-        }
-      }
-      else if (type == ":" && state.lastToken == "property") state.stack.push("propertyValue");
-      else if (context == "propertyValue" && type == ";") state.stack.pop();
-      else if (context == "@import" && type == ";") state.stack.pop();
-
-      return state.lastToken = style;
+      override = style;
+      state.state = states[state.state](type, stream, state);
+      return override;
     },
 
     indent: function(state, textAfter) {
-      var n = state.stack.length;
-      if (/^\}/.test(textAfter))
-        n -= state.stack[n-1] == "propertyValue" ? 2 : 1;
-      return state.baseIndent + n * indentUnit;
+      var cx = state.context, ch = textAfter && textAfter.charAt(0);
+      var indent = cx.indent;
+      if (cx.prev &&
+          (ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "font_face") ||
+           ch == ")" && (cx.type == "parens" || cx.type == "params" || cx.type == "media_parens") ||
+           ch == "{" && (cx.type == "at" || cx.type == "media"))) {
+        indent = cx.indent - indentUnit;
+        cx = cx.prev;
+      }
+      return indent;
     },
 
     electricChars: "}",
     blockCommentStart: "/*",
     blockCommentEnd: "*/",
     fold: "brace"
   };
 });
@@ -316,33 +329,33 @@ CodeMirror.defineMode("css", function(co
   function keySet(array) {
     var keys = {};
     for (var i = 0; i < array.length; ++i) {
       keys[array[i]] = true;
     }
     return keys;
   }
 
-  var atMediaTypes = keySet([
+  var mediaTypes_ = [
     "all", "aural", "braille", "handheld", "print", "projection", "screen",
     "tty", "tv", "embossed"
-  ]);
+  ], mediaTypes = keySet(mediaTypes_);
 
-  var atMediaFeatures = keySet([
+  var mediaFeatures_ = [
     "width", "min-width", "max-width", "height", "min-height", "max-height",
     "device-width", "min-device-width", "max-device-width", "device-height",
     "min-device-height", "max-device-height", "aspect-ratio",
     "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio",
     "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color",
     "max-color", "color-index", "min-color-index", "max-color-index",
     "monochrome", "min-monochrome", "max-monochrome", "resolution",
     "min-resolution", "max-resolution", "scan", "grid"
-  ]);
+  ], mediaFeatures = keySet(mediaFeatures_);
 
-  var propertyKeywords = keySet([
+  var propertyKeywords_ = [
     "align-content", "align-items", "align-self", "alignment-adjust",
     "alignment-baseline", "anchor-point", "animation", "animation-delay",
     "animation-direction", "animation-duration", "animation-iteration-count",
     "animation-name", "animation-play-state", "animation-timing-function",
     "appearance", "azimuth", "backface-visibility", "background",
     "background-attachment", "background-clip", "background-color",
     "background-image", "background-origin", "background-position",
     "background-repeat", "background-size", "baseline-shift", "binding",
@@ -420,19 +433,19 @@ CodeMirror.defineMode("css", function(co
     "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
     "color-interpolation", "color-interpolation-filters", "color-profile",
     "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
     "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
     "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
     "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
     "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
     "glyph-orientation-vertical", "kerning", "text-anchor", "writing-mode"
-  ]);
+  ], propertyKeywords = keySet(propertyKeywords_);
 
-  var colorKeywords = keySet([
+  var colorKeywords_ = [
     "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige",
     "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
     "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
     "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
     "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
     "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
     "darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
     "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
@@ -449,19 +462,19 @@ CodeMirror.defineMode("css", function(co
     "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered",
     "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
     "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
     "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon",
     "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
     "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
     "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
     "whitesmoke", "yellow", "yellowgreen"
-  ]);
+  ], colorKeywords = keySet(colorKeywords_);
 
-  var valueKeywords = keySet([
+  var valueKeywords_ = [
     "above", "absolute", "activeborder", "activecaption", "afar",
     "after-white-space", "ahead", "alias", "all", "all-scroll", "alternate",
     "always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
     "arabic-indic", "armenian", "asterisks", "auto", "avoid", "avoid-column", "avoid-page",
     "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
     "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
     "both", "bottom", "break", "break-all", "break-word", "button", "button-bevel",
     "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian",
@@ -534,106 +547,137 @@ CodeMirror.defineMode("css", function(co
     "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
     "transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
     "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
     "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
     "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
     "visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
     "window", "windowframe", "windowtext", "x-large", "x-small", "xor",
     "xx-large", "xx-small"
-  ]);
+  ], valueKeywords = keySet(valueKeywords_);
+
+  var fontProperties_ = [
+    "font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
+    "font-stretch", "font-weight", "font-style"
+  ], fontProperties = keySet(fontProperties_);
+
+  var allWords = mediaTypes_.concat(mediaFeatures_).concat(propertyKeywords_).concat(colorKeywords_).concat(valueKeywords_);
+  CodeMirror.registerHelper("hintWords", "css", allWords);
 
   function tokenCComment(stream, state) {
     var maybeEnd = false, ch;
     while ((ch = stream.next()) != null) {
       if (maybeEnd && ch == "/") {
         state.tokenize = null;
         break;
       }
       maybeEnd = (ch == "*");
     }
     return ["comment", "comment"];
   }
 
+  function tokenSGMLComment(stream, state) {
+    if (stream.skipTo("-->")) {
+      stream.match("-->");
+      state.tokenize = null;
+    } else {
+      stream.skipToEnd();
+    }
+    return ["comment", "comment"];
+  }
+
   CodeMirror.defineMIME("text/css", {
-    atMediaTypes: atMediaTypes,
-    atMediaFeatures: atMediaFeatures,
+    mediaTypes: mediaTypes,
+    mediaFeatures: mediaFeatures,
     propertyKeywords: propertyKeywords,
     colorKeywords: colorKeywords,
     valueKeywords: valueKeywords,
-    hooks: {
+    fontProperties: fontProperties,
+    tokenHooks: {
       "<": function(stream, state) {
-        function tokenSGMLComment(stream, state) {
-          var dashes = 0, ch;
-          while ((ch = stream.next()) != null) {
-            if (dashes >= 2 && ch == ">") {
-              state.tokenize = null;
-              break;
-            }
-            dashes = (ch == "-") ? dashes + 1 : 0;
-          }
-          return ["comment", "comment"];
-        }
-        if (stream.eat("!")) {
-          state.tokenize = tokenSGMLComment;
-          return tokenSGMLComment(stream, state);
-        }
+        if (!stream.match("!--")) return false;
+        state.tokenize = tokenSGMLComment;
+        return tokenSGMLComment(stream, state);
       },
       "/": function(stream, state) {
-        if (stream.eat("*")) {
-          state.tokenize = tokenCComment;
-          return tokenCComment(stream, state);
-        }
-        return false;
+        if (!stream.eat("*")) return false;
+        state.tokenize = tokenCComment;
+        return tokenCComment(stream, state);
       }
     },
     name: "css"
   });
 
   CodeMirror.defineMIME("text/x-scss", {
-    atMediaTypes: atMediaTypes,
-    atMediaFeatures: atMediaFeatures,
+    mediaTypes: mediaTypes,
+    mediaFeatures: mediaFeatures,
     propertyKeywords: propertyKeywords,
     colorKeywords: colorKeywords,
     valueKeywords: valueKeywords,
+    fontProperties: fontProperties,
     allowNested: true,
-    hooks: {
-      ":": function(stream) {
-        if (stream.match(/\s*{/)) {
-          return [null, "{"];
-        }
-        return false;
-      },
-      "$": function(stream) {
-        stream.match(/^[\w-]+/);
-        if (stream.peek() == ":") {
-          return ["variable", "variable-definition"];
-        }
-        return ["variable", "variable"];
-      },
-      ",": function(stream, state) {
-        if (state.stack[state.stack.length - 1] == "propertyValue" && stream.match(/^ *\$/, false)) {
-          return ["operator", ";"];
-        }
-      },
+    tokenHooks: {
       "/": function(stream, state) {
         if (stream.eat("/")) {
           stream.skipToEnd();
           return ["comment", "comment"];
         } else if (stream.eat("*")) {
           state.tokenize = tokenCComment;
           return tokenCComment(stream, state);
         } else {
           return ["operator", "operator"];
         }
       },
+      ":": function(stream) {
+        if (stream.match(/\s*{/))
+          return [null, "{"];
+        return false;
+      },
+      "$": function(stream) {
+        stream.match(/^[\w-]+/);
+        if (stream.match(/^\s*:/, false))
+          return ["variable-2", "variable-definition"];
+        return ["variable-2", "variable"];
+      },
       "#": function(stream) {
-        if (stream.eat("{")) {
-          return ["operator", "interpolation"];
-        } else {
-          stream.eatWhile(/[\w\\\-]/);
-          return ["atom", "hash"];
-        }
+        if (!stream.eat("{")) return false;
+        return [null, "interpolation"];
       }
     },
-    name: "css"
+    name: "css",
+    helperType: "scss"
+  });
+
+  CodeMirror.defineMIME("text/x-less", {
+    mediaTypes: mediaTypes,
+    mediaFeatures: mediaFeatures,
+    propertyKeywords: propertyKeywords,
+    colorKeywords: colorKeywords,
+    valueKeywords: valueKeywords,
+    fontProperties: fontProperties,
+    allowNested: true,
+    tokenHooks: {
+      "/": function(stream, state) {
+        if (stream.eat("/")) {
+          stream.skipToEnd();
+          return ["comment", "comment"];
+        } else if (stream.eat("*")) {
+          state.tokenize = tokenCComment;
+          return tokenCComment(stream, state);
+        } else {
+          return ["operator", "operator"];
+        }
+      },
+      "@": function(stream) {
+        if (stream.match(/^(charset|document|font-face|import|keyframes|media|namespace|page|supports)\b/, false)) return false;
+        stream.eatWhile(/[\w\\\-]/);
+        if (stream.match(/^\s*:/, false))
+          return ["variable-2", "variable-definition"];
+        return ["variable-2", "variable"];
+      },
+      "&": function() {
+        return ["atom", "atom"];
+      }
+    },
+    name: "css",
+    helperType: "less"
   });
 })();
--- a/browser/devtools/sourceeditor/codemirror/dialog/dialog.js
+++ b/browser/devtools/sourceeditor/codemirror/dialog/dialog.js
@@ -30,16 +30,17 @@
     var closed = false, me = this;
     function close() {
       if (closed) return;
       closed = true;
       dialog.parentNode.removeChild(dialog);
     }
     var inp = dialog.getElementsByTagName("input")[0], button;
     if (inp) {
+      if (options && options.value) inp.value = options.value;
       CodeMirror.on(inp, "keydown", function(e) {
         if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
         if (e.keyCode == 13 || e.keyCode == 27) {
           CodeMirror.e_stop(e);
           close();
           me.focus();
           if (e.keyCode == 13) callback(inp.value);
         }
--- a/browser/devtools/sourceeditor/codemirror/fold/comment-fold.js
+++ b/browser/devtools/sourceeditor/codemirror/fold/comment-fold.js
@@ -1,9 +1,11 @@
-CodeMirror.registerHelper("fold", "comment", function(cm, start) {
+CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
+  return mode.blockCommentStart && mode.blockCommentEnd;
+}, function(cm, start) {
   var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
   if (!startToken || !endToken) return;
   var line = start.line, lineText = cm.getLine(line);
 
   var startCh;
   for (var at = start.ch, pass = 0;;) {
     var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
     if (found == -1) {
--- a/browser/devtools/sourceeditor/codemirror/fold/foldcode.js
+++ b/browser/devtools/sourceeditor/codemirror/fold/foldcode.js
@@ -1,15 +1,14 @@
 (function() {
   "use strict";
 
   function doFold(cm, pos, options, force) {
     var finder = options && (options.call ? options : options.rangeFinder);
-    if (!finder) finder = cm.getHelper(pos, "fold");
-    if (!finder) return;
+    if (!finder) finder = CodeMirror.fold.auto;
     if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
     var minSize = options && options.minFoldSize || 0;
 
     function getRange(allowFolded) {
       var range = finder(cm, pos);
       if (!range || range.to.line - range.from.line < minSize) return null;
       var marks = cm.findMarksAt(range.from);
       for (var i = 0; i < marks.length; ++i) {
@@ -58,18 +57,30 @@
     return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
   };
 
   // New-style interface
   CodeMirror.defineExtension("foldCode", function(pos, options, force) {
     doFold(this, pos, options, force);
   });
 
+  CodeMirror.commands.fold = function(cm) {
+    cm.foldCode(cm.getCursor());
+  };
+
   CodeMirror.registerHelper("fold", "combine", function() {
     var funcs = Array.prototype.slice.call(arguments, 0);
     return function(cm, start) {
       for (var i = 0; i < funcs.length; ++i) {
         var found = funcs[i](cm, start);
         if (found) return found;
       }
     };
   });
+
+  CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+    var helpers = cm.getHelpers(start, "fold");
+    for (var i = 0; i < helpers.length; i++) {
+      var cur = helpers[i](cm, start);
+      if (cur) return cur;
+    }
+  });
 })();
--- a/browser/devtools/sourceeditor/codemirror/fold/foldgutter.js
+++ b/browser/devtools/sourceeditor/codemirror/fold/foldgutter.js
@@ -57,17 +57,17 @@
 
   function updateFoldInfo(cm, from, to) {
     var opts = cm.state.foldGutter.options, cur = from;
     cm.eachLine(from, to, function(line) {
       var mark = null;
       if (isFolded(cm, cur)) {
         mark = marker(opts.indicatorFolded);
       } else {
-        var pos = Pos(cur, 0), func = opts.rangeFinder || cm.getHelper(pos, "fold");
+        var pos = Pos(cur, 0), func = opts.rangeFinder || CodeMirror.fold.auto;
         var range = func && func(cm, pos);
         if (range && range.from.line + 1 < range.to.line)
           mark = marker(opts.indicatorOpen);
       }
       cm.setGutterMarker(line, opts.gutter, mark);
       ++cur;
     });
   }
--- a/browser/devtools/sourceeditor/codemirror/fold/xml-fold.js
+++ b/browser/devtools/sourceeditor/codemirror/fold/xml-fold.js
@@ -159,9 +159,15 @@
     for (;;) {
       var open = findMatchingOpen(iter);
       if (!open) break;
       var forward = new Iter(cm, pos.line, pos.ch, range);
       var close = findMatchingClose(forward, open.tag);
       if (close) return {open: open, close: close};
     }
   };
+
+  // Used by addon/edit/closetag.js
+  CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+    var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+    return !!findMatchingClose(iter, name);
+  };
 })();
--- a/browser/devtools/sourceeditor/codemirror/htmlmixed.js
+++ b/browser/devtools/sourceeditor/codemirror/htmlmixed.js
@@ -88,17 +88,15 @@ CodeMirror.defineMode("htmlmixed", funct
       if (!state.localMode || /^\s*<\//.test(textAfter))
         return htmlMode.indent(state.htmlState, textAfter);
       else if (state.localMode.indent)
         return state.localMode.indent(state.localState, textAfter);
       else
         return CodeMirror.Pass;
     },
 
-    electricChars: "/{}:",
-
     innerMode: function(state) {
       return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode};
     }
   };
 }, "xml", "javascript", "css");
 
 CodeMirror.defineMIME("text/html", "htmlmixed");
--- a/browser/devtools/sourceeditor/codemirror/javascript.js
+++ b/browser/devtools/sourceeditor/codemirror/javascript.js
@@ -10,17 +10,17 @@ CodeMirror.defineMode("javascript", func
 
   var keywords = function(){
     function kw(type) {return {type: type, style: "keyword"};}
     var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
     var operator = kw("operator"), atom = {type: "atom", style: "atom"};
 
     var jsKeywords = {
       "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
-      "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C,
+      "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C,
       "var": kw("var"), "const": kw("var"), "let": kw("var"),
       "function": kw("function"), "catch": kw("catch"),
       "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
       "in": operator, "typeof": operator, "instanceof": operator,
       "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
       "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"),
       "yield": C, "export": kw("export"), "import": kw("import"), "extends": C
     };
@@ -49,24 +49,26 @@ CodeMirror.defineMode("javascript", func
       }
     }
 
     return jsKeywords;
   }();
 
   var isOperatorChar = /[+\-*&%=<>!?|~^]/;
 
-  function nextUntilUnescaped(stream, end) {
-    var escaped = false, next;
+  function readRegexp(stream) {
+    var escaped = false, next, inSet = false;
     while ((next = stream.next()) != null) {
-      if (next == end && !escaped)
-        return false;
+      if (!escaped) {
+        if (next == "/" && !inSet) return;
+        if (next == "[") inSet = true;
+        else if (inSet && next == "]") inSet = false;
+      }
       escaped = !escaped && next == "\\";
     }
-    return escaped;
   }
 
   // Used as scratch variables to communicate multiple values without
   // consing up tons of objects.
   var type, content;
   function ret(tp, style, cont) {
     type = tp; content = cont;
     return style;
@@ -78,60 +80,64 @@ CodeMirror.defineMode("javascript", func
       return state.tokenize(stream, state);
     } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
       return ret("number", "number");
     } else if (ch == "." && stream.match("..")) {
       return ret("spread", "meta");
     } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
       return ret(ch);
     } else if (ch == "=" && stream.eat(">")) {
-      return ret("=>");
+      return ret("=>", "operator");
     } else if (ch == "0" && stream.eat(/x/i)) {
       stream.eatWhile(/[\da-f]/i);
       return ret("number", "number");
     } else if (/\d/.test(ch)) {
       stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
       return ret("number", "number");
     } else if (ch == "/") {
       if (stream.eat("*")) {
         state.tokenize = tokenComment;
         return tokenComment(stream, state);
       } else if (stream.eat("/")) {
         stream.skipToEnd();
         return ret("comment", "comment");
       } else if (state.lastType == "operator" || state.lastType == "keyword c" ||
                state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) {
-        nextUntilUnescaped(stream, "/");
+        readRegexp(stream);
         stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla
         return ret("regexp", "string-2");
       } else {
         stream.eatWhile(isOperatorChar);
-        return ret("operator", null, stream.current());
+        return ret("operator", "operator", stream.current());
       }
     } else if (ch == "`") {
       state.tokenize = tokenQuasi;
       return tokenQuasi(stream, state);
     } else if (ch == "#") {
       stream.skipToEnd();
       return ret("error", "error");
     } else if (isOperatorChar.test(ch)) {
       stream.eatWhile(isOperatorChar);
-      return ret("operator", null, stream.current());
+      return ret("operator", "operator", stream.current());
     } else {
       stream.eatWhile(/[\w\$_]/);
       var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
       return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
                      ret("variable", "variable", word);
     }
   }
 
   function tokenString(quote) {
     return function(stream, state) {
-      if (!nextUntilUnescaped(stream, quote))
-        state.tokenize = tokenBase;
+      var escaped = false, next;
+      while ((next = stream.next()) != null) {
+        if (next == quote && !escaped) break;
+        escaped = !escaped && next == "\\";
+      }
+      if (!escaped) state.tokenize = tokenBase;
       return ret("string", "string");
     };
   }
 
   function tokenComment(stream, state) {
     var maybeEnd = false, ch;
     while (ch = stream.next()) {
       if (ch == "/" && maybeEnd) {
@@ -299,17 +305,17 @@ CodeMirror.defineMode("javascript", func
   function statement(type, value) {
     if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
     if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
     if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
     if (type == "{") return cont(pushlex("}"), block, poplex);
     if (type == ";") return cont();
     if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse);
     if (type == "function") return cont(functiondef);
-    if (type == "for") return cont(pushlex("form"), forspec, poplex, statement, poplex);
+    if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
     if (type == "variable") return cont(pushlex("stat"), maybelabel);
     if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
                                       block, poplex, poplex);
     if (type == "case") return cont(expression, expect(":"));
     if (type == "default") return cont(expect(":"));
     if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
                                      statement, poplex, popcontext);
     if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex);
@@ -322,28 +328,28 @@ CodeMirror.defineMode("javascript", func
     return expressionInner(type, false);
   }
   function expressionNoComma(type) {
     return expressionInner(type, true);
   }
   function expressionInner(type, noComma) {
     if (cx.state.fatArrowAt == cx.stream.start) {
       var body = noComma ? arrowBodyNoComma : arrowBody;
-      if (type == "(") return cont(pushcontext, commasep(pattern, ")"), expect("=>"), body, popcontext);
+      if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
       else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
     }
 
     var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
     if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
     if (type == "function") return cont(functiondef);
     if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
     if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
     if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
-    if (type == "[") return cont(pushlex("]"), expressionNoComma, maybeArrayComprehension, poplex, maybeop);
-    if (type == "{") return cont(commasep(objprop, "}"), maybeop);
+    if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
+    if (type == "{") return contCommasep(objprop, "}", null, maybeop);
     return cont();
   }
   function maybeexpression(type) {
     if (type.match(/[;\}\)\],]/)) return pass();
     return pass(expression);
   }
   function maybeexpressionNoComma(type) {
     if (type.match(/[;\}\)\],]/)) return pass();
@@ -360,22 +366,21 @@ CodeMirror.defineMode("javascript", func
     if (value == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
     if (type == "operator") {
       if (/\+\+|--/.test(value)) return cont(me);
       if (value == "?") return cont(expression, expect(":"), expr);
       return cont(expr);
     }
     if (type == "quasi") { cx.cc.push(me); return quasi(value); }
     if (type == ";") return;
-    if (type == "(") return cont(commasep(expressionNoComma, ")", "call"), me);
+    if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
     if (type == ".") return cont(property, me);
     if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
   }
   function quasi(value) {
-    if (!value) debugger;
     if (value.slice(value.length - 2) != "${") return cont();
     return cont(expression, continueQuasi);
   }
   function continueQuasi(type) {
     if (type == "}") {
       cx.marked = "string-2";
       cx.state.tokenize = tokenQuasi;
       return cont();
@@ -413,49 +418,53 @@ CodeMirror.defineMode("javascript", func
     if (type != "variable") return pass(afterprop);
     cx.marked = "property";
     return cont(functiondef);
   }
   function afterprop(type) {
     if (type == ":") return cont(expressionNoComma);
     if (type == "(") return pass(functiondef);
   }
-  function commasep(what, end, info) {
+  function commasep(what, end) {
     function proceed(type) {
       if (type == ",") {
         var lex = cx.state.lexical;
         if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
         return cont(what, proceed);
       }
       if (type == end) return cont();
       return cont(expect(end));
     }
     return function(type) {
       if (type == end) return cont();
-      if (info === false) return pass(what, proceed);
-      return pass(pushlex(end, info), what, proceed, poplex);
+      return pass(what, proceed);
     };
   }
+  function contCommasep(what, end, info) {
+    for (var i = 3; i < arguments.length; i++)
+      cx.cc.push(arguments[i]);
+    return cont(pushlex(end, info), commasep(what, end), poplex);
+  }
   function block(type) {
     if (type == "}") return cont();
     return pass(statement, block);
   }
   function maybetype(type) {
     if (isTS && type == ":") return cont(typedef);
   }
   function typedef(type) {
     if (type == "variable"){cx.marked = "variable-3"; return cont();}
   }
   function vardef() {
     return pass(pattern, maybetype, maybeAssign, vardefCont);
   }
   function pattern(type, value) {
     if (type == "variable") { register(value); return cont(); }
-    if (type == "[") return cont(commasep(pattern, "]"));
-    if (type == "{") return cont(commasep(proppattern, "}"));
+    if (type == "[") return contCommasep(pattern, "]");
+    if (type == "{") return contCommasep(proppattern, "}");
   }
   function proppattern(type, value) {
     if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
       register(value);
       return cont(maybeAssign);
     }
     if (type == "variable") cx.marked = "property";
     return cont(expect(":"), pattern, maybeAssign);
@@ -465,17 +474,17 @@ CodeMirror.defineMode("javascript", func
   }
   function vardefCont(type) {
     if (type == ",") return cont(vardef);
   }
   function maybeelse(type, value) {
     if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex);
   }
   function forspec(type) {
-    if (type == "(") return cont(pushlex(")"), forspec1, expect(")"));
+    if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
   }
   function forspec1(type) {
     if (type == "var") return cont(vardef, expect(";"), forspec2);
     if (type == ";") return cont(forspec2);
     if (type == "variable") return cont(formaybeinof);
     return pass(expression, expect(";"), forspec2);
   }
   function formaybeinof(_type, value) {
@@ -488,56 +497,60 @@ CodeMirror.defineMode("javascript", func
     return pass(expression, expect(";"), forspec3);
   }
   function forspec3(type) {
     if (type != ")") cont(expression);
   }
   function functiondef(type, value) {
     if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
     if (type == "variable") {register(value); return cont(functiondef);}
-    if (type == "(") return cont(pushcontext, commasep(funarg, ")"), statement, popcontext);
+    if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext);
   }
   function funarg(type) {
     if (type == "spread") return cont(funarg);
     return pass(pattern, maybetype);
   }
   function className(type, value) {
     if (type == "variable") {register(value); return cont(classNameAfter);}
   }
   function classNameAfter(_type, value) {
     if (value == "extends") return cont(expression);
   }
   function objlit(type) {
-    if (type == "{") return cont(commasep(objprop, "}"));
+    if (type == "{") return contCommasep(objprop, "}");
   }
   function afterModule(type, value) {
     if (type == "string") return cont(statement);
     if (type == "variable") { register(value); return cont(maybeFrom); }
   }
   function afterExport(_type, value) {
     if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
     if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
     return pass(statement);
   }
   function afterImport(type) {
     if (type == "string") return cont();
     return pass(importSpec, maybeFrom);
   }
   function importSpec(type, value) {
-    if (type == "{") return cont(commasep(importSpec, "}"));
+    if (type == "{") return contCommasep(importSpec, "}");
     if (type == "variable") register(value);
     return cont();
   }
   function maybeFrom(_type, value) {
     if (value == "from") { cx.marked = "keyword"; return cont(expression); }
   }
+  function arrayLiteral(type) {
+    if (type == "]") return cont();
+    return pass(expressionNoComma, maybeArrayComprehension);
+  }
   function maybeArrayComprehension(type) {
-    if (type == "for") return pass(comprehension);
-    if (type == ",") return cont(commasep(expressionNoComma, "]", false));
-    return pass(commasep(expressionNoComma, "]", false));
+    if (type == "for") return pass(comprehension, expect("]"));
+    if (type == ",") return cont(commasep(expressionNoComma, "]"));
+    return pass(commasep(expressionNoComma, "]"));
   }
   function comprehension(type) {
     if (type == "for") return cont(forspec, comprehension);
     if (type == "if") return cont(expression, comprehension);
   }
 
   // Interface
 
--- a/browser/devtools/sourceeditor/codemirror/keymap/emacs.js
+++ b/browser/devtools/sourceeditor/codemirror/keymap/emacs.js
@@ -196,16 +196,21 @@
   // Utilities
 
   function setMark(cm) {
     cm.setCursor(cm.getCursor());
     cm.setExtending(true);
     cm.on("change", function() { cm.setExtending(false); });
   }
 
+  function clearMark(cm) {
+    cm.setExtending(false);
+    cm.setCursor(cm.getCursor());
+  }
+
   function getInput(cm, msg, f) {
     if (cm.openDialog)
       cm.openDialog(msg + ": <input type=\"text\" style=\"width: 10em\"/>", f, {bottom: true});
     else
       f(prompt(msg, ""));
   }
 
   function operateOnWord(cm, op) {
@@ -229,31 +234,37 @@
           stack.push("{");
         else if (/[\(\{\[]/.test(ch) && (!stack.length || stack.pop() != ch))
           return cm.extendSelection(Pos(line, i));
       }
       --line; ch = null;
     }
   }
 
+  function quit(cm) {
+    cm.execCommand("clearSearch");
+    clearMark(cm);
+  }
+
   // Actual keymap
 
   var keyMap = CodeMirror.keyMap.emacs = {
     "Ctrl-W": function(cm) {kill(cm, cm.getCursor("start"), cm.getCursor("end"));},
     "Ctrl-K": repeated(function(cm) {
       var start = cm.getCursor(), end = cm.clipPos(Pos(start.line));
       var text = cm.getRange(start, end);
       if (!/\S/.test(text)) {
         text += "\n";
         end = Pos(start.line + 1, 0);
       }
       kill(cm, start, end, true, text);
     }),
     "Alt-W": function(cm) {
       addToRing(cm.getSelection());
+      clearMark(cm);
     },
     "Ctrl-Y": function(cm) {
       var start = cm.getCursor();
       cm.replaceRange(getFromRing(getPrefix(cm)), start, start, "paste");
       cm.setSelection(start, cm.getCursor());
     },
     "Alt-Y": function(cm) {cm.replaceSelection(popFromRing());},
 
@@ -329,17 +340,17 @@
       operateOnWord(cm, function(w) { return w.toLowerCase(); });
     }),
 
     "Alt-;": "toggleComment",
 
     "Ctrl-/": repeated("undo"), "Shift-Ctrl--": repeated("undo"),
     "Ctrl-Z": repeated("undo"), "Cmd-Z": repeated("undo"),
     "Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
-    "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": "clearSearch", "Shift-Alt-5": "replace",
+    "Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
     "Alt-/": "autocomplete",
     "Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
 
     "Alt-G": function(cm) {cm.setOption("keyMap", "emacs-Alt-G");},
     "Ctrl-X": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-X");},
     "Ctrl-Q": function(cm) {cm.setOption("keyMap", "emacs-Ctrl-Q");},
     "Ctrl-U": addPrefixMap
   };
--- a/browser/devtools/sourceeditor/codemirror/keymap/vim.js
+++ b/browser/devtools/sourceeditor/codemirror/keymap/vim.js
@@ -79,16 +79,17 @@
     { keys: ['s'], type: 'keyToKey', toKeys: ['c', 'l'], context: 'normal' },
     { keys: ['s'], type: 'keyToKey', toKeys: ['x', 'i'], context: 'visual'},
     { keys: ['S'], type: 'keyToKey', toKeys: ['c', 'c'], context: 'normal' },
     { keys: ['S'], type: 'keyToKey', toKeys: ['d', 'c', 'c'], context: 'visual' },
     { keys: ['<Home>'], type: 'keyToKey', toKeys: ['0'] },
     { keys: ['<End>'], type: 'keyToKey', toKeys: ['$'] },
     { keys: ['<PageUp>'], type: 'keyToKey', toKeys: ['<C-b>'] },
     { keys: ['<PageDown>'], type: 'keyToKey', toKeys: ['<C-f>'] },
+    { keys: ['<CR>'], type: 'keyToKey', toKeys: ['j', '^'], context: 'normal' },
     // Motions
     { keys: ['H'], type: 'motion',
         motion: 'moveToTopLine',
         motionArgs: { linewise: true, toJumplist: true }},
     { keys: ['M'], type: 'motion',
         motion: 'moveToMiddleLine',
         motionArgs: { linewise: true, toJumplist: true }},
     { keys: ['L'], type: 'motion',
@@ -242,16 +243,22 @@
     { keys: ['~'], type: 'operatorMotion',
         operator: 'swapcase', operatorArgs: { shouldMoveCursor: true },
         motion: 'moveByCharacters', motionArgs: { forward: true }},
     // Actions
     { keys: ['<C-i>'], type: 'action', action: 'jumpListWalk',
         actionArgs: { forward: true }},
     { keys: ['<C-o>'], type: 'action', action: 'jumpListWalk',
         actionArgs: { forward: false }},
+    { keys: ['<C-e>'], type: 'action',
+        action: 'scroll',
+        actionArgs: { forward: true, linewise: true }},
+    { keys: ['<C-y>'], type: 'action',
+        action: 'scroll',
+        actionArgs: { forward: false, linewise: true }},
     { keys: ['a'], type: 'action', action: 'enterInsertMode', isEdit: true,
         actionArgs: { insertAt: 'charAfter' }},
     { keys: ['A'], type: 'action', action: 'enterInsertMode', isEdit: true,
         actionArgs: { insertAt: 'eol' }},
     { keys: ['i'], type: 'action', action: 'enterInsertMode', isEdit: true,
         actionArgs: { insertAt: 'inplace' }},
     { keys: ['I'], type: 'action', action: 'enterInsertMode', isEdit: true,
         actionArgs: { insertAt: 'firstNonBlank' }},
@@ -319,34 +326,50 @@
     // Ex command
     { keys: [':'], type: 'ex' }
   ];
 
   var Vim = function() {
     CodeMirror.defineOption('vimMode', false, function(cm, val) {
       if (val) {
         cm.setOption('keyMap', 'vim');
+        cm.setOption('disableInput', true);
         CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
         cm.on('beforeSelectionChange', beforeSelectionChange);
         maybeInitVimState(cm);
+        CodeMirror.on(cm.getInputField(), 'paste', getOnPasteFn(cm));
       } else if (cm.state.vim) {
         cm.setOption('keyMap', 'default');
+        cm.setOption('disableInput', false);
         cm.off('beforeSelectionChange', beforeSelectionChange);
+        CodeMirror.off(cm.getInputField(), 'paste', getOnPasteFn(cm));
         cm.state.vim = null;
       }
     });
     function beforeSelectionChange(cm, cur) {
       var vim = cm.state.vim;
       if (vim.insertMode || vim.exMode) return;
 
       var head = cur.head;
       if (head.ch && head.ch == cm.doc.getLine(head.line).length) {
         head.ch--;
       }
     }
+    function getOnPasteFn(cm) {
+      var vim = cm.state.vim;
+      if (!vim.onPasteFn) {
+        vim.onPasteFn = function() {
+          if (!vim.insertMode) {
+            cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
+            actions.enterInsertMode(cm, {}, vim);
+          }
+        };
+      }
+      return vim.onPasteFn;
+    }
 
     var numberRegex = /[\d]/;
     var wordRegexp = [(/\w/), (/[^\w\s]/)], bigWordRegexp = [(/\S/)];
     function makeKeyRange(start, size) {
       var keys = [];
       for (var i = start; i < start + size; i++) {
         keys.push(String.fromCharCode(i));
       }
@@ -544,19 +567,19 @@
       getVimGlobalState_: function() {
         return vimGlobalState;
       },
 
       // Testing hook.
       maybeInitVimState_: maybeInitVimState,
 
       InsertModeKey: InsertModeKey,
-      map: function(lhs, rhs) {
+      map: function(lhs, rhs, ctx) {
         // Add user defined key bindings.
-        exCommandDispatcher.map(lhs, rhs);
+        exCommandDispatcher.map(lhs, rhs, ctx);
       },
       defineEx: function(name, prefix, func){
         if (name.indexOf(prefix) !== 0) {
           throw new Error('(Vim.defineEx) "'+prefix+'" is not a prefix of "'+name+'", command not registered');
         }
         exCommands[name]=func;
         exCommandDispatcher.commandMap_[prefix]={name:name, shortName:prefix, type:'api'};
       },
@@ -828,21 +851,27 @@
           // Clear the buffer since there were no matches.
           inputState.keyBuffer = [];
           return null;
         } else if (matchedCommands.length == 1) {
           return getFullyMatchedCommandOrNull(matchedCommands[0]);
         } else {
           // Find the best match in the list of matchedCommands.
           var context = vim.visualMode ? 'visual' : 'normal';
-          var bestMatch = matchedCommands[0]; // Default to first in the list.
+          var bestMatch; // Default to first in the list.
           for (var i = 0; i < matchedCommands.length; i++) {
-            if (matchedCommands[i].context == context) {
-              bestMatch = matchedCommands[i];
+            var current = matchedCommands[i];
+            if (current.context == context) {
+              bestMatch = current;
               break;
+            } else if (!bestMatch && !current.context) {
+              // Only set an imperfect match to best match if no best match is
+              // set and the imperfect match is not restricted to another
+              // context.
+              bestMatch = current;
             }
           }
           return getFullyMatchedCommandOrNull(bestMatch);
         }
       },
       processCommand: function(cm, vim, command) {
         vim.inputState.repeatOverride = command.repeatOverride;
         switch (command.type) {
@@ -1631,16 +1660,53 @@
         var forward = actionArgs.forward;
         var jumpList = vimGlobalState.jumpList;
 
         var mark = jumpList.move(cm, forward ? repeat : -repeat);
         var markPos = mark ? mark.find() : undefined;
         markPos = markPos ? markPos : cm.getCursor();
         cm.setCursor(markPos);
       },
+      scroll: function(cm, actionArgs, vim) {
+        if (vim.visualMode) {
+          return;
+        }
+        var repeat = actionArgs.repeat || 1;
+        var lineHeight = cm.defaultTextHeight();
+        var top = cm.getScrollInfo().top;
+        var delta = lineHeight * repeat;
+        var newPos = actionArgs.forward ? top + delta : top - delta;
+        var cursor = cm.getCursor();
+        var cursorCoords = cm.charCoords(cursor, 'local');
+        if (actionArgs.forward) {
+          if (newPos > cursorCoords.top) {
+             cursor.line += (newPos - cursorCoords.top) / lineHeight;
+             cursor.line = Math.ceil(cursor.line);
+             cm.setCursor(cursor);
+             cursorCoords = cm.charCoords(cursor, 'local');
+             cm.scrollTo(null, cursorCoords.top);
+          } else {
+             // Cursor stays within bounds.  Just reposition the scroll window.
+             cm.scrollTo(null, newPos);
+          }
+        } else {
+          var newBottom = newPos + cm.getScrollInfo().clientHeight;
+          if (newBottom < cursorCoords.bottom) {
+             cursor.line -= (cursorCoords.bottom - newBottom) / lineHeight;
+             cursor.line = Math.floor(cursor.line);
+             cm.setCursor(cursor);
+             cursorCoords = cm.charCoords(cursor, 'local');
+             cm.scrollTo(
+                 null, cursorCoords.bottom - cm.getScrollInfo().clientHeight);
+          } else {
+             // Cursor stays within bounds.  Just reposition the scroll window.
+             cm.scrollTo(null, newPos);
+          }
+        }
+      },
       scrollToCursor: function(cm, actionArgs) {
         var lineNum = cm.getCursor().line;
         var charCoords = cm.charCoords({line: lineNum, ch: 0}, 'local');
         var height = cm.getScrollInfo().clientHeight;
         var y = charCoords.top;
         var lineHeight = charCoords.bottom - y;
         switch (actionArgs.position) {
           case 'center': y = y - (height / 2) + lineHeight;
@@ -1686,16 +1752,17 @@
           cursor = { line: cursor.line, ch: lineLength(cm, cursor.line) };
           cm.setCursor(cursor);
         } else if (insertAt == 'charAfter') {
           cm.setCursor(offsetCursor(cm.getCursor(), 0, 1));
         } else if (insertAt == 'firstNonBlank') {
           cm.setCursor(motions.moveToFirstNonWhiteSpaceCharacter(cm));
         }
         cm.setOption('keyMap', 'vim-insert');
+        cm.setOption('disableInput', false);
         if (actionArgs && actionArgs.replace) {
           // Handle Replace-mode as a special case of insert mode.
           cm.toggleOverwrite(true);
           cm.setOption('keyMap', 'vim-replace');
           CodeMirror.signal(cm, "vim-mode-change", {mode: "replace"});
         } else {
           cm.setOption('keyMap', 'vim-insert');
           CodeMirror.signal(cm, "vim-mode-change", {mode: "insert"});
@@ -2891,24 +2958,26 @@
       return {top: from.line, bottom: to.line};
     }
 
     // Ex command handling
     // Care must be taken when adding to the default Ex command map. For any
     // pair of commands that have a shared prefix, at least one of their
     // shortNames must not match the prefix of the other command.
     var defaultExCommandMap = [
-      { name: 'map', type: 'builtIn' },
-      { name: 'write', shortName: 'w', type: 'builtIn' },
-      { name: 'undo', shortName: 'u', type: 'builtIn' },
-      { name: 'redo', shortName: 'red', type: 'builtIn' },
-      { name: 'sort', shortName: 'sor', type: 'builtIn'},
-      { name: 'substitute', shortName: 's', type: 'builtIn'},
-      { name: 'nohlsearch', shortName: 'noh', type: 'builtIn'},
-      { name: 'delmarks', shortName: 'delm', type: 'builtin'}
+      { name: 'map' },
+      { name: 'nmap', shortName: 'nm' },
+      { name: 'vmap', shortName: 'vm' },
+      { name: 'write', shortName: 'w' },
+      { name: 'undo', shortName: 'u' },
+      { name: 'redo', shortName: 'red' },
+      { name: 'sort', shortName: 'sor' },
+      { name: 'substitute', shortName: 's' },
+      { name: 'nohlsearch', shortName: 'noh' },
+      { name: 'delmarks', shortName: 'delm' }
     ];
     Vim.ExCommandDispatcher = function() {
       this.buildCommandMap_();
     };
     Vim.ExCommandDispatcher.prototype = {
       processCommand: function(cm, input) {
         var vim = cm.state.vim;
         if (vim.visualMode) {
@@ -2950,16 +3019,17 @@
         if (!commandName) {
           showConfirm(cm, 'Not an editor command ":' + input + '"');
           return;
         }
         try {
           exCommands[commandName](cm, params);
         } catch(e) {
           showConfirm(cm, e);
+          throw e;
         }
       },
       parseInput_: function(cm, inputStream, result) {
         inputStream.eatWhile(':');
         // Parse range.
         if (inputStream.eat('%')) {
           result.line = cm.firstLine();
           result.lineEnd = cm.lastLine();
@@ -3032,18 +3102,19 @@
       buildCommandMap_: function() {
         this.commandMap_ = {};
         for (var i = 0; i < defaultExCommandMap.length; i++) {
           var command = defaultExCommandMap[i];
           var key = command.shortName || command.name;
           this.commandMap_[key] = command;
         }
       },
-      map: function(lhs, rhs) {
+      map: function(lhs, rhs, ctx) {
         if (lhs != ':' && lhs.charAt(0) == ':') {
+          if (ctx) { throw Error('Mode not supported for ex mappings'); }
           var commandName = lhs.substring(1);
           if (rhs != ':' && rhs.charAt(0) == ':') {
             // Ex to Ex mapping
             this.commandMap_[commandName] = {
               name: commandName,
               type: 'exToEx',
               toInput: rhs.substring(1)
             };
@@ -3053,27 +3124,31 @@
               name: commandName,
               type: 'exToKey',
               toKeys: parseKeyString(rhs)
             };
           }
         } else {
           if (rhs != ':' && rhs.charAt(0) == ':') {
             // Key to Ex mapping.
-            defaultKeymap.unshift({
+            var mapping = {
               keys: parseKeyString(lhs),
               type: 'keyToEx',
-              exArgs: { input: rhs.substring(1) }});
+              exArgs: { input: rhs.substring(1) }};
+            if (ctx) { mapping.context = ctx; }
+            defaultKeymap.unshift(mapping);
           } else {
             // Key to key mapping
-            defaultKeymap.unshift({
+            var mapping = {
               keys: parseKeyString(lhs),
               type: 'keyToKey',
               toKeys: parseKeyString(rhs)
-            });
+            };
+            if (ctx) { mapping.context = ctx; }
+            defaultKeymap.unshift(mapping);
           }
         }
       }
     };
 
     // Converts a key string sequence of the form a<C-w>bd<Left> into Vim's
     // keymap representation.
     function parseKeyString(str) {
@@ -3085,57 +3160,63 @@
         key = match[0];
         str = str.substring(match.index + key.length);
         keys.push(key);
       }
       return keys;
     }
 
     var exCommands = {
-      map: function(cm, params) {
+      map: function(cm, params, ctx) {
         var mapArgs = params.args;
         if (!mapArgs || mapArgs.length < 2) {
           if (cm) {
             showConfirm(cm, 'Invalid mapping: ' + params.input);
           }
           return;
         }
-        exCommandDispatcher.map(mapArgs[0], mapArgs[1], cm);
+        exCommandDispatcher.map(mapArgs[0], mapArgs[1], ctx);
       },
+      nmap: function(cm, params) { this.map(cm, params, 'normal'); },
+      vmap: function(cm, params) { this.map(cm, params, 'visual'); },
       move: function(cm, params) {
         commandDispatcher.processCommand(cm, cm.state.vim, {
             type: 'motion',
             motion: 'moveToLineOrEdgeOfDocument',
             motionArgs: { forward: false, explicitRepeat: true,
               linewise: true },
             repeatOverride: params.line+1});
       },
       sort: function(cm, params) {
         var reverse, ignoreCase, unique, number;
         function parseArgs() {
           if (params.argString) {
             var args = new CodeMirror.StringStream(params.argString);
             if (args.eat('!')) { reverse = true; }
             if (args.eol()) { return; }
-            if (!args.eatSpace()) { throw new Error('invalid arguments ' + args.match(/.*/)[0]); }
+            if (!args.eatSpace()) { return 'Invalid arguments'; }
             var opts = args.match(/[a-z]+/);
             if (opts) {
               opts = opts[0];
               ignoreCase = opts.indexOf('i') != -1;
               unique = opts.indexOf('u') != -1;
               var decimal = opts.indexOf('d') != -1 && 1;
               var hex = opts.indexOf('x') != -1 && 1;
               var octal = opts.indexOf('o') != -1 && 1;
-              if (decimal + hex + octal > 1) { throw new Error('invalid arguments'); }
+              if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
               number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
             }
-            if (args.eatSpace() && args.match(/\/.*\//)) { throw new Error('patterns not supported'); }
+            if (args.eatSpace() && args.match(/\/.*\//)) { 'patterns not supported'; }
           }
         }
-        parseArgs();
+        var err = parseArgs();
+        if (err) {
+          showConfirm(cm, err + ': ' + params.argString);
+          return;
+        }
         var lineStart = params.line || cm.firstLine();
         var lineEnd = params.lineEnd || params.line || cm.lastLine();
         if (lineStart == lineEnd) { return; }
         var curStart = { line: lineStart, ch: 0 };
         var curEnd = { line: lineEnd, ch: lineLength(cm, lineEnd) };
         var text = cm.getRange(curStart, curEnd).split('\n');
         var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
            (number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
@@ -3246,23 +3327,23 @@
           // Saves to text area if no save command is defined.
           cm.save();
         }
       },
       nohlsearch: function(cm) {
         clearSearchHighlight(cm);
       },
       delmarks: function(cm, params) {
-        if (!params.argString || !params.argString.trim()) {
+        if (!params.argString || !trim(params.argString)) {
           showConfirm(cm, 'Argument required');
           return;
         }
 
         var state = cm.state.vim;
-        var stream = new CodeMirror.StringStream(params.argString.trim());
+        var stream = new CodeMirror.StringStream(trim(params.argString));
         while (!stream.eol()) {
           stream.eatSpace();
 
           // Record the streams position at the beginning of the loop for use
           // in error messages.
           var count = stream.pos;
 
           if (!stream.match(/[a-zA-Z]/, false)) {
@@ -3389,17 +3470,18 @@
             break;
         }
         if (done) { stop(close); }
       }
 
       // Actually do replace.
       next();
       if (done) {
-        throw new Error('No matches for ' + query.source);
+        showConfirm(cm, 'No matches for ' + query.source);
+        return;
       }
       if (!confirm) {
         replaceAll();
         return;
       }
       showPrompt(cm, {
         prefix: 'replace with <strong>' + replaceWith + '</strong> (y/n/a/q/l)',
         onKeyDown: onPromptKeyDown
@@ -3440,17 +3522,16 @@
       function keyMapper(vimKey) {
         return function(cm) {
           CodeMirror.Vim.handleKey(cm, vimKey);
         };
       }
 
       var cmToVimKeymap = {
         'nofallthrough': true,
-        'disableInput': true,
         'style': 'fat-cursor'
       };
       function bindKeys(keys, modifier) {
         for (var i = 0; i < keys.length; i++) {
           var key = keys[i];
           if (!modifier && inArray(key, specialSymbols)) {
             // Wrap special symbols with '' because that's how CodeMirror binds
             // them.
@@ -3487,16 +3568,17 @@
         repeatLastEdit(cm, vim, vim.insertModeRepeat - 1,
             true /** repeatForInsert */);
         vim.lastEditInputState.repeatOverride = vim.insertModeRepeat;
       }
       delete vim.insertModeRepeat;
       cm.setCursor(cm.getCursor().line, cm.getCursor().ch-1, true);
       vim.insertMode = false;
       cm.setOption('keyMap', 'vim');
+      cm.setOption('disableInput', true);
       cm.toggleOverwrite(false); // exit replace mode if we were in it.
       CodeMirror.signal(cm, "vim-mode-change", {mode: "normal"});
     }
 
     CodeMirror.keyMap['vim-insert'] = {
       // TODO: override navigation keys so that Esc will cancel automatic
       // indentation from o, O, i_<CR>
       'Esc': exitInsertMode,
--- a/browser/devtools/sourceeditor/codemirror/matchbrackets.js
+++ b/browser/devtools/sourceeditor/codemirror/matchbrackets.js
@@ -49,17 +49,17 @@
     if (!found || cm.getLine(found.from.line).length > maxHighlightLen ||
        found.to && cm.getLine(found.to.line).length > maxHighlightLen)
       return;
 
     var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
     var one = cm.markText(found.from, Pos(found.from.line, found.from.ch + 1), {className: style});
     var two = found.to && cm.markText(found.to, Pos(found.to.line, found.to.ch + 1), {className: style});
     // Kludge to work around the IE bug from issue #1193, where text
-    // input stops going to the textare whever this fires.
+    // input stops going to the textarea whenever this fires.
     if (ie_lt8 && cm.state.focused) cm.display.input.focus();
     var clear = function() {
       cm.operation(function() { one.clear(); two && two.clear(); });
     };
     if (autoclear) setTimeout(clear, 800);
     else return clear;
   }
 
--- a/browser/devtools/sourceeditor/codemirror/mozilla.css
+++ b/browser/devtools/sourceeditor/codemirror/mozilla.css
@@ -69,24 +69,16 @@ selector in floating-scrollbar-light.css
   background-image: url("chrome://browser/skin/devtools/background-noise-toolbar.png"),
     linear-gradient(#3e4750, #3e4750);
 }
 
 .CodeMirror-dialog input {
   font: message-box;
 }
 
-.CodeMirror-code > div > div:first-child {
-  top: 50%;
-}
-
-.CodeMirror-gutter-elt {
-  transform: translate(0,-50%);
-}
-
 .cm-trailingspace {
   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAACCAYAAAB/qH1jAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QUXCToH00Y1UgAAACFJREFUCNdjPMDBUc/AwNDAAAFMTAwMDA0OP34wQgX/AQBYgwYEx4f9lQAAAABJRU5ErkJggg==");
   opacity: 0.75;
   background-position: left bottom;
   background-repeat: repeat-x;
 }
 
 /* Fold addon */
--- a/browser/devtools/sourceeditor/codemirror/search/search.js
+++ b/browser/devtools/sourceeditor/codemirror/search/search.js
@@ -2,77 +2,77 @@
 // implementation of the openDialog method.
 
 // Replace works a little oddly -- it will do the replace on the next
 // Ctrl-G (or whatever is bound to findNext) press. You prevent a
 // replace by making sure the match is no longer selected when hitting
 // Ctrl-G.
 
 (function() {
-  function searchOverlay(query) {
+  function searchOverlay(query, caseInsensitive) {
+    var startChar;
+    if (typeof query == "string") {
+      startChar = query.charAt(0);
+      query = new RegExp("^" + query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"),
+                         caseInsensitive ? "i" : "");
+    } else {
+      query = new RegExp("^(?:" + query.source + ")", query.ignoreCase ? "i" : "");
+    }
     if (typeof query == "string") return {token: function(stream) {
       if (stream.match(query)) return "searching";
       stream.next();
       stream.skipTo(query.charAt(0)) || stream.skipToEnd();
     }};
     return {token: function(stream) {
       if (stream.match(query)) return "searching";
       while (!stream.eol()) {
         stream.next();
+        if (startChar)
+          stream.skipTo(startChar) || stream.skipToEnd();
         if (stream.match(query, false)) break;
       }
     }};
   }
 
   function SearchState() {
     this.posFrom = this.posTo = this.query = null;
     this.overlay = null;
   }
   function getSearchState(cm) {
     return cm.state.search || (cm.state.search = new SearchState());
   }
+  function queryCaseInsensitive(query) {
+    return typeof query == "string" && query == query.toLowerCase();
+  }
   function getSearchCursor(cm, query, pos) {
     // Heuristic: if the query string is all lowercase, do a case insensitive search.
-    return cm.getSearchCursor(query, pos, typeof query == "string" && query == query.toLowerCase());
+    return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
   }
-  function dialog(cm, text, shortText, f) {
-    if (cm.openDialog) cm.openDialog(text, f);
-    else f(prompt(shortText, ""));
+  function dialog(cm, text, shortText, deflt, f) {
+    if (cm.openDialog) cm.openDialog(text, f, {value: deflt});
+    else f(prompt(shortText, deflt));
   }
   function confirmDialog(cm, text, shortText, fs) {
     if (cm.openConfirm) cm.openConfirm(text, fs);
     else if (confirm(shortText)) fs[0]();
   }
   function parseQuery(query) {
     var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
     return isRE ? new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i") : query;
   }
-  var queryDialog;
+  var queryDialog =
+    'Search: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
   function doSearch(cm, rev) {
-    if (!queryDialog) {
-      let doc = cm.getWrapperElement().ownerDocument;
-      let inp = doc.createElement("input");
-      let txt = doc.createTextNode(cm.l10n("findCmd.promptMessage"));
-
-      inp.type = "text";
-      inp.style.width = "10em";
-      inp.style.MozMarginStart = "1em";
-
-      queryDialog = doc.createElement("div");
-      queryDialog.appendChild(txt);
-      queryDialog.appendChild(inp);
-    }
-
     var state = getSearchState(cm);
     if (state.query) return findNext(cm, rev);
-    dialog(cm, queryDialog, cm.l10n('findCmd.promptMessage'), function(query) {
+    dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
       cm.operation(function() {
         if (!query || state.query) return;
         state.query = parseQuery(query);
-        cm.removeOverlay(state.overlay);
+        cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
         state.overlay = searchOverlay(state.query);
         cm.addOverlay(state.overlay);
         state.posFrom = state.posTo = cm.getCursor();
         findNext(cm, rev);
       });
     });
   }
   function findNext(cm, rev) {cm.operation(function() {
@@ -93,20 +93,20 @@
     cm.removeOverlay(state.overlay);
   });}
 
   var replaceQueryDialog =
     'Replace: <input type="text" style="width: 10em"/> <span style="color: #888">(Use /re/ syntax for regexp search)</span>';
   var replacementQueryDialog = 'With: <input type="text" style="width: 10em"/>';
   var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>";
   function replace(cm, all) {
-    dialog(cm, replaceQueryDialog, "Replace:", function(query) {
+    dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) {
       if (!query) return;
       query = parseQuery(query);
-      dialog(cm, replacementQueryDialog, "Replace with:", function(text) {
+      dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
         if (all) {
           cm.operation(function() {
             for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
               if (typeof query != "string") {
                 var match = cm.getRange(cursor.from(), cursor.to()).match(query);
                 cursor.replace(text.replace(/\$(\d)/, function(_, i) {return match[i];}));
               } else cursor.replace(text);
             }
--- a/browser/devtools/sourceeditor/codemirror/search/searchcursor.js
+++ b/browser/devtools/sourceeditor/codemirror/search/searchcursor.js
@@ -42,54 +42,67 @@
           if (start + matchLen != line.length && !matchLen) matchLen = 1;
         }
         if (match && matchLen)
           return {from: Pos(pos.line, start),
                   to: Pos(pos.line, start + matchLen),
                   match: match};
       };
     } else { // String query
+      var origQuery = query;
       if (caseFold) query = query.toLowerCase();
       var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
       var target = query.split("\n");
       // Different methods for single-line and multi-line queries
       if (target.length == 1) {
         if (!query.length) {
           // Empty string would match anything and never progress, so
           // we define it to match nothing instead.
           this.matches = function() {};
         } else {
           this.matches = function(reverse, pos) {
-            var line = fold(doc.getLine(pos.line)), len = query.length, match;
-            if (reverse ? (pos.ch >= len && (match = line.lastIndexOf(query, pos.ch - len)) != -1)
-                        : (match = line.indexOf(query, pos.ch)) != -1)
-              return {from: Pos(pos.line, match),
-                      to: Pos(pos.line, match + len)};
+            if (reverse) {
+              var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+              var match = line.lastIndexOf(query);
+              if (match > -1) {
+                match = adjustPos(orig, line, match);
+                return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+              }
+             } else {
+               var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+               var match = line.indexOf(query);
+               if (match > -1) {
+                 match = adjustPos(orig, line, match) + pos.ch;
+                 return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+               }
+            }
           };
         }
       } else {
+        var origTarget = origQuery.split("\n");
         this.matches = function(reverse, pos) {
-          var ln = pos.line, idx = (reverse ? target.length - 1 : 0), match = target[idx], line = fold(doc.getLine(ln));
-          var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match));
-          if (reverse ? offsetA > pos.ch || offsetA != match.length
-              : offsetA < pos.ch || offsetA != line.length - match.length)
-            return;
-          for (;;) {
-            if (reverse ? !ln : ln == doc.lineCount() - 1) return;
-            line = fold(doc.getLine(ln += reverse ? -1 : 1));
-            match = target[reverse ? --idx : ++idx];
-            if (idx > 0 && idx < target.length - 1) {
-              if (line != match) return;
-              else continue;
-            }
-            var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length);
-            if (reverse ? offsetB != line.length - match.length : offsetB != match.length)
-              return;
-            var start = Pos(pos.line, offsetA), end = Pos(ln, offsetB);
-            return {from: reverse ? end : start, to: reverse ? start : end};
+          var last = target.length - 1;
+          if (reverse) {
+            if (pos.line - (target.length - 1) < doc.firstLine()) return;
+            if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+            var to = Pos(pos.line, origTarget[last].length);
+            for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+              if (target[i] != fold(doc.getLine(ln))) return;
+            var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+            if (fold(line.slice(cut)) != target[0]) return;
+            return {from: Pos(ln, cut), to: to};
+          } else {
+            if (pos.line + (target.length - 1) > doc.lastLine()) return;
+            var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+            if (fold(line.slice(cut)) != target[0]) return;
+            var from = Pos(pos.line, cut);
+            for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+              if (target[i] != fold(doc.getLine(ln))) return;
+            if (doc.getLine(ln).slice(0, origTarget[last].length) != target[last]) return;
+            return {from: from, to: Pos(ln, origTarget[last].length)};
           }
         };
       }
     }
   }
 
   SearchCursor.prototype = {
     findNext: function() {return this.find(false);},
@@ -101,17 +114,16 @@
         var pos = Pos(line, 0);
         self.pos = {from: pos, to: pos};
         self.atOccurrence = false;
         return false;
       }
 
       for (;;) {
         if (this.pos = this.matches(reverse, pos)) {
-          if (!this.pos.from || !this.pos.to) { console.log(this.matches, this.pos); }
           this.atOccurrence = true;
           return this.pos.match || true;
         }
         if (reverse) {
           if (!pos.line) return savePosAndFail(0);
           pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
         }
         else {
@@ -129,15 +141,27 @@
       if (!this.atOccurrence) return;
       var lines = CodeMirror.splitLines(newText);
       this.doc.replaceRange(lines, this.pos.from, this.pos.to);
       this.pos.to = Pos(this.pos.from.line + lines.length - 1,
                         lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
     }
   };
 
+  // Maps a position in a case-folded line back to a position in the original line
+  // (compensating for codepoints increasing in number during folding)
+  function adjustPos(orig, folded, pos) {
+    if (orig.length == folded.length) return pos;
+    for (var pos1 = Math.min(pos, orig.length);;) {
+      var len1 = orig.slice(0, pos1).toLowerCase().length;
+      if (len1 < pos) ++pos1;
+      else if (len1 > pos) --pos1;
+      else return pos1;
+    }
+  }
+
   CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
     return new SearchCursor(this.doc, query, pos, caseFold);
   });
   CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
     return new SearchCursor(this, query, pos, caseFold);
   });
 })();
--- a/browser/devtools/sourceeditor/codemirror/trailingspace.js
+++ b/browser/devtools/sourceeditor/codemirror/trailingspace.js
@@ -1,19 +1,15 @@
-(function() {
-  "use strict";
-
-  CodeMirror.defineOption("showTrailingSpace", true, function(cm, val, prev) {
-    if (prev == CodeMirror.Init) prev = false;
-    if (prev && !val)
+CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
+  if (prev == CodeMirror.Init) prev = false;
+  if (prev && !val)
     cm.removeOverlay("trailingspace");
-    else if (!prev && val)
+  else if (!prev && val)
     cm.addOverlay({
       token: function(stream) {
         for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
         if (i > stream.pos) { stream.pos = i; return null; }
         stream.pos = l;
         return "trailingspace";
       },
       name: "trailingspace"
     });
-  });
-})();
\ No newline at end of file
+});
--- a/browser/devtools/sourceeditor/codemirror/xml.js
+++ b/browser/devtools/sourceeditor/codemirror/xml.js
@@ -40,17 +40,17 @@ CodeMirror.defineMode("xml", function(co
     contextGrabbers: {},
     doNotIndent: {},
     allowUnquoted: false,
     allowMissing: false
   };
   var alignCDATA = parserConfig.alignCDATA;
 
   // Return variables for tokenizers
-  var tagName, type;
+  var tagName, type, setStyle;
 
   function inText(stream, state) {
     function chain(parser) {
       state.tokenize = parser;
       return parser(stream, state);
     }
 
     var ch = stream.next();
@@ -105,16 +105,18 @@ CodeMirror.defineMode("xml", function(co
       state.tokenize = inText;
       type = ch == ">" ? "endTag" : "selfcloseTag";
       return "tag";
     } else if (ch == "=") {
       type = "equals";
       return null;
     } else if (ch == "<") {
       state.tokenize = inText;
+      state.state = baseState;
+      state.tagName = state.tagStart = null;
       var next = state.tokenize(stream, state);
       return next ? next + " error" : "error";
     } else if (/[\'\"]/.test(ch)) {
       state.tokenize = inAttribute(ch);
       state.stringStartCol = stream.column();
       return state.tokenize(stream, state);
     } else {
       stream.eatWhile(/[^\s\u00a0=<>\"\']/);
@@ -164,160 +166,145 @@ CodeMirror.defineMode("xml", function(co
             return state.tokenize(stream, state);
           }
         }
       }
       return "meta";
     };
   }
 
-  var curState, curStream, setStyle;
-  function pass() {
-    for (var i = arguments.length - 1; i >= 0; i--) curState.cc.push(arguments[i]);
+  function Context(state, tagName, startOfLine) {
+    this.prev = state.context;
+    this.tagName = tagName;
+    this.indent = state.indented;
+    this.startOfLine = startOfLine;
+    if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
+      this.noIndent = true;
+  }
+  function popContext(state) {
+    if (state.context) state.context = state.context.prev;
   }
-  function cont() {
-    pass.apply(null, arguments);
-    return true;
+  function maybePopContext(state, nextTagName) {
+    var parentTagName;
+    while (true) {
+      if (!state.context) {
+        return;
+      }
+      parentTagName = state.context.tagName.toLowerCase();
+      if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
+          !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+        return;
+      }
+      popContext(state);
+    }
   }
 
-  function pushContext(tagName, startOfLine) {
-    var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName) || (curState.context && curState.context.noIndent);
-    curState.context = {
-      prev: curState.context,
-      tagName: tagName,
-      indent: curState.indented,
-      startOfLine: startOfLine,
-      noIndent: noIndent
-    };
-  }
-  function popContext() {
-    if (curState.context) curState.context = curState.context.prev;
-  }
-
-  function element(type) {
+  function baseState(type, stream, state) {
     if (type == "openTag") {
-      curState.tagName = tagName;
-      curState.tagStart = curStream.column();
-      return cont(attributes, endtag(curState.startOfLine));
+      state.tagName = tagName;
+      state.tagStart = stream.column();
+      return attrState;
     } else if (type == "closeTag") {
       var err = false;
-      if (curState.context) {
-        if (curState.context.tagName != tagName) {
-          if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) {
-            popContext();
-          }
-          err = !curState.context || curState.context.tagName != tagName;
+      if (state.context) {
+        if (state.context.tagName != tagName) {
+          if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName.toLowerCase()))
+            popContext(state);
+          err = !state.context || state.context.tagName != tagName;
         }
       } else {
         err = true;
       }
       if (err) setStyle = "error";
-      return cont(endclosetag(err));
-    }
-    return cont();
-  }
-  function endtag(startOfLine) {
-    return function(type) {
-      var tagName = curState.tagName;
-      curState.tagName = curState.tagStart = null;
-      if (type == "selfcloseTag" ||
-          (type == "endTag" && Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase()))) {
-        maybePopContext(tagName.toLowerCase());
-        return cont();
-      }
-      if (type == "endTag") {
-        maybePopContext(tagName.toLowerCase());
-        pushContext(tagName, startOfLine);
-        return cont();
-      }
-      return cont();
-    };
-  }
-  function endclosetag(err) {
-    return function(type) {
-      if (err) setStyle = "error";
-      if (type == "endTag") { popContext(); return cont(); }
-      setStyle = "error";
-      return cont(arguments.callee);
-    };
-  }
-  function maybePopContext(nextTagName) {
-    var parentTagName;
-    while (true) {
-      if (!curState.context) {
-        return;
-      }
-      parentTagName = curState.context.tagName.toLowerCase();
-      if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) ||
-          !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
-        return;
-      }
-      popContext();
+      return err ? closeStateErr : closeState;
+    } else {
+      return baseState;
     }
   }
-
-  function attributes(type) {
-    if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);}
-    if (type == "endTag" || type == "selfcloseTag") return pass();
+  function closeState(type, _stream, state) {
+    if (type != "endTag") {
+      setStyle = "error";
+      return closeState;
+    }
+    popContext(state);
+    return baseState;
+  }
+  function closeStateErr(type, stream, state) {
     setStyle = "error";
-    return cont(attributes);
+    return closeState(type, stream, state);
   }
-  function attribute(type) {
-    if (type == "equals") return cont(attvalue, attributes);
+
+  function attrState(type, _stream, state) {
+    if (type == "word") {
+      setStyle = "attribute";
+      return attrEqState;
+    } else if (type == "endTag" || type == "selfcloseTag") {
+      var tagName = state.tagName, tagStart = state.tagStart;
+      state.tagName = state.tagStart = null;
+      if (type == "selfcloseTag" ||
+          Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase())) {
+        maybePopContext(state, tagName.toLowerCase());
+      } else {
+        maybePopContext(state, tagName.toLowerCase());
+        state.context = new Context(state, tagName, tagStart == state.indented);
+      }
+      return baseState;
+    }
+    setStyle = "error";
+    return attrState;
+  }
+  function attrEqState(type, stream, state) {
+    if (type == "equals") return attrValueState;
     if (!Kludges.allowMissing) setStyle = "error";
-    else if (type == "word") {setStyle = "attribute"; return cont(attribute, attributes);}
-    return (type == "endTag" || type == "selfcloseTag") ? pass() : cont();
+    return attrState(type, stream, state);
   }
-  function attvalue(type) {
-    if (type == "string") return cont(attvaluemaybe);
-    if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return cont();}
+  function attrValueState(type, stream, state) {
+    if (type == "string") return attrContinuedState;
+    if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;}
     setStyle = "error";
-    return (type == "endTag" || type == "selfCloseTag") ? pass() : cont();
+    return attrState(type, stream, state);
   }
-  function attvaluemaybe(type) {
-    if (type == "string") return cont(attvaluemaybe);
-    else return pass();
+  function attrContinuedState(type, stream, state) {
+    if (type == "string") return attrContinuedState;
+    return attrState(type, stream, state);
   }
 
   return {
     startState: function() {
-      return {tokenize: inText, cc: [], indented: 0, startOfLine: true, tagName: null, tagStart: null, context: null};
+      return {tokenize: inText,
+              state: baseState,
+              indented: 0,
+              tagName: null, tagStart: null,
+              context: null};
     },
 
     token: function(stream, state) {
-      if (!state.tagName && stream.sol()) {
-        state.startOfLine = true;
+      if (!state.tagName && stream.sol())
         state.indented = stream.indentation();
-      }
+
       if (stream.eatSpace()) return null;
-
-      setStyle = type = tagName = null;
+      tagName = type = null;
       var style = state.tokenize(stream, state);
-      state.type = type;
       if ((style || type) && style != "comment") {
-        curState = state; curStream = stream;
-        while (true) {
-          var comb = state.cc.pop() || element;
-          if (comb(type || style)) break;
-        }
+        setStyle = null;
+        state.state = state.state(type || style, stream, state);
+        if (setStyle)
+          style = setStyle == "error" ? style + " error" : setStyle;
       }
-      state.startOfLine = false;
-      if (setStyle)
-        style = setStyle == "error" ? style + " error" : setStyle;
       return style;
     },
 
     indent: function(state, textAfter, fullLine) {
       var context = state.context;
       // Indent multi-line strings (e.g. css).
       if (state.tokenize.isInAttribute) {
         return state.stringStartCol + 1;
       }
-      if ((state.tokenize != inTag && state.tokenize != inText) ||
-          context && context.noIndent)
+      if (context && context.noIndent) return CodeMirror.Pass;
+      if (state.tokenize != inTag && state.tokenize != inText)
         return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0;
       // Indent the starts of attribute names.
       if (state.tagName) {
         if (multilineTagIndentPastTag)
           return state.tagStart + state.tagName.length + 2;
         else
           return state.tagStart + indentUnit * multilineTagIndentFactor;
       }
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -143,19 +143,19 @@ function Editor(config) {
     extraKeys:         {},
     indentWithTabs:    useTabs,
     styleActiveLine:   true,
     autoCloseBrackets: true,
     theme:             "mozilla"
   };
 
   // Additional shortcuts.
-  this.config.extraKeys[Editor.keyFor("jumpToLine")] = (cm) => this.jumpToLine(cm);
-  this.config.extraKeys[Editor.keyFor("moveLineUp")] = (cm) => this.moveLineUp();
-  this.config.extraKeys[Editor.keyFor("moveLineDown")] = (cm) => this.moveLineDown();
+  this.config.extraKeys[Editor.keyFor("jumpToLine")] = () => this.jumpToLine();
+  this.config.extraKeys[Editor.keyFor("moveLineUp")] = () => this.moveLineUp();
+  this.config.extraKeys[Editor.keyFor("moveLineDown")] = () => this.moveLineDown();
   this.config.extraKeys[Editor.keyFor("toggleComment")] = "toggleComment";
 
   // Disable ctrl-[ and ctrl-] because toolbox uses those shortcuts.
   this.config.extraKeys[Editor.keyFor("indentLess")] = false;
   this.config.extraKeys[Editor.keyFor("indentMore")] = false;
 
   // If alternative keymap is provided, use it.
   if (keyMap === "emacs" || keyMap === "vim")
@@ -171,16 +171,26 @@ function Editor(config) {
     if (!config.extraKeys)
       return;
 
     Object.keys(config.extraKeys).forEach((key) => {
       this.config.extraKeys[key] = config.extraKeys[key];
     });
   });
 
+  // Set the code folding gutter, if needed.
+  if (this.config.enableCodeFolding) {
+    this.config.foldGutter = true;
+
+    if (!this.config.gutters) {
+      this.config.gutters = this.config.lineNumbers ? ["CodeMirror-linenumbers"] : [];
+      this.config.gutters.push("CodeMirror-foldgutter");
+    }
+  }
+
   // Overwrite default tab behavior. If something is selected,
   // indent those lines. If nothing is selected and we're
   // indenting with tabs, insert one tab. Otherwise insert N
   // whitespaces where N == indentUnit option.
   this.config.extraKeys.Tab = (cm) => {
     if (cm.somethingSelected()) {
       cm.indentSelection("add");
       return;
@@ -252,17 +262,19 @@ Editor.prototype = {
 
       // Create a CodeMirror instance add support for context menus,
       // overwrite the default controller (otherwise items in the top and
       // context menus won't work).
 
       cm = win.CodeMirror(win.document.body, this.config);
       cm.getWrapperElement().addEventListener("contextmenu", (ev) => {
         ev.preventDefault();
-        this.showContextMenu(el.ownerDocument, ev.screenX, ev.screenY);
+        if (!this.config.contextMenu) return;
+        let popup = el.ownerDocument.getElementById(this.config.contextMenu);
+        popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
       }, false);
 
       cm.on("focus", () => this.emit("focus"));
       cm.on("scroll", () => this.emit("scroll"));
       cm.on("change", () => {
         this.emit("change");
         if (!this._lastDirty) {
           this._lastDirty = true;
@@ -655,42 +667,21 @@ Editor.prototype = {
    * clean i.e. no changes were made since the last version.
    */
   isClean: function () {
     let cm = editors.get(this);
     return cm.isClean(this.version);
   },
 
   /**
-   * True if the editor is in the read-only mode, false otherwise.
-   */
-  isReadOnly: function () {
-    return this.getOption("readOnly");
-  },
-
-  /**
-   * Displays a context menu at the point x:y. The first
-   * argument, container, should be a DOM node that contains
-   * a context menu element specified by the ID from
-   * config.contextMenu.
-   */
-  showContextMenu: function (container, x, y) {
-    if (this.config.contextMenu == null)
-      return;
-
-    let popup = container.getElementById(this.config.contextMenu);
-    popup.openPopupAtScreen(x, y, true);
-  },
-
-  /**
    * This method opens an in-editor dialog asking for a line to
    * jump to. Once given, it changes cursor to that line.
    */
-  jumpToLine: function (cm) {
-    let doc = cm.getWrapperElement().ownerDocument;
+  jumpToLine: function () {
+    let doc = editors.get(this).getWrapperElement().ownerDocument;
     let div = doc.createElement("div");
     let inp = doc.createElement("input");
     let txt = doc.createTextNode(L10N.GetStringFromName("gotoLineCmd.promptTitle"));
 
     inp.type = "text";
     inp.style.width = "10em";
     inp.style.MozMarginStart = "1em";
 
@@ -928,16 +919,16 @@ function controller(ed) {
       };
 
       if (map[cmd]) {
         cm.execCommand(map[cmd]);
         return;
       }
 
       if (cmd == "cmd_gotoLine")
-        ed.jumpToLine(cm);
+        ed.jumpToLine();
     },
 
     onEvent: function () {}
   };
 }
 
 module.exports = Editor;
--- a/browser/devtools/sourceeditor/test/cm_comment_test.js
+++ b/browser/devtools/sourceeditor/test/cm_comment_test.js
@@ -24,28 +24,40 @@ namespace = "comment_";
   }, simpleProg, "function foo() {\n//   return bar;\n}");
 
   test("lineToggle", "javascript", function(cm) {
     cm.lineComment(Pos(0, 0), Pos(2, 1));
     cm.uncomment(Pos(0, 0), Pos(2, 1));
   }, simpleProg, simpleProg);
 
   // test("fallbackToBlock", "css", function(cm) {
-  //   cm.lineComment(Pos(0, 0), Pos(2, 1));
+  //  cm.lineComment(Pos(0, 0), Pos(2, 1));
   // }, "html {\n  border: none;\n}", "/* html {\n  border: none;\n} */");
 
   // test("fallbackToLine", "ruby", function(cm) {
-  //   cm.blockComment(Pos(0, 0), Pos(1));
+  //  cm.blockComment(Pos(0, 0), Pos(1));
   // }, "def blah()\n  return hah\n", "# def blah()\n#   return hah\n");
 
   test("commentRange", "javascript", function(cm) {
     cm.blockComment(Pos(1, 2), Pos(1, 13), {fullLines: false});
   }, simpleProg, "function foo() {\n  /*return bar;*/\n}");
 
   test("indented", "javascript", function(cm) {
     cm.lineComment(Pos(1, 0), Pos(2), {indent: true});
   }, simpleProg, "function foo() {\n  // return bar;\n  // }");
 
   test("singleEmptyLine", "javascript", function(cm) {
     cm.setCursor(1);
     cm.execCommand("toggleComment");
   }, "a;\n\nb;", "a;\n// \nb;");
+
+  test("dontMessWithStrings", "javascript", function(cm) {
+    cm.execCommand("toggleComment");
+  }, "console.log(\"/*string*/\");", "// console.log(\"/*string*/\");");
+
+  test("dontMessWithStrings2", "javascript", function(cm) {
+    cm.execCommand("toggleComment");
+  }, "console.log(\"// string\");", "// console.log(\"// string\");");
+
+  test("dontMessWithStrings3", "javascript", function(cm) {
+    cm.execCommand("toggleComment");
+  }, "// console.log(\"// string\");", "console.log(\"// string\");");
 })();
--- a/browser/devtools/sourceeditor/test/cm_driver.js
+++ b/browser/devtools/sourceeditor/test/cm_driver.js
@@ -1,9 +1,9 @@
-var tests = [], debug = null, debugUsed = new Array(), allNames = [];
+var tests = [], filters = [], allNames = [];
 
 function Failure(why) {this.message = why;}
 Failure.prototype.toString = function() { return this.message; };
 
 function indexOf(collection, elt) {
   if (collection.indexOf) return collection.indexOf(elt);
   for (var i = 0, e = collection.length; i < e; ++i)
     if (collection[i] == elt) return i;
@@ -27,69 +27,53 @@ var namespace = "";
 function testCM(name, run, opts, expectedFail) {
   return test(namespace + name, function() {
     var place = document.getElementById("testground"), cm = window.cm = CodeMirror(place, opts);
     var successful = false;
     try {
       run(cm);
       successful = true;
     } finally {
-      if ((debug && !successful) || verbose) {
+      if (!successful || verbose) {
         place.style.visibility = "visible";
       } else {
         place.removeChild(cm.getWrapperElement());
       }
     }
   }, expectedFail);
 }
 
 function runTests(callback) {
-  if (debug) {
-    if (indexOf(debug, "verbose") === 0) {
-      verbose = true;
-      debug.splice(0, 1);
-    }
-    if (debug.length < 1) {
-      debug = null;
-    }
-  }
   var totalTime = 0;
   function step(i) {
     if (i === tests.length){
       running = false;
       return callback("done");
-    }
+    } 
     var test = tests[i], expFail = test.expectedFail, startTime = +new Date;
-    if (debug !== null) {
-      var debugIndex = indexOf(debug, test.name);
-      if (debugIndex !== -1) {
-        // Remove from array for reporting incorrect tests later
-        debug.splice(debugIndex, 1);
-      } else {
-        var wildcardName = test.name.split("_")[0] + "_*";
-        debugIndex = indexOf(debug, wildcardName);
-        if (debugIndex !== -1) {
-          // Remove from array for reporting incorrect tests later
-          debug.splice(debugIndex, 1);
-          debugUsed.push(wildcardName);
-        } else {
-          debugIndex = indexOf(debugUsed, wildcardName);
-          if (debugIndex == -1) return step(i + 1);
+    if (filters.length) {
+      for (var j = 0; j < filters.length; j++) {
+        if (test.name.match(filters[j])) {
+          break;
         }
       }
+      if (j == filters.length) {      
+        callback("skipped", test.name, message);
+        return step(i + 1);
+      }
     }
     var threw = false;
     try {
       var message = test.func();
     } catch(e) {
       threw = true;
       if (expFail) callback("expected", test.name);
       else if (e instanceof Failure) callback("fail", test.name, e.message);
       else {
-        var pos = /\bat .*?([^\/:]+):(\d+):/.exec(e.stack);
+        var pos = /(?:\bat |@).*?([^\/:]+):(\d+)/.exec(e.stack);
         callback("error", test.name, e.toString() + (pos ? " (" + pos[1] + ":" + pos[2] + ")" : ""));
       }
     }
     if (!threw) {
       if (expFail) callback("fail", test.name, message || "expected failure, but succeeded");
       else callback("ok", test.name, message);
     }
     if (!quit) { // Run next test
@@ -122,18 +106,26 @@ function eqPos(a, b, msg) {
   if (b == null) throw new Failure(label("comparing " + str(a) + " to null", msg));
   if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
 }
 function is(a, msg) {
   if (!a) throw new Failure(label("assertion failed", msg));
 }
 
 function countTests() {
-  if (!debug) return tests.length;
+  if (!filters.length) return tests.length;
   var sum = 0;
   for (var i = 0; i < tests.length; ++i) {
     var name = tests[i].name;
-    if (indexOf(debug, name) != -1 ||
-        indexOf(debug, name.split("_")[0] + "_*") != -1)
-      ++sum;
+    for (var j = 0; j < filters.length; j++) {
+      if (name.match(filters[j])) {
+        ++sum;
+        break;
+      }
+    }
   }
   return sum;
 }
+
+function parseTestFilter(s) {
+  if (/_\*$/.test(s)) return new RegExp("^" + s.slice(0, s.length - 2), "i");
+  else return new RegExp(s, "i");
+}
--- a/browser/devtools/sourceeditor/test/cm_emacs_test.js
+++ b/browser/devtools/sourceeditor/test/cm_emacs_test.js
@@ -120,16 +120,19 @@
       "Ctrl-2", "Alt-U", txt("FOO BAR bAz"),
       "Ctrl-A", "Ctrl-3", "Alt-C", txt("Foo Bar Baz"));
 
   sim("upExpr", "foo {\n  bar[];\n  baz(blah);\n}",
       Pos(2, 7), "Ctrl-Alt-U", at(2, 5), "Ctrl-Alt-U", at(0, 4));
   sim("transposeExpr", "do foo[bar] dah",
       Pos(0, 6), "Ctrl-Alt-T", txt("do [bar]foo dah"));
 
+  sim("clearMark", "abcde", Pos(0, 2), "Ctrl-Space", "Ctrl-F", "Ctrl-F",
+      "Ctrl-G", "Ctrl-W", txt("abcde"));
+
   testCM("save", function(cm) {
     var saved = false;
     CodeMirror.commands.save = function(cm) { saved = cm.getValue(); };
     cm.triggerOnKeyDown(fakeEvent("Ctrl-X"));
     cm.triggerOnKeyDown(fakeEvent("Ctrl-S"));
     is(saved, "hi");
   }, {value: "hi", keyMap: "emacs"});
 })();
--- a/browser/devtools/sourceeditor/test/cm_mode_javascript_test.js
+++ b/browser/devtools/sourceeditor/test/cm_mode_javascript_test.js
@@ -1,72 +1,113 @@
 (function() {
   var mode = CodeMirror.getMode({indentUnit: 2}, "javascript");
   function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); }
 
   MT("locals",
-     "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] = [number 10]; [keyword return] [variable-2 a] + [variable-2 c] + [variable d]; }");
+     "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }");
 
   MT("comma-and-binop",
-     "[keyword function](){ [keyword var] [def x] = [number 1] + [number 2], [def y]; }");
+     "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }");
 
   MT("destructuring",
      "([keyword function]([def a], [[[def b], [def c] ]]) {",
-     "  [keyword let] {[def d], [property foo]: [def c]=[number 10], [def x]} = [variable foo]([variable-2 a]);",
-     "  [[[variable-2 c], [variable y] ]] = [variable-2 c];",
+     "  [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);",
+     "  [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];",
      "})();");
 
   MT("class",
      "[keyword class] [variable Point] [keyword extends] [variable SuperThing] {",
      "  [[ [string-2 /expr/] ]]: [number 24],",
      "  [property constructor]([def x], [def y]) {",
      "    [keyword super]([string 'something']);",
-     "    [keyword this].[property x] = [variable-2 x];",
+     "    [keyword this].[property x] [operator =] [variable-2 x];",
      "  }",
      "}");
 
   MT("module",
      "[keyword module] [string 'foo'] {",
-     "  [keyword export] [keyword let] [def x] = [number 42];",
+     "  [keyword export] [keyword let] [def x] [operator =] [number 42];",
      "  [keyword export] [keyword *] [keyword from] [string 'somewhere'];",
      "}");
 
   MT("import",
      "[keyword function] [variable foo]() {",
      "  [keyword import] [def $] [keyword from] [string 'jquery'];",
      "  [keyword module] [def crypto] [keyword from] [string 'crypto'];",
      "  [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];",
      "}");
 
   MT("const",
      "[keyword function] [variable f]() {",
-     "  [keyword const] [[ [def a], [def b] ]] = [[ [number 1], [number 2] ]];",
+     "  [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];",
      "}");
 
   MT("for/of",
      "[keyword for]([keyword let] [variable of] [keyword of] [variable something]) {}");
 
   MT("generator",
      "[keyword function*] [variable repeat]([def n]) {",
-     "  [keyword for]([keyword var] [def i] = [number 0]; [variable-2 i] < [variable-2 n]; ++[variable-2 i])",
+     "  [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])",
      "    [keyword yield] [variable-2 i];",
      "}");
 
   MT("fatArrow",
-     "[variable array].[property filter]([def a] => [variable-2 a] + [number 1]);",
+     "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);",
      "[variable a];", // No longer in scope
-     "[keyword let] [variable f] = ([[ [def a], [def b] ]], [def c]) => [variable-2 a] + [variable-2 c];",
+     "[keyword let] [variable f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];",
      "[variable c];");
 
   MT("spread",
      "[keyword function] [variable f]([def a], [meta ...][def b]) {",
      "  [variable something]([variable-2 a], [meta ...][variable-2 b]);",
      "}");
 
   MT("comprehension",
      "[keyword function] [variable f]() {",
-     "  [[ [variable x] + [number 1] [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
-     "  ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] === [string 'blue']));",
+     "  [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];",
+     "  ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));",
      "}");
 
   MT("quasi",
-     "[variable re][string-2 `fofdlakj${][variable x] + ([variable re][string-2 `foo`]) + [number 1][string-2 }fdsa`] + [number 2]");
+     "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]");
+
+  MT("indent_statement",
+     "[keyword var] [variable x] [operator =] [number 10]",
+     "[variable x] [operator +=] [variable y] [operator +]",
+     "  [atom Infinity]",
+     "[keyword debugger];");
+
+  MT("indent_if",
+     "[keyword if] ([number 1])",
+     "  [keyword break];",
+     "[keyword else] [keyword if] ([number 2])",
+     "  [keyword continue];",
+     "[keyword else]",
+     "  [number 10];",
+     "[keyword if] ([number 1]) {",
+     "  [keyword break];",
+     "} [keyword else] [keyword if] ([number 2]) {",
+     "  [keyword continue];",
+     "} [keyword else] {",
+     "  [number 10];",
+     "}");
+
+  MT("indent_for",
+     "[keyword for] ([keyword var] [variable i] [operator =] [number 0];",
+     "     [variable i] [operator <] [number 100];",
+     "     [variable i][operator ++])",
+     "  [variable doSomething]([variable i]);",
+     "[keyword debugger];");
+
+  MT("indent_c_style",
+     "[keyword function] [variable foo]()",
+     "{",
+     "  [keyword debugger];",
+     "}");
+
+  MT("multilinestring",
+     "[keyword var] [variable x] [operator =] [string 'foo\\]",
+     "[string bar'];");
+
+  MT("scary_regexp",
+     "[string-2 /foo[[/]]bar/];");
 })();
--- a/browser/devtools/sourceeditor/test/cm_mode_test.js
+++ b/browser/devtools/sourceeditor/test/cm_mode_test.js
@@ -54,94 +54,79 @@
         text = text.replace(/\[\[|\]\]/g, function(s) {return s.charAt(0);});
         tokens.push(style, text);
         plain += text;
       }
     }
     return {tokens: tokens, plain: plain};
   }
 
-  test.indentation = function(name, mode, tokens, modeName) {
-    var data = parseTokens(tokens);
-    return test((modeName || mode.name) + "_indent_" + name, function() {
-      return compare(data.plain, data.tokens, mode, true);
-    });
-  };
-
   test.mode = function(name, mode, tokens, modeName) {
     var data = parseTokens(tokens);
     return test((modeName || mode.name) + "_" + name, function() {
       return compare(data.plain, data.tokens, mode);
     });
   };
 
-  function compare(text, expected, mode, compareIndentation) {
+  function esc(str) {
+    return str.replace('&', '&amp;').replace('<', '&lt;');
+  }
+
+  function compare(text, expected, mode) {
 
     var expectedOutput = [];
     for (var i = 0; i < expected.length; i += 2) {
       var sty = expected[i];
       if (sty && sty.indexOf(" ")) sty = sty.split(' ').sort().join(' ');
       expectedOutput.push(sty, expected[i + 1]);
     }
 
-    var observedOutput = highlight(text, mode, compareIndentation);
-
-    var pass, passStyle = "";
-    pass = highlightOutputsEqual(expectedOutput, observedOutput);
-    passStyle = pass ? 'mt-pass' : 'mt-fail';
+    var observedOutput = highlight(text, mode);
 
-    var s = '';
-    if (pass) {
-      s += '<div class="mt-test ' + passStyle + '">';
-      s +=   '<pre>' + text.replace('&', '&amp;').replace('<', '&lt;') + '</pre>';
-      s +=   '<div class="cm-s-default">';
-      s +=   prettyPrintOutputTable(observedOutput);
-      s +=   '</div>';
-      s += '</div>';
-      return s;
-    } else {
-      s += '<div class="mt-test ' + passStyle + '">';
-      s +=   '<pre>' + text.replace('&', '&amp;').replace('<', '&lt;') + '</pre>';
+    var s = "";
+    var diff = highlightOutputsDifferent(expectedOutput, observedOutput);
+    if (diff != null) {
+      s += '<div class="mt-test mt-fail">';
+      s +=   '<pre>' + esc(text) + '</pre>';
       s +=   '<div class="cm-s-default">';
       s += 'expected:';
-      s +=   prettyPrintOutputTable(expectedOutput);
+      s +=   prettyPrintOutputTable(expectedOutput, diff);
       s += 'observed:';
-      s +=   prettyPrintOutputTable(observedOutput);
+      s +=   prettyPrintOutputTable(observedOutput, diff);
       s +=   '</div>';
       s += '</div>';
-      throw s;
     }
+    if (observedOutput.indentFailures) {
+      for (var i = 0; i < observedOutput.indentFailures.length; i++)
+        s += "<div class='mt-test mt-fail'>" + esc(observedOutput.indentFailures[i]) + "</div>";
+    }
+    if (s) throw new Failure(s);
   }
 
-  /**
-   * Emulation of CodeMirror's internal highlight routine for testing. Multi-line
-   * input is supported.
-   *
-   * @param string to highlight
-   *
-   * @param mode the mode that will do the actual highlighting
-   *
-   * @return array of [style, token] pairs
-   */
-  function highlight(string, mode, compareIndentation) {
+  function highlight(string, mode) {
     var state = mode.startState()
 
     var lines = string.replace(/\r\n/g,'\n').split('\n');
     var st = [], pos = 0;
     for (var i = 0; i < lines.length; ++i) {
       var line = lines[i], newLine = true;
+      if (mode.indent) {
+        var ws = line.match(/^\s*/)[0];
+        var indent = mode.indent(state, line.slice(ws.length));
+        if (indent != CodeMirror.Pass && indent != ws.length)
+          (st.indentFailures || (st.indentFailures = [])).push(
+            "Indentation of line " + (i + 1) + " is " + indent + " (expected " + ws.length + ")");
+      }
       var stream = new CodeMirror.StringStream(line);
       if (line == "" && mode.blankLine) mode.blankLine(state);
       /* Start copied code from CodeMirror.highlight */
       while (!stream.eol()) {
-				var compare = mode.token(stream, state), substr = stream.current();
-				if(compareIndentation) compare = mode.indent(state) || null;
-        else if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
-
-				stream.start = stream.pos;
+        var compare = mode.token(stream, state), substr = stream.current();
+        if (compare && compare.indexOf(" ") > -1) compare = compare.split(' ').sort().join(' ');
+        stream.start = stream.pos;
         if (pos && st[pos-2] == compare && !newLine) {
           st[pos-1] += substr;
         } else if (substr) {
           st[pos++] = compare; st[pos++] = substr;
         }
         // Give up when line is ridiculously long
         if (stream.pos > 5000) {
           st[pos++] = null; st[pos++] = this.text.slice(stream.pos);
@@ -149,49 +134,32 @@
         }
         newLine = false;
       }
     }
 
     return st;
   }
 
-  /**
-   * Compare two arrays of output from highlight.
-   *
-   * @param o1 array of [style, token] pairs
-   *
-   * @param o2 array of [style, token] pairs
-   *
-   * @return boolean; true iff outputs equal
-   */
-  function highlightOutputsEqual(o1, o2) {
-    if (o1.length != o2.length) return false;
-    for (var i = 0; i < o1.length; ++i)
-      if (o1[i] != o2[i]) return false;
-    return true;
+  function highlightOutputsDifferent(o1, o2) {
+    var minLen = Math.min(o1.length, o2.length);
+    for (var i = 0; i < minLen; ++i)
+      if (o1[i] != o2[i]) return i >> 1;
+    if (o1.length > minLen || o2.length > minLen) return minLen;
   }
 
-  /**
-   * Print tokens and corresponding styles in a table. Spaces in the token are
-   * replaced with 'interpunct' dots (&middot;).
-   *
-   * @param output array of [style, token] pairs
-   *
-   * @return html string
-   */
-  function prettyPrintOutputTable(output) {
+  function prettyPrintOutputTable(output, diffAt) {
     var s = '<table class="mt-output">';
     s += '<tr>';
     for (var i = 0; i < output.length; i += 2) {
       var style = output[i], val = output[i+1];
       s +=
-      '<td class="mt-token">' +
-        '<span class="cm-' + String(style).replace(/ +/g, " cm-") + '">' +
-        val.replace(/ /g,'\xb7').replace('&', '&amp;').replace('<', '&lt;') +
+      '<td class="mt-token"' + (i == diffAt * 2 ? " style='background: pink'" : "") + '>' +
+        '<span class="cm-' + esc(String(style)) + '">' +
+        esc(val.replace(/ /g,'\xb7')) +
         '</span>' +
         '</td>';
     }
     s += '</tr><tr>';
     for (var i = 0; i < output.length; i += 2) {
       s += '<td class="mt-style"><span>' + (output[i] || null) + '</span></td>';
     }
     s += '</table>';
--- a/browser/devtools/sourceeditor/test/cm_test.js
+++ b/browser/devtools/sourceeditor/test/cm_test.js
@@ -431,16 +431,46 @@ testCM("markTextUndo", function(cm) {
 testCM("markTextStayGone", function(cm) {
   var m1 = cm.markText(Pos(0, 0), Pos(0, 1));
   cm.replaceRange("hi", Pos(0, 2));
   m1.clear();
   cm.undo();
   eq(m1.find(), null);
 }, {value: "hello"});
 
+testCM("markTextAllowEmpty", function(cm) {
+  var m1 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false});
+  is(m1.find());
+  cm.replaceRange("x", Pos(0, 0));
+  is(m1.find());
+  cm.replaceRange("y", Pos(0, 2));
+  is(m1.find());
+  cm.replaceRange("z", Pos(0, 3), Pos(0, 4));
+  is(!m1.find());
+  var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {clearWhenEmpty: false,
+                                              inclusiveLeft: true,
+                                              inclusiveRight: true});
+  cm.replaceRange("q", Pos(0, 1), Pos(0, 2));
+  is(m2.find());
+  cm.replaceRange("", Pos(0, 0), Pos(0, 3));
+  is(!m2.find());
+  var m3 = cm.markText(Pos(0, 1), Pos(0, 1), {clearWhenEmpty: false});
+  cm.replaceRange("a", Pos(0, 3));
+  is(m3.find());
+  cm.replaceRange("b", Pos(0, 1));
+  is(!m3.find());
+}, {value: "abcde"});
+
+testCM("markTextStacked", function(cm) {
+  var m1 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+  var m2 = cm.markText(Pos(0, 0), Pos(0, 0), {clearWhenEmpty: false});
+  cm.replaceRange("B", Pos(0, 1));
+  is(m1.find() && m2.find());
+}, {value: "A"});
+
 testCM("undoPreservesNewMarks", function(cm) {
   cm.markText(Pos(0, 3), Pos(0, 4));
   cm.markText(Pos(1, 1), Pos(1, 3));
   cm.replaceRange("", Pos(0, 3), Pos(3, 1));
   var mBefore = cm.markText(Pos(0, 0), Pos(0, 1));
   var mAfter = cm.markText(Pos(0, 5), Pos(0, 6));
   var mAround = cm.markText(Pos(0, 2), Pos(0, 4));
   cm.undo();
@@ -707,18 +737,18 @@ testCM("collapsedLines", function(cm) {
 
 testCM("collapsedRangeCoordsChar", function(cm) {
   var pos_1_3 = cm.charCoords(Pos(1, 3));
   pos_1_3.left += 2; pos_1_3.top += 2;
   var opts = {collapsed: true, inclusiveLeft: true, inclusiveRight: true};
   var m1 = cm.markText(Pos(0, 0), Pos(2, 0), opts);
   eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
   m1.clear();
-  var m1 = cm.markText(Pos(0, 0), Pos(1, 1), opts);
-  var m2 = cm.markText(Pos(1, 1), Pos(2, 0), opts);
+  var m1 = cm.markText(Pos(0, 0), Pos(1, 1), {collapsed: true, inclusiveLeft: true});
+  var m2 = cm.markText(Pos(1, 1), Pos(2, 0), {collapsed: true, inclusiveRight: true});
   eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
   m1.clear(); m2.clear();
   var m1 = cm.markText(Pos(0, 0), Pos(1, 6), opts);
   eqPos(cm.coordsChar(pos_1_3), Pos(3, 3));
 }, {value: "123456\nabcdef\nghijkl\nmnopqr\n"});
 
 testCM("hiddenLinesAutoUnfold", function(cm) {
   var range = foldLines(cm, 1, 3, true), cleared = 0;
@@ -836,16 +866,33 @@ testCM("badNestedFold", function(cm) {
   cm.markText(Pos(0, 2), Pos(3, 2), {collapsed: true});
   var caught;
   try {cm.markText(Pos(0, 1), Pos(0, 3), {collapsed: true});}
   catch(e) {caught = e;}
   is(caught instanceof Error, "no error");
   is(/overlap/i.test(caught.message), "wrong error");
 });
 
+testCM("nestedFoldOnSide", function(cm) {
+  var m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true, inclusiveRight: true});
+  var m2 = cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true});
+  cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true}).clear();
+  try { cm.markText(Pos(0, 1), Pos(0, 2), {collapsed: true, inclusiveLeft: true}); }
+  catch(e) { var caught = e; }
+  is(caught && /overlap/i.test(caught.message));
+  var m3 = cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true});
+  var m4 = cm.markText(Pos(2, 0), Pos(2, 1), {collapse: true, inclusiveRight: true});
+  m1.clear(); m4.clear();
+  m1 = cm.markText(Pos(0, 1), Pos(2, 1), {collapsed: true});
+  cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true}).clear();
+  try { cm.markText(Pos(2, 0), Pos(2, 1), {collapsed: true, inclusiveRight: true}); }
+  catch(e) { var caught = e; }
+  is(caught && /overlap/i.test(caught.message));
+}, {value: "ab\ncd\ef"});
+
 testCM("wrappingInlineWidget", function(cm) {
   cm.setSize("11em");
   var w = document.createElement("span");
   w.style.color = "red";
   w.innerHTML = "one two three four";
   cm.markText(Pos(0, 6), Pos(0, 9), {replacedWith: w});
   var cur0 = cm.cursorCoords(Pos(0, 0)), cur1 = cm.cursorCoords(Pos(0, 10));
   is(cur0.top < cur1.top);
@@ -968,16 +1015,34 @@ testCM("moveVstuck", function(cm) {
     cm.setSize(w);
     if (lines.offsetHeight <= 3.5 * h0) break;
   }
   cm.setCursor(Pos(0, val.length - 1));
   cm.moveV(-1, "line");
   eqPos(cm.getCursor(), Pos(0, 26));
 }, {lineWrapping: true}, ie_lt8 || opera_lt10);
 
+testCM("collapseOnMove", function(cm) {
+  cm.setSelection(Pos(0, 1), Pos(2, 4));
+  cm.execCommand("goLineUp");
+  is(!cm.somethingSelected());
+  eqPos(cm.getCursor(), Pos(0, 1));
+  cm.setSelection(Pos(0, 1), Pos(2, 4));
+  cm.execCommand("goPageDown");
+  is(!cm.somethingSelected());
+  eqPos(cm.getCursor(), Pos(2, 4));
+  cm.execCommand("goLineUp");
+  cm.execCommand("goLineUp");
+  eqPos(cm.getCursor(), Pos(0, 4));
+  cm.setSelection(Pos(0, 1), Pos(2, 4));
+  cm.execCommand("goCharLeft");
+  is(!cm.somethingSelected());
+  eqPos(cm.getCursor(), Pos(0, 1));
+}, {value: "aaaaa\nb\nccccc"});
+
 testCM("clickTab", function(cm) {
   var p0 = cm.charCoords(Pos(0, 0));
   eqPos(cm.coordsChar({left: p0.left + 5, top: p0.top + 5}), Pos(0, 0));
   eqPos(cm.coordsChar({left: p0.right - 5, top: p0.top + 5}), Pos(0, 1));
 }, {value: "\t\n\n", lineWrapping: true, tabSize: 8});
 
 testCM("verticalScroll", function(cm) {
   cm.setSize(100, 200);
@@ -1136,17 +1201,17 @@ testCM("verticalMovementCommandsWrapping
     if (cur.line == 1) eq(cur.ch, 5);
     if (cur.line == 2) { eq(cur.ch, 1); break; }
   }
 }, {value: "a very long line that wraps around somehow so that we can test cursor movement\nshortone\nk",
     lineWrapping: true});
 
 testCM("rtlMovement", function(cm) {
   forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج",
-           "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر",
+           "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق",
            "<img src=\"/בדיקה3.jpg\">"], function(line) {
     var inv = line.charAt(0) == "خ";
     cm.setValue(line + "\n"); cm.execCommand(inv ? "goLineEnd" : "goLineStart");
     var cursor = byClassName(cm.getWrapperElement(), "CodeMirror-cursor")[0];
     var prevX = cursor.offsetLeft, prevY = cursor.offsetTop;
     for (var i = 0; i <= line.length; ++i) {
       cm.execCommand("goCharRight");
       if (i == line.length) is(cursor.offsetTop > prevY, "next line");
@@ -1409,16 +1474,31 @@ testCM("dirtyBit", function(cm) {
   cm.markClean();
   eq(cm.isClean(), true);
   cm.undo();
   eq(cm.isClean(), false);
   cm.redo();
   eq(cm.isClean(), true);
 });
 
+testCM("changeGeneration", function(cm) {
+  cm.replaceSelection("x", null, "+insert");
+  var softGen = cm.changeGeneration();
+  cm.replaceSelection("x", null, "+insert");
+  cm.undo();
+  eq(cm.getValue(), "");
+  is(!cm.isClean(softGen));
+  cm.replaceSelection("x", null, "+insert");
+  var hardGen = cm.changeGeneration(true);
+  cm.replaceSelection("x", null, "+insert");
+  cm.undo();
+  eq(cm.getValue(), "x");
+  is(cm.isClean(hardGen));
+});
+
 testCM("addKeyMap", function(cm) {
   function sendKey(code) {
     cm.triggerOnKeyDown({type: "keydown", keyCode: code,
                          preventDefault: function(){}, stopPropagation: function(){}});
   }
 
   sendKey(39);
   eqPos(cm.getCursor(), Pos(0, 1));
@@ -1555,8 +1635,27 @@ testCM("lineStyleFromMode", function(cm)
   eq(bracketElts.length, 1);
   eq(bracketElts[0].nodeName, "PRE");
   is(!/brackets.*brackets/.test(bracketElts[0].className));
   var parenElts = byClassName(cm.getWrapperElement(), "parens");
   eq(parenElts.length, 1);
   eq(parenElts[0].nodeName, "DIV");
   is(!/parens.*parens/.test(parenElts[0].className));
 }, {value: "line1: [br] [br]\nline2: (par) (par)\nline3: nothing"});
+
+CodeMirror.registerHelper("xxx", "a", "A");
+CodeMirror.registerHelper("xxx", "b", "B");
+CodeMirror.defineMode("yyy", function() {
+  return {
+    token: function(stream) { stream.skipToEnd(); },
+    xxx: ["a", "b", "q"]
+  };
+});
+CodeMirror.registerGlobalHelper("xxx", "c", function(m) { return m.enableC; }, "C");
+
+testCM("helpers", function(cm) {
+  cm.setOption("mode", "yyy");
+  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "A/B");
+  cm.setOption("mode", {name: "yyy", modeProps: {xxx: "b", enableC: true}});
+  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "B/C");
+  cm.setOption("mode", "javascript");
+  eq(cm.getHelpers(Pos(0, 0), "xxx").join("/"), "");
+});
--- a/browser/devtools/sourceeditor/test/cm_vim_test.js
+++ b/browser/devtools/sourceeditor/test/cm_vim_test.js
@@ -91,16 +91,22 @@ var seekInside = {
   start: { line: seekBraceLine.line, ch: 14 },
   end: { line: seekBraceLine.line, ch: 11 }
 };
 
 function copyCursor(cur) {
   return { ch: cur.ch, line: cur.line };
 }
 
+function forEach(arr, func) {
+  for (var i = 0; i < arr.length; i++) {
+    func(arr[i]);
+  }
+}
+
 function testVim(name, run, opts, expectedFail) {
   var vimOpts = {
     lineNumbers: true,
     vimMode: true,
     showCursorWhenSelecting: true,
     value: code
   };
   for (var prop in opts) {
@@ -184,17 +190,17 @@ function testVim(name, run, opts, expect
       }
     }
     CodeMirror.Vim.resetVimGlobalState_();
     var successful = false;
     try {
       run(cm, vim, helpers);
       successful = true;
     } finally {
-      if ((debug && !successful) || verbose) {
+      if (!successful || verbose) {
         place.style.visibility = "visible";
       } else {
         place.removeChild(cm.getWrapperElement());
       }
     }
   }, expectedFail);
 };
 testVim('qq@q', function(cm, vim, helpers) {
@@ -1042,17 +1048,17 @@ testVim('ctrl-x', function(cm, vim, help
   cm.setCursor(0, 0);
   helpers.doKeys('<C-x>');
   eq('-1', cm.getValue());
   helpers.assertCursorAt(0, 1);
   helpers.doKeys('2','<C-x>');
   eq('-3', cm.getValue());
 }, {value: '0'});
 testVim('<C-x>/<C-a> search forward', function(cm, vim, helpers) {
-  ['<C-x>', '<C-a>'].forEach(function(key) {
+  forEach(['<C-x>', '<C-a>'], function(key) {
     cm.setCursor(0, 0);
     helpers.doKeys(key);
     helpers.assertCursorAt(0, 5);
     helpers.doKeys('l');
     helpers.doKeys(key);
     helpers.assertCursorAt(0, 10);
     cm.setCursor(0, 11);
     helpers.doKeys(key);
@@ -1992,16 +1998,43 @@ testVim('zb==z-', function(cm, vim, help
 });
 testVim('zz==z.', function(cm, vim, helpers){
   eq(zVals[1], zVals[4]);
 });
 testVim('zt==z<CR>', function(cm, vim, helpers){
   eq(zVals[2], zVals[5]);
 });
 
+var scrollMotionSandbox =
+  '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+  '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+  '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'
+  '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
+testVim('scrollMotion', function(cm, vim, helpers){
+  var prevCursor, prevScrollInfo;
+  cm.setCursor(0, 0);
+  // ctrl-y at the top of the file should have no effect.
+  helpers.doKeys('<C-y>');
+  eq(0, cm.getCursor().line);
+  prevScrollInfo = cm.getScrollInfo();
+  helpers.doKeys('<C-e>');
+  eq(1, cm.getCursor().line);
+  eq(true, prevScrollInfo.top < cm.getScrollInfo().top);
+  // Jump to the end of the sandbox.
+  cm.setCursor(1000, 0);
+  prevCursor = cm.getCursor();
+  // ctrl-e at the bottom of the file should have no effect.
+  helpers.doKeys('<C-e>');
+  eq(prevCursor.line, cm.getCursor().line);
+  prevScrollInfo = cm.getScrollInfo();
+  helpers.doKeys('<C-y>');
+  eq(prevCursor.line - 1, cm.getCursor().line);
+  eq(true, prevScrollInfo.top > cm.getScrollInfo().top);
+}, { value: scrollMotionSandbox});
+
 var squareBracketMotionSandbox = ''+
   '({\n'+//0
   '  ({\n'+//11
   '  /*comment {\n'+//2
   '            */(\n'+//3
   '#else                \n'+//4
   '  /*       )\n'+//5
   '#if        }\n'+//6
@@ -2075,17 +2108,17 @@ testVim('[(, ])', function(cm, vim, help
   helpers.doKeys('2', ']', ')');
   helpers.assertCursorAt(8,0);
   helpers.doKeys('[', '(');
   helpers.assertCursorAt(0,0);
   helpers.doKeys(']', ')');
   helpers.assertCursorAt(8,0);
 }, { value: squareBracketMotionSandbox});
 testVim('[*, ]*, [/, ]/', function(cm, vim, helpers) {
-  ['*', '/'].forEach(function(key){
+  forEach(['*', '/'], function(key){
     cm.setCursor(7, 0);
     helpers.doKeys('2', '[', key);
     helpers.assertCursorAt(2,2);
     helpers.doKeys('2', ']', key);
     helpers.assertCursorAt(7,5);
   });
 }, { value: squareBracketMotionSandbox});
 testVim('[#, ]#', function(cm, vim, helpers) {
@@ -2363,16 +2396,36 @@ testVim('ex_map_key2ex', function(cm, vi
     written = true;
     actualCm = cm;
   };
   helpers.doKeys('a');
   CodeMirror.commands.save = tmp;
   eq(written, true);
   eq(actualCm, cm);
 });
+testVim('ex_map_key2key_visual_api', function(cm, vim, helpers) {
+  CodeMirror.Vim.map('b', ':w', 'visual');
+  var tmp = CodeMirror.commands.save;
+  var written = false;
+  var actualCm;
+  CodeMirror.commands.save = function(cm) {
+    written = true;
+    actualCm = cm;
+  };
+  // Mapping should not work in normal mode.
+  helpers.doKeys('b');
+  eq(written, false);
+  // Mapping should work in visual mode.
+  helpers.doKeys('v', 'b');
+  eq(written, true);
+  eq(actualCm, cm);
+
+  CodeMirror.commands.save = tmp;
+});
+
 // Testing registration of functions as ex-commands and mapping to <Key>-keys
 testVim('ex_api_test', function(cm, vim, helpers) {
   var res=false;
   var val='from';
   CodeMirror.Vim.defineEx('extest','ext',function(cm,params){
     if(params.args)val=params.args[0];
     else res=true;
   });
--- a/browser/devtools/sourceeditor/test/codemirror.html
+++ b/browser/devtools/sourceeditor/test/codemirror.html
@@ -95,43 +95,52 @@
       function esc(str) {
         return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
       }
 
       var output = document.getElementById("output"),
           progress = document.getElementById("progress"),
           progressRan = document.getElementById("progress_ran").childNodes[0],
           progressTotal = document.getElementById("progress_total").childNodes[0];
+
       var count = 0,
           failed = 0,
+          skipped = 0,
           bad = "",
           running = false, // Flag that states tests are running
-         quit = false, // Flag to quit tests ASAP
-         verbose = false; // Adds message for *every* test to output
+          quit = false, // Flag to quit tests ASAP
+          verbose = false, // Adds message for *every* test to output
+          phantom = false;
 
       function runHarness(){
         if (running) {
           quit = true;
           setStatus("Restarting tests...", '', true);
           setTimeout(function(){runHarness();}, 500);
           return;
         }
+        filters = [];
+        verbose = false;
         if (window.location.hash.substr(1)){
-          debug = window.location.hash.substr(1).split(",");
-        } else {
-          debug = null;
+          var strings = window.location.hash.substr(1).split(",");
+          while (strings.length) {
+            var s = strings.shift();
+            if (s === "verbose")
+              verbose = true;
+            else
+              filters.push(parseTestFilter(decodeURIComponent(s)));
+          }
         }
         quit = false;
         running = true;
         setStatus("Loading tests...");
         count = 0;
         failed = 0;
+        skipped = 0;
         bad = "";
-        verbose = false;
-        debugUsed = Array();
         totalTests = countTests();
         progressTotal.nodeValue = " of " + totalTests;
         progressRan.nodeValue = count;
         output.innerHTML = '';
         document.getElementById("testground").innerHTML = "<form>" +
           "<textarea id=\"code\" name=\"code\"></textarea>" +
           "<input type=submit value=ok name=submit>" +
           "</form>";
@@ -154,22 +163,26 @@
         var newMessage = document.createElement("dd");
         newMessage.innerHTML = code;
         newOutput.appendChild(newTitle);
         newOutput.appendChild(newMessage);
         output.appendChild(newOutput);
       }
       function displayTest(type, name, customMessage) {
         var message = "???";
-        if (type != "done") ++count;
+        if (type != "done" && type != "skipped") ++count;
         progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
         progressRan.nodeValue = count;
         if (type == "ok") {
           message = "Test '" + name + "' succeeded";
           if (!verbose) customMessage = false;
+        } else if (type == "skipped") {
+          message = "Test '" + name + "' skipped";
+          ++skipped;
+          if (!verbose) customMessage = false;
         } else if (type == "expected") {
           message = "Test '" + name + "' failed as expected";
           if (!verbose) customMessage = false;
         } else if (type == "error" || type == "fail") {
           ++failed;
           message = "Test '" + name + "' failed";
         } else if (type == "done") {
           if (failed) {
@@ -177,25 +190,21 @@
             message = failed + " failure" + (failed > 1 ? "s" : "");
           } else if (count < totalTests) {
             failed = totalTests - count;
             type += " fail";
             message = failed + " failure" + (failed > 1 ? "s" : "");
           } else {
             type += " ok";
             message = "All passed";
+            if (skipped) {
+              message += " (" + skipped + " skipped)";
+            }
           }
-          if (debug && debug.length) {
-            var bogusTests = totalTests - count;
-            message += " — " + bogusTests + " nonexistent test" +
-              (bogusTests > 1 ? "s" : "") + " requested by location.hash: " +
-              "`" + debug.join("`, `") + "`";
-          } else {
-            progressTotal.nodeValue = '';
-          }
+          progressTotal.nodeValue = '';
           customMessage = true; // Hack to avoid adding to output
         }
         if (verbose && !customMessage)  customMessage = message;
         setStatus(message, type);
         if (customMessage && customMessage.length > 0) {
           addOutput(name, type, customMessage);
         }
       }
--- a/browser/devtools/sourceeditor/test/vimemacs.html
+++ b/browser/devtools/sourceeditor/test/vimemacs.html
@@ -95,44 +95,52 @@
       function esc(str) {
         return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
       }
 
       var output = document.getElementById("output"),
           progress = document.getElementById("progress"),
           progressRan = document.getElementById("progress_ran").childNodes[0],
           progressTotal = document.getElementById("progress_total").childNodes[0];
+
       var count = 0,
           failed = 0,
+          skipped = 0,
           bad = "",
           running = false, // Flag that states tests are running
           quit = false, // Flag to quit tests ASAP
           verbose = false, // Adds message for *every* test to output
           phantom = false;
 
       function runHarness(){
         if (running) {
           quit = true;
           setStatus("Restarting tests...", '', true);
           setTimeout(function(){runHarness();}, 500);
           return;
         }
+        filters = [];
+        verbose = false;
         if (window.location.hash.substr(1)){
-          debug = window.location.hash.substr(1).split(",");
-        } else {
-          debug = null;
+          var strings = window.location.hash.substr(1).split(",");
+          while (strings.length) {
+            var s = strings.shift();
+            if (s === "verbose")
+              verbose = true;
+            else
+              filters.push(parseTestFilter(decodeURIComponent(s)));
+          }
         }
         quit = false;
         running = true;
         setStatus("Loading tests...");
         count = 0;
         failed = 0;
+        skipped = 0;
         bad = "";
-        verbose = false;
-        debugUsed = Array();
         totalTests = countTests();
         progressTotal.nodeValue = " of " + totalTests;
         progressRan.nodeValue = count;
         output.innerHTML = '';
         document.getElementById("testground").innerHTML = "<form>" +
           "<textarea id=\"code\" name=\"code\"></textarea>" +
           "<input type=submit value=ok name=submit>" +
           "</form>";
@@ -155,22 +163,26 @@
         var newMessage = document.createElement("dd");
         newMessage.innerHTML = code;
         newOutput.appendChild(newTitle);
         newOutput.appendChild(newMessage);
         output.appendChild(newOutput);
       }
       function displayTest(type, name, customMessage) {
         var message = "???";
-        if (type != "done") ++count;
+        if (type != "done" && type != "skipped") ++count;
         progress.style.width = (count * (progress.parentNode.clientWidth - 2) / totalTests) + "px";
         progressRan.nodeValue = count;
         if (type == "ok") {
           message = "Test '" + name + "' succeeded";
           if (!verbose) customMessage = false;
+        } else if (type == "skipped") {
+          message = "Test '" + name + "' skipped";
+          ++skipped;
+          if (!verbose) customMessage = false;
         } else if (type == "expected") {
           message = "Test '" + name + "' failed as expected";
           if (!verbose) customMessage = false;
         } else if (type == "error" || type == "fail") {
           ++failed;
           message = "Test '" + name + "' failed";
         } else if (type == "done") {
           if (failed) {
@@ -178,25 +190,21 @@
             message = failed + " failure" + (failed > 1 ? "s" : "");
           } else if (count < totalTests) {
             failed = totalTests - count;
             type += " fail";
             message = failed + " failure" + (failed > 1 ? "s" : "");
           } else {
             type += " ok";
             message = "All passed";
+            if (skipped) {
+              message += " (" + skipped + " skipped)";
+            }
           }
-          if (debug && debug.length) {
-            var bogusTests = totalTests - count;
-            message += " — " + bogusTests + " nonexistent test" +
-              (bogusTests > 1 ? "s" : "") + " requested by location.hash: " +
-              "`" + debug.join("`, `") + "`";
-          } else {
-            progressTotal.nodeValue = '';
-          }
+          progressTotal.nodeValue = '';
           customMessage = true; // Hack to avoid adding to output
         }
         if (verbose && !customMessage)  customMessage = message;
         setStatus(message, type);
         if (customMessage && customMessage.length > 0) {
           addOutput(name, type, customMessage);
         }
       }
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -108,17 +108,16 @@ support-files =
 [browser_bug_638949_copy_link_location.js]
 [browser_bug_862916_console_dir_and_filter_off.js]
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
 [browser_cached_messages.js]
 [browser_console.js]
-skip-if = os == "linux" # Intermittent failures, bug 952865
 [browser_console_addonsdk_loader_exception.js]
 [browser_console_clear_on_reload.js]
 [browser_console_consolejsm_output.js]
 [browser_console_dead_objects.js]
 [browser_console_error_source_click.js]
 [browser_console_filters.js]
 [browser_console_iframe_messages.js]
 [browser_console_keyboard_accessibility.js]
--- a/browser/devtools/webconsole/test/browser_console.js
+++ b/browser/devtools/webconsole/test/browser_console.js
@@ -45,55 +45,44 @@ function consoleOpened(hud)
   hud.jsterm.execute("document.location.href");
 
   // Check for network requests.
   let xhr = new XMLHttpRequest();
   xhr.onload = () => console.log("xhr loaded, status is: " + xhr.status);
   xhr.open("get", TEST_URI, true);
   xhr.send();
 
-  let chromeConsole = -1;
-  let contentConsole = -1;
-  let execValue = -1;
-  let exception = -1;
-  let xhrRequest = false;
-
-  let output = hud.outputNode;
-  function performChecks()
-  {
-    let text = output.textContent;
-    chromeConsole = text.indexOf("bug587757a");
-    contentConsole = text.indexOf("bug587757b");
-    execValue = text.indexOf("browser.xul");
-    exception = text.indexOf("foobarExceptionBug587757");
-    xhrRequest = text.indexOf("test-console.html");
-  }
-
-  function showResults()
-  {
-    isnot(chromeConsole, -1, "chrome window console.log() is displayed");
-    isnot(contentConsole, -1, "content window console.log() is displayed");
-    isnot(execValue, -1, "jsterm eval result is displayed");
-    isnot(exception, -1, "exception is displayed");
-    isnot(xhrRequest, -1, "xhr request is displayed");
-  }
-
-  waitForSuccess({
-    name: "messages displayed",
-    validatorFn: () => {
-      performChecks();
-      return chromeConsole > -1 &&
-             contentConsole > -1 &&
-             execValue > -1 &&
-             exception > -1 &&
-             xhrRequest > -1;
-    },
-    successFn: () => {
-      showResults();
-      executeSoon(finishTest);
-    },
-    failureFn: () => {
-      showResults();
-      info("output: " + output.textContent);
-      executeSoon(finishTest);
-    },
-  });
+  waitForMessages({
+    webconsole: hud,
+    messages: [
+      {
+        name: "chrome window console.log() is displayed",
+        text: "bug587757a",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      },
+      {
+        name: "content window console.log() is displayed",
+        text: "bug587757b",
+        category: CATEGORY_WEBDEV,
+        severity: SEVERITY_LOG,
+      },
+      {
+        name: "jsterm eval result",
+        text: "browser.xul",
+        category: CATEGORY_OUTPUT,
+        severity: SEVERITY_LOG,
+      },
+      {
+        name: "exception message",
+        text: "foobarExceptionBug587757",
+        category: CATEGORY_JS,
+        severity: SEVERITY_ERROR,
+      },
+      {
+        name: "network message",
+        text: "test-console.html",
+        category: CATEGORY_NETWORK,
+        severity: SEVERITY_LOG,
+      },
+    ],
+  }).then(finishTest);
 }
--- a/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
+++ b/browser/devtools/webconsole/test/browser_console_addonsdk_loader_exception.js
@@ -6,16 +6,18 @@
 // Check that exceptions from scripts loaded with the addon-sdk loader are
 // opened correctly in View Source from the Browser Console.
 // See bug 866950.
 
 const TEST_URI = "data:text/html;charset=utf8,<p>hello world from bug 866950";
 
 function test()
 {
+  requestLongerTimeout(2);
+
   let webconsole, browserconsole;
 
   addTab(TEST_URI);
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
 
     openConsole(null, consoleOpened);
   }, true);
--- a/browser/metro/base/content/bookmarks.js
+++ b/browser/metro/base/content/bookmarks.js
@@ -36,32 +36,41 @@ var Bookmarks = {
       event.initEvent("BookmarkCreated", true, false);
       window.dispatchEvent(event);
 
       if (callback)
         callback(bookmarkId);
     });
   },
 
+  _isMetroBookmark: function(aItemId) {
+    return PlacesUtils.bookmarks.getFolderIdForItem(aItemId) == Bookmarks.metroRoot;
+  },
+
   isURIBookmarked: function bh_isURIBookmarked(aURI, callback) {
     if (!callback)
       return;
     PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
-      callback(aItemIds && aItemIds.length > 0);
+      callback(aItemIds && aItemIds.length > 0 && aItemIds.some(this._isMetroBookmark));
     }, this);
   },
 
   removeForURI: function bh_removeForURI(aURI, callback) {
     // XXX blargle xpconnect! might not matter, but a method on
     // nsINavBookmarksService that takes an array of items to
     // delete would be faster. better yet, a method that takes a URI!
-    PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
-      aItemIds.forEach(PlacesUtils.bookmarks.removeItem);
+    PlacesUtils.asyncGetBookmarkIds(aURI, (aItemIds) => {
+      aItemIds.forEach((aItemId) => {
+        if (this._isMetroBookmark(aItemId)) {
+          PlacesUtils.bookmarks.removeItem(aItemId);
+        }
+      });
+
       if (callback)
-        callback(aURI, aItemIds);
+        callback();
 
       // XXX Used for browser-chrome tests
       let event = document.createEvent("Events");
       event.initEvent("BookmarkRemoved", true, false);
       window.dispatchEvent(event);
     });
   }
 };
--- a/browser/metro/base/content/browser-ui.js
+++ b/browser/metro/base/content/browser-ui.js
@@ -1099,32 +1099,37 @@ var BrowserUI = {
   },
 
   confirmSanitizeDialog: function () {
     let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
     let title = bundle.GetStringFromName("clearPrivateData.title");
     let message = bundle.GetStringFromName("clearPrivateData.message");
     let clearbutton = bundle.GetStringFromName("clearPrivateData.clearButton");
 
+    let prefsClearButton = document.getElementById("prefs-clear-data");
+    prefsClearButton.disabled = true; 
+
     let buttonPressed = Services.prompt.confirmEx(
                           null,
                           title,
                           message,
                           Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING +
                           Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_CANCEL,
                           clearbutton,
                           null,
                           null,
                           null,
                           { value: false });
 
     // Clicking 'Clear' will call onSanitize().
     if (buttonPressed === 0) {
       SanitizeUI.onSanitize();
     }
+
+    prefsClearButton.disabled = false;
   },
 };
 
 var PanelUI = {
   get _panels() { return document.getElementById("panel-items"); },
 
   get isVisible() {
     return !Elements.panelUI.hidden;
--- a/browser/metro/base/tests/mochitest/browser_form_selects.js
+++ b/browser/metro/base/tests/mochitest/browser_form_selects.js
@@ -30,17 +30,19 @@ gTests.push({
     sendNativeTap(select);
     yield promise;
 
     // tap every option
     for (let node of SelectHelperUI._listbox.childNodes) {
       sendNativeTap(node);
     }
 
-    yield waitForMs(100);
+    yield waitForCondition2(function () {
+      return Browser.selectedTab.browser.contentWindow.document.getElementById("opt9").selected;
+      }, "waiting for last option to select");
 
     // check the menu state
     for (let node of SelectHelperUI._listbox.childNodes) {
       ok(node.selected, "option is selected");
     }
 
     // check the underlying form state
     for (let index = 1; index < 10; index++) {
@@ -48,17 +50,19 @@ gTests.push({
       ok(option.selected, "opt" + index + " form option selected");
     }
 
     // tap every option again
     for (let node of SelectHelperUI._listbox.childNodes) {
       sendNativeTap(node);
     }
 
-    yield waitForMs(100);
+    yield waitForCondition2(function () {
+      return !Browser.selectedTab.browser.contentWindow.document.getElementById("opt9").selected;
+      }, "waiting for last option to deselect");
 
     // check the menu state
     for (let node of SelectHelperUI._listbox.childNodes) {
       ok(!node.selected, "option is not selected");
     }
 
     // check the underlying form state
     for (let index = 1; index < 10; index++) {
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1889,17 +1889,17 @@ chatbox {
 #main-window[customize-entered] #tab-view-deck {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
                     linear-gradient(to bottom, #bcbcbc, #b5b5b5);
   background-attachment: fixed;
 }
 
 #main-window:-moz-any([customize-entering],[customize-entered]) #tab-view-deck {
-  padding: 2em;
+  padding: 0 2em 2em;
 }
 
 #main-window[customize-entered] #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar),
 #main-window[customize-entered] #customization-container {
   border: 3px solid hsla(0,0%,0%,.1);
   border-top-width: 0;
   background-clip: padding-box;
   background-origin: padding-box;
index 6359d7262a286a34074f6f245636ad15a95993b5..8daaffe9556f3fc93fbfa7883fa0639b72e0a26e
GIT binary patch
literal 568
zc$@(^0>}M{P)<h;3K|Lk000e1NJLTq001xm000mO1^@s6P_F#300061Nkl<Zc-qC5
zziPrz6vj;iHwUvRbTFA+I&^TTeT@W1adB+z;@?;3(4ns(g1&=!4VCDu;FKQWyYNx&
z^+;N1;D?;U7x}*A-g8s>`22El5OQIU+DV@0HV(q|+F$De428Myc1y=bLZMD|ZlGQR
zI$MBHsMDYGWqJ+hY-VagmFl^HdR6F31~#7w%NqeaVGSLJFy=F1c_V-)tf31n=7&P!
z3V{2&(A7AAFKozc&k<l?YUq8K_k|6a?KuJrOaZ-5#`<LT(GlRu)X;k-Ktje&LfaMq
z{S);P1W3r(Nod;wpnsxXf_{sPjf4kV0Q5)HYncEmGBr^)@r<-91Xz)&iL!}jq+Oxc
zB~u+`6TgtQYXTg}RHRItkhD7lIFhMInJgh`cj&#7sX)2EM%q^sASWtHfC_0Z2*6iG
zF;F4x#j;e&%Kc^1PKvyN<}cqs^Dp0kZ4Zo<08?cTj1>b@We+?n_jh;?C{u_iFol>R
zQ;2a*A=f!@#*>b)<|EYSJI{eLo^*sYAE7=g=KzQ32m#i7fO_OQ2TCxR!JJ0+bFaW;
zmfv?}mc0^~v8e%Fz$Y<>NEP!bndbl|M5>ri*}Nr0Uoqb|<~ac43$d^ewiuN=)QR9L
zU!%Sd3kzY3QMp5%(6VviVQt5lG86p9=zET@(c&P$zwr&)(Yse9b!qtk0000<MNUMn
GLSTYC3IJ&U
index e230b9c1bc704b75416a472f9a1fe53faf3c7d70..ed04d29b4ae69f5daaf73fc14cd41f6605e4d646
GIT binary patch
literal 811
zc$@(%1JwM9P)<h;3K|Lk000e1NJLTq001xm000mO1ONa4`lmEH0008=Nkl<Zcmbu&
zPl!|n0LSsqdo#1onQ`1^-QCt{$G~;7!={pUvBC~QY-xq2=|K-RA#|&V=psS{l64V-
z1xXMoRy-(l6*~CuvJtCG31J8u>%Y`Bb@tE9yjM^SGlHi+-|G*4ffjpySWuXqw!veJ
z+|=W`B_8Kvc@KQ4MVLrb`YYWP6+Kb;es`{0w?<E+zu~_|mW?bKF+vaeK*=&qiV7f)
zg`l7aguK@u9gRMVM1mv{CZQWa^m%ktKX7=N3l3`)IO0O{qNrk%R)IHF6d|mH*#mu<
z!s^-0vyy)cws$_)m&qPT!o=awalKwwq3M0E>UCU*z*c$Bc%G&gESI-cKo|~pE=|Q9
z_2KleBuK<T>^13nD(+kw4u@>F7F9pf^ph&q+KwS5)ZC)!ni@)m2#edIdM$kR>Xr*z
z)RVhW9LL@|yW(msjOxX0MRtPy&f^xY<Ge4xPOJiFP2wRQVA46TigfsNn9QXHPmWEp
z|CIbcgB7QK`5;?Qm9Z9l2YLY<vk$9K0++x60OMZ4lceL0V6G8#orB++gU&OJ4@n|)
zgGudv2aAyd_q6*H<OnmhMi3+ivj?^I^@1RYW#%*d0m|Av1#Y7XI<;E_s)U7m^&k~{
zLz{SWDy|2)`#HwI4()b=aZG_B?N)*tgv&3^r&4k1?djFinypU%eK4OIyg0})@Tqk)
zt@8;uhH>z+GEHSKfpNl(C$iN>P)HUX3ic%BB$JebJ&O(%l17lNZalG(QDewCZLa|x
z2JCfO4r4|!VG0?$tk)%_%X+(HFlB-u34Z<P$fmR~naRwg<1mb~^=vH;>UMuW_>}|$
z&g=E4o7TAky1XmzQ4f9N4yk<>UmobYRGN)C8u{A7TDBf0^XX`A_nCnaGvKb1UR2WR
zN5ifFW?WWQ&}z!J?m&=0S7YnxH5W_M3+K{N{fP%FuWcAhop48!&^vZmquXs`MvaQK
p3Je)il2bKh!i0q8!Q%)2-(MFH3Kvh(UJd{N002ovPDHLkV1hUScclOT
index 81c901bee74a1042ff4227b96215a911523b477e..fa877a9146a6807be966dd9661381a4a7c27d119
GIT binary patch
literal 1673
zc$@)=26p+0P)<h;3K|Lk000e1NJLTq003YB001Be1ONa4N6EF{0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU&D@jB_R9M5+Slw$BR}@dPCZ@#TM};B{
zhzhBNprBCgi?vUp)lw=LADT^ZH-05+cB8x5o!!~*HEI*V4-$<cQX!Taeei?0MxF0W
zHqi%PsxSQmB7&yWf<ipKvv($QXJ>Y2U7@t+yzHDg=icA%%(?fTyH!<1#NFRD<$dIR
z)HUVquc|;`1ZF{zSs1A(eiy_bjx+|lDv7V^dK?+2kw(V59#;f_kc#5tx>iJhlKU8}
zl>EOS;I4Lu-9PBhwDdEBm6iyv-N>ujazY0TfZL7$bRaK>WqdV!3+wC%z?aCY+7kZ0
zzV1nn;Qp)>0r&DlV)x{}J~IUb|2jhYC#)z%0Gi-0^myG&0l{xUP(!e|6an}IDD>Q7
zL7_e9&!q)gI&eV)XC~lw9Q-9Jr3HVkJ!ArKM#ai#gPRG!JeW`oXHD_jRIoq6N5u#r
zl5W+qtsvaB#W$JaPcX;XBNMJxY>P-I7_1SMfNO7HLU~T{zDd^>!}Ki#T3=BDG+}Bn
z#5WUgzbZ=LYr@t{!f)stO9&WjQs|IfE362(zUa)QIc!M?onx+sx#<_dGy&~zSZzfB
z*1=oCG!2X9;_JXqKnufCD*~_#B|x|Np-v}oASfSau%`H)UtRTzH7seiFeXq>{g&c2
zTTpf#AgYxV78)!GG@v)6=#{;h1dEDjz9oVA=nd(&S%Y8Wxqp<!urgfc!Au-yppXD8
zn@MIKWH@X)%6jg*YV!74cuv523JZ&t&_eW9m*-HcE`KuromyrsN~jLKCBFgQ3ifOY
z$j2B(ds$k1a!SiUQ0`^1H7IKtcyfvsvGo`ekoRoL+qV;--oeI_yU<4Dq04r4XVO8%
zpt0mG)To}$Sew-SC&6P#X|Df{o`I9MLoy{%<Dv1BxAhG4-$`@WJHg|<2u>4_#tf~*
zJ98@mV}$payk#o^$!XV0fRpgXdr|k*G}p^08WnC`vdh4&OHl!Pr@8K{d7DT;YPUUK
z+X-)6ntO<u*4xIfC%j#GlmB6c#gM|{Gml*co_WF|_Rg^Whj~A81+20Ku!>v-c?-D$
zme|HGAy+}(6cCRxSVPjx2D<@fHiRV2KOp9Zd_zF0wFOX1c)w9k2OW~bHr_#a(|J>n
zAiy=+4e-aOI)R{>K*I+Tm|p>bFqdL8jG~ly$hHK$%PA3ir`RxODxuG90jw-pLQ8Gq
z7n@3`cQnaiNVKs3d)uD%J6gcrNv?M^KcqxJYPAK>YFflD0@7OB__d}*JUATX(~P1q
z&U3+TfX<;9hrOfxz*W;Wde;^}w&XVYV;Ko3+(t)}l!zgvSfAXsrc`*_8lRkEu{R}-
zrb-=TmeU&#vK^%kGRt`5L52(V^>&I+uqj5-u(0<=+k&F-7wo+r7O-c6rTEu7)Mfd?
zs+F*qej)sB*-8YgS_zBzI&>W}w-V5FWjLb3N->9J_r<n)OZfHfi-%>+FCttS)|RYw
zGiRFqJ)1dGe6eN@94{jwF2*JqMPr<IqWy-`R7lP>?Kiv=F;4YjL;TCIwrdx()5=%a
zXi+>*4L;9{w$sX2XtXGvPJ<8GPQ&JVLp@@Aj)G6_$hiibJDrWrdCrfWE1WxB106Y^
ztig}7J>tEgxeOFuXz*IdqNZUb)IdF~M!&L#+E-+E(F+Y;3$3VWSd7Z67M7x4t%lk!
zywE5aI5!d!;%tIZG{y!buk6VkoZLUPf9l|*C+C#|QcQgtXG6ld5km%)G0wvKNbxIW
zd0Kd-gyz|J-_4MqflM$o7ZUu^5u^brB+#5@N5v!b-JCUMK)=+NC&e%I<*C)B9=6TK
z2S);8bdG|RM%jQkI5KO_fcCdaIa2Cxm2%))|E+>EkR1x5%tzTcW7a6jeUQrz70Cb|
zWp}}HE|h6w?VIA83zl=C`U}6jDFd%Emxp`!fJpH)hlxa4nxl}Vhrc}hDpNEA`UO8>
zI2Uwzf!-{#J!$LU8^gJvYXH5evkT8>;BA5%Jbx{f4RXpF2y&_H`D@(Zo1{$!^igWi
z>3kYwl<&7lr_ry|$WmKXrl4YMLJfu+%|;{jqhDK*#Zg}Hhh^ZuAs>{1e_j3o^9Dpw
TG}we)00000NkvXXu0mjf)G8dt
--- a/browser/themes/osx/places/organizer.css
+++ b/browser/themes/osx/places/organizer.css
@@ -36,26 +36,16 @@
   border-top: @sidebarItemInactiveBorderTop@;
 }
 
 #placesList > treechildren::-moz-tree-row(History),
 #placesList > treechildren::-moz-tree-row(history)  {
   background-color: blue;
 }
 
-#placesList > treechildren::-moz-tree-cell(leaf) ,
-#placesList > treechildren::-moz-tree-image(leaf) {
-  cursor: pointer;
-}
-
-#placesList > treechildren::-moz-tree-cell-text(leaf, hover) {
-  cursor: pointer;
-  text-decoration: underline;
-}
-
 #placesList > treechildren::-moz-tree-cell(separator) {
   cursor: default;
 }
 
 #placesList > treechildren::-moz-tree-separator {
   border-top: 1px solid #505d6d;
   margin: 0 10px;
 }
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -277,16 +277,24 @@ toolbarpaletteitem[place="palette"] > to
 #PanelUI-customize:hover,
 #PanelUI-quit:not([disabled]):hover {
   border-color: rgba(8,25,42,0.2);
   border-top-color: rgba(8,25,42,0.1);
   background-color: rgba(0,0,0,0.1);
   box-shadow: none;
 }
 
+#PanelUI-quit:not([disabled]):hover {
+  background-color: #d94141;
+}
+
+#PanelUI-quit:not([disabled]):hover:active {
+  background-color: #ad3434;
+}
+
 #main-window[customize-entered] #PanelUI-customize {
   color: white;
   background-image: linear-gradient(rgb(41,123,204), rgb(40,133,203));
   box-shadow: inset 0 1px 1px rgba(0,0,0,0.5), 0 2px rgba(255,255,255,0.2);
   text-shadow: 0 1px 0 rgba(0,0,0,0.4);
 }
 
 #main-window[customize-entered] #PanelUI-customize:hover,
index 6359d7262a286a34074f6f245636ad15a95993b5..8daaffe9556f3fc93fbfa7883fa0639b72e0a26e
GIT binary patch
literal 568
zc$@(^0>}M{P)<h;3K|Lk000e1NJLTq001xm000mO1^@s6P_F#300061Nkl<Zc-qC5
zziPrz6vj;iHwUvRbTFA+I&^TTeT@W1adB+z;@?;3(4ns(g1&=!4VCDu;FKQWyYNx&
z^+;N1;D?;U7x}*A-g8s>`22El5OQIU+DV@0HV(q|+F$De428Myc1y=bLZMD|ZlGQR
zI$MBHsMDYGWqJ+hY-VagmFl^HdR6F31~#7w%NqeaVGSLJFy=F1c_V-)tf31n=7&P!
z3V{2&(A7AAFKozc&k<l?YUq8K_k|6a?KuJrOaZ-5#`<LT(GlRu)X;k-Ktje&LfaMq
z{S);P1W3r(Nod;wpnsxXf_{sPjf4kV0Q5)HYncEmGBr^)@r<-91Xz)&iL!}jq+Oxc
zB~u+`6TgtQYXTg}RHRItkhD7lIFhMInJgh`cj&#7sX)2EM%q^sASWtHfC_0Z2*6iG
zF;F4x#j;e&%Kc^1PKvyN<}cqs^Dp0kZ4Zo<08?cTj1>b@We+?n_jh;?C{u_iFol>R
zQ;2a*A=f!@#*>b)<|EYSJI{eLo^*sYAE7=g=KzQ32m#i7fO_OQ2TCxR!JJ0+bFaW;
zmfv?}mc0^~v8e%Fz$Y<>NEP!bndbl|M5>ri*}Nr0Uoqb|<~ac43$d^ewiuN=)QR9L
zU!%Sd3kzY3QMp5%(6VviVQt5lG86p9=zET@(c&P$zwr&)(Yse9b!qtk0000<MNUMn
GLSTYC3IJ&U
--- a/configure.in
+++ b/configure.in
@@ -240,16 +240,18 @@ if test -n "$gonkdir" ; then
             MOZ_B2G_BT_BLUEDROID=1
         fi
 
         MOZ_RTSP=1
         MOZ_NFC=1
         MOZ_B2G_CAMERA=1
         MOZ_OMX_DECODER=1
         AC_SUBST(MOZ_OMX_DECODER)
+        MOZ_OMX_ENCODER=1
+        AC_SUBST(MOZ_OMX_ENCODER)
         ;;
     19)
         GONK_INCLUDES="-I$gonkdir/frameworks/native/include"
         MOZ_NFC=1
 
         ;;
     *)
         AC_MSG_ERROR([Unsupported platform version: $ANDROID_VERSION])
new file mode 100644
--- /dev/null
+++ b/content/media/encoder/Makefile.in
@@ -0,0 +1,13 @@
+# 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/.
+
+include $(topsrcdir)/config/rules.mk
+
+# These includes are from Android JB, for use of MediaCodec.
+INCLUDES	+= \
+		-I$(topsrcdir)/ipc/chromium/src \
+		-I$(ANDROID_SOURCE)/frameworks/native/opengl/include/ \
+		-I$(ANDROID_SOURCE)/frameworks/native/include/ \
+		-I$(ANDROID_SOURCE)/frameworks/av/include/media/ \
+		$(NULL)
new file mode 100644
--- /dev/null
+++ b/content/media/encoder/OmxTrackEncoder.cpp
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxTrackEncoder.h"
+#include "OMXCodecWrapper.h"
+#include "VideoUtils.h"
+#include "ISOTrackMetadata.h"
+
+#ifdef MOZ_WIDGET_GONK
+#include <android/log.h>
+#define OMX_LOG(args...)                                                       \
+  do {                                                                         \
+    __android_log_print(ANDROID_LOG_INFO, "OmxTrackEncoder", ##args);          \
+  } while (0)
+#else
+#define OMX_LOG(args, ...)
+#endif
+
+using namespace android;
+
+namespace mozilla {
+
+#define ENCODER_CONFIG_FRAME_RATE 30 // fps
+#define GET_ENCODED_VIDEO_FRAME_TIMEOUT 100000 // microseconds
+
+nsresult
+OmxVideoTrackEncoder::Init(int aWidth, int aHeight, TrackRate aTrackRate)
+{
+  mFrameWidth = aWidth;
+  mFrameHeight = aHeight;
+  mTrackRate = aTrackRate;
+
+  mEncoder = OMXCodecWrapper::CreateAVCEncoder();
+  NS_ENSURE_TRUE(mEncoder, NS_ERROR_FAILURE);
+
+  nsresult rv = mEncoder->Configure(mFrameWidth, mFrameHeight,
+                                    ENCODER_CONFIG_FRAME_RATE);
+
+  ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+  mInitialized = (rv == NS_OK);
+
+  mReentrantMonitor.NotifyAll();
+
+  return rv;
+}
+
+already_AddRefed<TrackMetadataBase>
+OmxVideoTrackEncoder::GetMetadata()
+{
+  {
+    // Wait if mEncoder is not initialized nor is being canceled.
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+    while (!mCanceled && !mInitialized) {
+      mReentrantMonitor.Wait();
+    }
+  }
+
+  if (mCanceled || mEncodingComplete) {
+    return nullptr;
+  }
+
+  nsRefPtr<AVCTrackMetadata> meta = new AVCTrackMetadata();
+  meta->Width = mFrameWidth;
+  meta->Height = mFrameHeight;
+  meta->FrameRate = ENCODER_CONFIG_FRAME_RATE;
+  meta->VideoFrequency = 90000; // Hz
+  return meta.forget();
+}
+
+nsresult
+OmxVideoTrackEncoder::GetEncodedTrack(EncodedFrameContainer& aData)
+{
+  VideoSegment segment;
+  {
+    // Move all the samples from mRawSegment to segment. We only hold the
+    // monitor in this block.
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+
+    // Wait if mEncoder is not initialized nor is being canceled.
+    while (!mCanceled && (!mInitialized ||
+          (mRawSegment.GetDuration() == 0 && !mEndOfStream))) {
+      mReentrantMonitor.Wait();
+    }
+
+    if (mCanceled || mEncodingComplete) {
+      return NS_ERROR_FAILURE;
+    }
+
+    segment.AppendFrom(&mRawSegment);
+  }
+
+  // Start queuing raw frames to the input buffers of OMXCodecWrapper.
+  VideoSegment::ChunkIterator iter(segment);
+  while (!iter.IsEnded()) {
+    VideoChunk chunk = *iter;
+
+    // Send only the unique video frames to OMXCodecWrapper.
+    if (mLastFrame != chunk.mFrame) {
+      uint64_t totalDurationUs = mTotalFrameDuration * USECS_PER_S / mTrackRate;
+      layers::Image* img = (chunk.IsNull() || chunk.mFrame.GetForceBlack()) ?
+                           nullptr : chunk.mFrame.GetImage();
+      mEncoder->Encode(img, mFrameWidth, mFrameHeight, totalDurationUs);
+    }
+
+    mLastFrame.TakeFrom(&chunk.mFrame);
+    mTotalFrameDuration += chunk.GetDuration();
+
+    iter.Next();
+  }
+
+  // Send the EOS signal to OMXCodecWrapper.
+  if (mEndOfStream && iter.IsEnded() && !mEosSetInEncoder) {
+    mEosSetInEncoder = true;
+    uint64_t totalDurationUs = mTotalFrameDuration * USECS_PER_S / mTrackRate;
+    layers::Image* img = (!mLastFrame.GetImage() || mLastFrame.GetForceBlack())
+                         ? nullptr : mLastFrame.GetImage();
+    mEncoder->Encode(img, mFrameWidth, mFrameHeight, totalDurationUs,
+                     OMXCodecWrapper::BUFFER_EOS);
+  }
+
+  // Dequeue an encoded frame from the output buffers of OMXCodecWrapper.
+  nsTArray<uint8_t> buffer;
+  int outFlags = 0;
+  int64_t outTimeStampUs = 0;
+  mEncoder->GetNextEncodedFrame(&buffer, &outTimeStampUs, &outFlags,
+                                GET_ENCODED_VIDEO_FRAME_TIMEOUT);
+  if (!buffer.IsEmpty()) {
+    nsRefPtr<EncodedFrame> videoData = new EncodedFrame();
+    if (outFlags & OMXCodecWrapper::BUFFER_CODEC_CONFIG) {
+      videoData->SetFrameType(EncodedFrame::AVC_CSD);
+    } else {
+      videoData->SetFrameType((outFlags & OMXCodecWrapper::BUFFER_SYNC_FRAME) ?
+                              EncodedFrame::I_FRAME : EncodedFrame::P_FRAME);
+    }
+    videoData->SetFrameData(&buffer);
+    videoData->SetTimeStamp(outTimeStampUs);
+    aData.AppendEncodedFrame(videoData);
+  }
+
+  if (outFlags & OMXCodecWrapper::BUFFER_EOS) {
+    mEncodingComplete = true;
+    OMX_LOG("Done encoding video.");
+  }
+
+  return NS_OK;
+}
+
+nsresult
+OmxAudioTrackEncoder::Init(int aChannels, int aSamplingRate)
+{
+  mChannels = aChannels;
+  mSamplingRate = aSamplingRate;
+
+  mEncoder = OMXCodecWrapper::CreateAACEncoder();
+  NS_ENSURE_TRUE(mEncoder, NS_ERROR_FAILURE);
+
+  nsresult rv = mEncoder->Configure(mChannels, mSamplingRate);
+
+  ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+  mInitialized = (rv == NS_OK);
+
+  mReentrantMonitor.NotifyAll();
+
+  return NS_OK;
+}
+
+already_AddRefed<TrackMetadataBase>
+OmxAudioTrackEncoder::GetMetadata()
+{
+  {
+    // Wait if mEncoder is not initialized nor is being canceled.
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+    while (!mCanceled && !mInitialized) {
+      mReentrantMonitor.Wait();
+    }
+  }
+
+  if (mCanceled || mEncodingComplete) {
+    return nullptr;
+  }
+
+  nsRefPtr<AACTrackMetadata> meta = new AACTrackMetadata();
+  meta->Channels = mChannels;
+  meta->SampleRate = mSamplingRate;
+  meta->FrameSize = OMXCodecWrapper::kAACFrameSize;
+  meta->FrameDuration = OMXCodecWrapper::kAACFrameDuration;
+
+  return meta.forget();
+}
+
+nsresult
+OmxAudioTrackEncoder::AppendEncodedFrames(EncodedFrameContainer& aContainer)
+{
+  nsTArray<uint8_t> frameData;
+  int outFlags = 0;
+  int64_t outTimeUs = -1;
+
+  nsresult rv = mEncoder->GetNextEncodedFrame(&frameData, &outTimeUs, &outFlags,
+                                              3000); // wait up to 3ms
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!frameData.IsEmpty()) {
+    bool isCSD = false;
+    if (outFlags & OMXCodecWrapper::BUFFER_CODEC_CONFIG) { // codec specific data
+      isCSD = true;
+    } else if (outFlags & OMXCodecWrapper::BUFFER_EOS) { // last frame
+      mEncodingComplete = true;
+    } else {
+      MOZ_ASSERT(frameData.Length() == OMXCodecWrapper::kAACFrameSize);
+    }
+
+    nsRefPtr<EncodedFrame> audiodata = new EncodedFrame();
+    audiodata->SetFrameType(isCSD ?
+      EncodedFrame::AAC_CSD : EncodedFrame::AUDIO_FRAME);
+    audiodata->SetTimeStamp(outTimeUs);
+    audiodata->SetFrameData(&frameData);
+    aContainer.AppendEncodedFrame(audiodata);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+OmxAudioTrackEncoder::GetEncodedTrack(EncodedFrameContainer& aData)
+{
+  AudioSegment segment;
+  // Move all the samples from mRawSegment to segment. We only hold
+  // the monitor in this block.
+  {
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+
+    // Wait if mEncoder is not initialized nor canceled.
+    while (!mInitialized && !mCanceled) {
+      mReentrantMonitor.Wait();
+    }
+
+    if (mCanceled || mEncodingComplete) {
+      return NS_ERROR_FAILURE;
+    }
+
+    segment.AppendFrom(&mRawSegment);
+  }
+
+  if (!mEosSetInEncoder) {
+    if (mEndOfStream) {
+      mEosSetInEncoder = true;
+    }
+    if (segment.GetDuration() > 0 || mEndOfStream) {
+      // Notify EOS at least once, even when segment is empty.
+      nsresult rv = mEncoder->Encode(segment,
+                                mEndOfStream ? OMXCodecWrapper::BUFFER_EOS : 0);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
+
+  return AppendEncodedFrames(aData);
+}
+
+}
new file mode 100644
--- /dev/null
+++ b/content/media/encoder/OmxTrackEncoder.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OmxTrackEncoder_h_
+#define OmxTrackEncoder_h_
+
+#include "TrackEncoder.h"
+
+namespace android {
+class OMXVideoEncoder;
+class OMXAudioEncoder;
+}
+
+/**
+ * There are two major classes defined in file OmxTrackEncoder;
+ * OmxVideoTrackEncoder and OmxAudioTrackEncoder, the video and audio track
+ * encoder for media type AVC/H.264 and AAC. OMXCodecWrapper wraps and controls
+ * an instance of MediaCodec, defined in libstagefright, runs on Android Jelly
+ * Bean platform.
+ */
+
+namespace mozilla {
+
+class OmxVideoTrackEncoder: public VideoTrackEncoder
+{
+public:
+  OmxVideoTrackEncoder()
+    : VideoTrackEncoder()
+  {}
+
+  already_AddRefed<TrackMetadataBase> GetMetadata() MOZ_OVERRIDE;
+
+  nsresult GetEncodedTrack(EncodedFrameContainer& aData) MOZ_OVERRIDE;
+
+protected:
+  nsresult Init(int aWidth, int aHeight, TrackRate aTrackRate) MOZ_OVERRIDE;
+
+private:
+  nsAutoPtr<android::OMXVideoEncoder> mEncoder;
+};
+
+class OmxAudioTrackEncoder MOZ_FINAL : public AudioTrackEncoder
+{
+public:
+  OmxAudioTrackEncoder()
+    : AudioTrackEncoder()
+  {}
+
+  already_AddRefed<TrackMetadataBase> GetMetadata() MOZ_OVERRIDE;
+
+  nsresult GetEncodedTrack(EncodedFrameContainer& aData) MOZ_OVERRIDE;
+
+protected:
+  nsresult Init(int aChannels, int aSamplingRate) MOZ_OVERRIDE;
+
+private:
+  // Append encoded frames to aContainer.
+  nsresult AppendEncodedFrames(EncodedFrameContainer& aContainer);
+
+  nsAutoPtr<android::OMXAudioEncoder> mEncoder;
+};
+
+}
+#endif
--- a/content/media/encoder/moz.build
+++ b/content/media/encoder/moz.build
@@ -19,11 +19,18 @@ UNIFIED_SOURCES += [
     'MediaEncoder.cpp',
     'TrackEncoder.cpp',
 ]
 
 if CONFIG['MOZ_OPUS']:
     EXPORTS += ['OpusTrackEncoder.h']
     UNIFIED_SOURCES += ['OpusTrackEncoder.cpp']
 
+if CONFIG['MOZ_OMX_ENCODER']:
+    DEFINES['MOZ_OMX_ENCODER'] = True
+    EXPORTS += ['OmxTrackEncoder.h']
+    UNIFIED_SOURCES += ['OmxTrackEncoder.cpp']
+
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'gklayout'
+
+include('/ipc/chromium/chromium-config.mozbuild')
--- a/content/media/omx/Makefile.in
+++ b/content/media/omx/Makefile.in
@@ -8,8 +8,15 @@ CXXFLAGS += \
 		-I$(ANDROID_SOURCE)/dalvik/libnativehelper/include/nativehelper \
 		-I$(ANDROID_SOURCE)/frameworks/base/include/ \
 		-I$(ANDROID_SOURCE)/frameworks/base/include/binder/ \
 		-I$(ANDROID_SOURCE)/frameworks/base/include/utils/ \
 		-I$(ANDROID_SOURCE)/frameworks/base/include/media/ \
 		-I$(ANDROID_SOURCE)/frameworks/base/include/media/stagefright/openmax \
 		-I$(ANDROID_SOURCE)/frameworks/base/media/libstagefright/include/ \
 		$(NULL)
+
+# These includes are for Android JB, using MediaCodec on OMXCodec.
+INCLUDES	+= \
+		-I$(ANDROID_SOURCE)/frameworks/native/opengl/include/ \
+		-I$(ANDROID_SOURCE)/frameworks/native/include/ \
+		-I$(ANDROID_SOURCE)/frameworks/av/include/media/ \
+		$(NULL)
new file mode 100644
--- /dev/null
+++ b/content/media/omx/OMXCodecDescriptorUtil.cpp
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OMXCodecDescriptorUtil.h"
+
+namespace android {
+
+// NAL unit start code.
+static const uint8_t kNALUnitStartCode[] = { 0x00, 0x00, 0x00, 0x01 };
+
+// This class is used to generate AVC/H.264 decoder config descriptor blob from
+// the sequence parameter set(SPS) + picture parameter set(PPS) data.
+//
+// SPS + PPS format:
+// --- SPS NAL unit ---
+//   Start code <0x00 0x00 0x00 0x01>  (4 bytes)
+//   NAL unit type <0x07>              (5 bits)
+//   SPS                               (1+ bytes)
+//   ...
+// --- PPS NAL unit ---
+//   Start code <0x00 0x00 0x00 0x01>  (4 bytes)
+//   NAL unit type <0x08>              (5 bits)
+//   PPS                               (1+ bytes)
+//   ...
+// --- End ---
+//
+// Descriptor format:
+// --- Header (5 bytes) ---
+//   Version <0x01>       (1 byte)
+//   Profile              (1 byte)
+//   Compatible profiles  (1 byte)
+//   Level                (1 byte)
+//   Reserved <111111>    (6 bits)
+//   NAL length type      (2 bits)
+// --- Parameter sets ---
+//   Reserved <111>       (3 bits)
+//   Number of SPS        (5 bits)
+//   SPS                  (3+ bytes)
+//     Length               (2 bytes)
+//     SPS NAL unit         (1+ bytes)
+//   ...
+//   Number of PPS        (1 byte)
+//   PPS                  (3+ bytes)
+//     Length               (2 bytes)
+//     PPS NAL unit         (1+ bytes)
+//   ...
+// --- End ---
+class AVCDecodeConfigDescMaker {
+public:
+  // Convert SPS + PPS data to decoder config descriptor blob. aParamSets
+  // contains the source data, and the generated blob will be appended to
+  // aOutputBuf.
+  status_t ConvertParamSetsToDescriptorBlob(ABuffer* aParamSets,
+                                            nsTArray<uint8_t>* aOutputBuf)
+  {
+    uint8_t header[] = {
+      0x01, // Version.
+      0x00, // Will be filled with 'profile' when parsing SPS later.
+      0x00, // Will be filled with 'compatible profiles' when parsing SPS later.
+      0x00, // Will be filled with 'level' when parsing SPS later.
+      0xFF, // 6 bits reserved value <111111> + 2 bits NAL length type <11>
+    };
+
+    size_t paramSetsSize = ParseParamSets(aParamSets, header);
+    NS_ENSURE_TRUE(paramSetsSize > 0, ERROR_MALFORMED);
+
+    // Extra 1 byte for number of SPS & the other for number of PPS.
+    aOutputBuf->SetCapacity(sizeof(header) + paramSetsSize + 2);
+    // 5 bytes Header.
+    aOutputBuf->AppendElements(header, sizeof(header));
+    // 3 bits <111> + 5 bits number of SPS.
+    uint8_t n = mSPS.Length();
+    aOutputBuf->AppendElement(0xE0 | n);
+    // SPS NAL units.
+    for (int i = 0; i < n; i++) {
+      mSPS.ElementAt(i).AppendTo(aOutputBuf);
+    }
+    // 1 byte number of PPS.
+    n = mPPS.Length();
+    aOutputBuf->AppendElement(n);
+    // PPS NAL units.
+    for (int i = 0; i < n; i++) {
+      mPPS.ElementAt(i).AppendTo(aOutputBuf);
+    }
+
+    return OK;
+  }
+
+private:
+  // Sequence parameter set or picture parameter set.
+  struct AVCParamSet {
+    AVCParamSet(const uint8_t* aPtr, const size_t aSize)
+      : mPtr(aPtr)
+      , mSize(aSize)
+    {}
+
+    // Append 2 bytes length value and NAL unit bitstream to aOutputBuf.
+    void AppendTo(nsTArray<uint8_t>* aOutputBuf)
+    {
+      MOZ_ASSERT(mPtr && mSize > 0);
+
+      // 2 bytes length value.
+      uint8_t size[] = {
+        (mSize & 0xFF00) >> 8, // MSB.
+        mSize & 0x00FF,        // LSB.
+      };
+      aOutputBuf->AppendElements(size, sizeof(size));
+
+      aOutputBuf->AppendElements(mPtr, mSize);
+    }
+
+    const uint8_t* mPtr; // Pointer to NAL unit.
+    const size_t mSize;  // NAL unit length in bytes.
+  };
+
+  // NAL unit types.
+  enum {
+    kNALUnitTypeSPS = 0x07, // Value for sequence parameter set.
+    kNALUnitTypePPS = 0x08, // Value for picture parameter set.
+  };
+
+  // Search for next start code to determine the location of parameter set data
+  // and save the result to corresponding parameter set arrays. The search range
+  // is from aPtr to (aPtr + aSize - 4), and aType indicates which array to save
+  // the result.
+  // The size (in bytes) of found parameter set will be stored in
+  // aParameterSize.
+  // This function also returns the pointer to found start code that caller can
+  // use for the next iteration of search. If the returned pointer is beyond
+  // the end of search range, it means no start code is found.
+  uint8_t* ParseParamSet(uint8_t* aPtr, size_t aSize, uint8_t aType,
+                         size_t* aParamSetSize)
+  {
+    MOZ_ASSERT(aPtr && aSize > 0);
+    MOZ_ASSERT(aType == kNALUnitTypeSPS || aType == kNALUnitTypePPS);
+    MOZ_ASSERT(aParamSetSize);
+
+    // Find next start code.
+    size_t index = 0;
+    size_t end = aSize - sizeof(kNALUnitStartCode);
+    uint8_t* nextStartCode = aPtr;
+    while (index <= end &&
+            memcmp(kNALUnitStartCode, aPtr + index, sizeof(kNALUnitStartCode))) {
+      ++index;
+    }
+    if (index <= end) {
+      // Found.
+      nextStartCode = aPtr + index;
+    } else {
+      nextStartCode = aPtr + aSize;
+    }
+
+    *aParamSetSize = nextStartCode - aPtr;
+    NS_ENSURE_TRUE(*aParamSetSize > 0, nullptr);
+
+    AVCParamSet paramSet(aPtr, *aParamSetSize);
+    if (aType == kNALUnitTypeSPS) {
+      // SPS should have at least 4 bytes.
+      NS_ENSURE_TRUE(*aParamSetSize >= 4, nullptr);
+      mSPS.AppendElement(paramSet);
+    } else {
+      mPPS.AppendElement(paramSet);
+    }
+    return nextStartCode;
+  }
+
+  // Walk through SPS + PPS data and save the pointer & size of each parameter
+  // set to corresponding arrays. It also fills several values in aHeader.
+  // Will return total size of all parameter sets, or 0 if fail to parse.
+  size_t ParseParamSets(ABuffer* aParamSets, uint8_t* aHeader)
+  {
+    // Data starts with a start code.
+    // SPS and PPS are separated with start codes.
+    // Also, SPS must come before PPS
+    uint8_t type = kNALUnitTypeSPS;
+    bool hasSPS = false;
+    bool hasPPS = false;
+    uint8_t* ptr = aParamSets->data();
+    uint8_t* nextStartCode = ptr;
+    size_t remain = aParamSets->size();
+    size_t paramSetSize = 0;
+    size_t totalSize = 0;
+    // Examine
+    while (remain > sizeof(kNALUnitStartCode) &&
+            !memcmp(kNALUnitStartCode, ptr, sizeof(kNALUnitStartCode))) {
+      ptr += sizeof(kNALUnitStartCode);
+      remain -= sizeof(kNALUnitStartCode);
+      // NAL unit format is defined in ISO/IEC 14496-10 7.3.1:
+      // --- NAL unit ---
+      // Reserved <111>         (3 bits)
+      // Type                   (5 bits)
+      // Parameter set          (4+ bytes for SPS, 1+ bytes for PPS)
+      // --- End ---
+      type = (ptr[0] & 0x1F);
+      if (type == kNALUnitTypeSPS) {
+        // SPS must come before PPS.
+        NS_ENSURE_FALSE(hasPPS, 0);
+        if (!hasSPS) {
+          // SPS contains some header values.
+          aHeader[1] = ptr[1]; // Profile.
+          aHeader[2] = ptr[2]; // Compatible Profiles.
+          aHeader[3] = ptr[3]; // Level.
+
+          hasSPS = true;
+        }
+        nextStartCode = ParseParamSet(ptr, remain, type, &paramSetSize);
+      } else if (type == kNALUnitTypePPS) {
+        // SPS must come before PPS.
+        NS_ENSURE_TRUE(hasSPS, 0);
+        if (!hasPPS) {
+          hasPPS = true;
+        }
+        nextStartCode = ParseParamSet(ptr, remain, type, &paramSetSize);
+      } else {
+        // Should never contain NAL unit other than SPS or PPS.
+        NS_ENSURE_TRUE(false, 0);
+      }
+      NS_ENSURE_TRUE(nextStartCode, 0);
+
+      // Move to next start code.
+      remain -= (nextStartCode - ptr);
+      ptr = nextStartCode;
+      totalSize += (2 + paramSetSize); // 2 bytes length + NAL unit.
+    }
+
+    // Sanity check on the number of parameter sets.
+    size_t n = mSPS.Length();
+    NS_ENSURE_TRUE(n > 0 && n <= 0x1F, 0); // 5 bits length only.
+    n = mPPS.Length();
+    NS_ENSURE_TRUE(n > 0 && n <= 0xFF, 0); // 1 byte length only.
+
+    return totalSize;
+  }
+
+  nsTArray<AVCParamSet> mSPS;
+  nsTArray<AVCParamSet> mPPS;
+};
+
+// Blob from OMX encoder could be in descriptor format already, or sequence
+// parameter set(SPS) + picture parameter set(PPS). If later, it needs to be
+// parsed and converted into descriptor format.
+// See MPEG4Writer::Track::makeAVCCodecSpecificData() and
+// MPEG4Writer::Track::writeAvccBox() implementation in libstagefright.
+status_t
+GenerateAVCDescriptorBlob(ABuffer* aData, nsTArray<uint8_t>* aOutputBuf)
+{
+  const size_t csdSize = aData->size();
+  const uint8_t* csd = aData->data();
+
+  MOZ_ASSERT(csdSize > sizeof(kNALUnitStartCode),
+             "Size of codec specific data is too short. "
+             "There could be a serious problem in MediaCodec.");
+
+  NS_ENSURE_TRUE(csdSize > sizeof(kNALUnitStartCode), ERROR_MALFORMED);
+
+  if (memcmp(csd, kNALUnitStartCode, sizeof(kNALUnitStartCode))) {
+    // Already in descriptor format. It should has at least 13 bytes.
+    NS_ENSURE_TRUE(csdSize >= 13, ERROR_MALFORMED);
+
+    aOutputBuf->AppendElements(aData->data(), csdSize);
+  } else {
+    // In SPS + PPS format. Generate descriptor blob from parameters sets.
+    AVCDecodeConfigDescMaker maker;
+    status_t result = maker.ConvertParamSetsToDescriptorBlob(aData, aOutputBuf);
+    NS_ENSURE_TRUE(result == OK, result);
+  }
+
+  return OK;
+}
+
+} // namespace android
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/content/media/omx/OMXCodecDescriptorUtil.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OMXCodecDescriptorUtil_h_
+#define OMXCodecDescriptorUtil_h_
+
+#include <stagefright/foundation/ABuffer.h>
+#include <stagefright/MediaErrors.h>
+
+#include <nsTArray.h>
+
+namespace android {
+
+// Generate decoder config descriptor (defined in ISO/IEC 14496-15 5.2.4.1.1)
+// for AVC/H.264 using codec config blob from encoder.
+status_t GenerateAVCDescriptorBlob(ABuffer* aData,
+                                   nsTArray<uint8_t>* aOutputBuf);
+
+}
+
+#endif // OMXCodecDescriptorUtil_h_
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/content/media/omx/OMXCodecWrapper.cpp
@@ -0,0 +1,609 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OMXCodecWrapper.h"
+#include "OMXCodecDescriptorUtil.h"
+#include "TrackEncoder.h"
+
+#include <binder/ProcessState.h>
+#include <media/ICrypto.h>
+#include <media/IOMX.h>
+#include <OMX_Component.h>
+#include <stagefright/MediaDefs.h>
+#include <stagefright/MediaErrors.h>
+
+#include "AudioChannelFormat.h"
+#include <mozilla/Monitor.h>
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+#define ENCODER_CONFIG_BITRATE 2000000 // bps
+// How many seconds between I-frames.
+#define ENCODER_CONFIG_I_FRAME_INTERVAL 1
+// Wait up to 5ms for input buffers.
+#define INPUT_BUFFER_TIMEOUT_US (5 * 1000ll)
+
+#define CODEC_ERROR(args...)                                                   \
+  do {                                                                         \
+    __android_log_print(ANDROID_LOG_ERROR, "OMXCodecWrapper", ##args);         \
+  } while (0)
+
+namespace android {
+
+OMXAudioEncoder*
+OMXCodecWrapper::CreateAACEncoder()
+{
+  nsAutoPtr<OMXAudioEncoder> aac(new OMXAudioEncoder(CodecType::AAC_ENC));
+  // Return the object only when media codec is valid.
+  NS_ENSURE_TRUE(aac->IsValid(), nullptr);
+
+  return aac.forget();
+}
+
+OMXVideoEncoder*
+OMXCodecWrapper::CreateAVCEncoder()
+{
+  nsAutoPtr<OMXVideoEncoder> avc(new OMXVideoEncoder(CodecType::AVC_ENC));
+  // Return the object only when media codec is valid.
+  NS_ENSURE_TRUE(avc->IsValid(), nullptr);
+
+  return avc.forget();
+}
+
+OMXCodecWrapper::OMXCodecWrapper(CodecType aCodecType)
+  : mStarted(false)
+{
+  ProcessState::self()->startThreadPool();
+
+  mLooper = new ALooper();
+  mLooper->start();
+
+  if (aCodecType == CodecType::AVC_ENC) {
+    mCodec = MediaCodec::CreateByType(mLooper, MEDIA_MIMETYPE_VIDEO_AVC, true);
+  } else if (aCodecType == CodecType::AAC_ENC) {
+    mCodec = MediaCodec::CreateByType(mLooper, MEDIA_MIMETYPE_AUDIO_AAC, true);
+  } else {
+    NS_ERROR("Unknown codec type.");
+  }
+}
+
+OMXCodecWrapper::~OMXCodecWrapper()
+{
+  if (mCodec.get()) {
+    Stop();
+    mCodec->release();
+  }
+  mLooper->stop();
+}
+
+status_t
+OMXCodecWrapper::Start()
+{
+  // Already started.
+  NS_ENSURE_FALSE(mStarted, OK);
+
+  status_t result = mCodec->start();
+  mStarted = (result == OK);
+
+  // Get references to MediaCodec buffers.
+  if (result == OK) {
+    mCodec->getInputBuffers(&mInputBufs);
+    mCodec->getOutputBuffers(&mOutputBufs);
+  }
+
+  return result;
+}
+
+status_t
+OMXCodecWrapper::Stop()
+{
+  // Already stopped.
+  NS_ENSURE_TRUE(mStarted, OK);
+
+  status_t result = mCodec->stop();
+  mStarted = !(result == OK);
+
+  return result;
+}
+
+nsresult
+OMXVideoEncoder::Configure(int aWidth, int aHeight, int aFrameRate)
+{
+  MOZ_ASSERT(!mStarted, "Configure() was called already.");
+
+  NS_ENSURE_TRUE(aWidth > 0 && aHeight > 0 && aFrameRate > 0,
+                 NS_ERROR_INVALID_ARG);
+
+  // Set up configuration parameters for AVC/H.264 encoder.
+  sp<AMessage> format = new AMessage;
+  // Fixed values
+  format->setString("mime", MEDIA_MIMETYPE_VIDEO_AVC);
+  format->setInt32("bitrate", ENCODER_CONFIG_BITRATE);
+  format->setInt32("i-frame-interval", ENCODER_CONFIG_I_FRAME_INTERVAL);
+  // See mozilla::layers::GrallocImage, supports YUV 4:2:0, CbCr width and
+  // height is half that of Y
+  format->setInt32("color-format", OMX_COLOR_FormatYUV420SemiPlanar);
+  format->setInt32("profile", OMX_VIDEO_AVCProfileBaseline);
+  format->setInt32("level", OMX_VIDEO_AVCLevel3);
+  format->setInt32("bitrate-mode", OMX_Video_ControlRateConstant);
+  format->setInt32("store-metadata-in-buffers", 0);
+  format->setInt32("prepend-sps-pps-to-idr-frames", 0);
+  // Input values.
+  format->setInt32("width", aWidth);
+  format->setInt32("height", aHeight);
+  format->setInt32("stride", aWidth);
+  format->setInt32("slice-height", aHeight);
+  format->setInt32("frame-rate", aFrameRate);
+
+  status_t result = mCodec->configure(format, nullptr, nullptr,
+                                      MediaCodec::CONFIGURE_FLAG_ENCODE);
+  NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+  mWidth = aWidth;
+  mHeight = aHeight;
+
+  result = Start();
+
+  return result == OK ? NS_OK : NS_ERROR_FAILURE;
+}
+
+// Copy pixels from planar YUV (4:4:4/4:2:2/4:2:0) or NV21 (semi-planar 4:2:0)
+// format to NV12 (semi-planar 4:2:0) format for QCOM HW encoder.
+// Planar YUV:  YYY...UUU...VVV...
+// NV21:        YYY...VUVU...
+// NV12:        YYY...UVUV...
+// For 4:4:4/4:2:2 -> 4:2:0, subsample using odd row/column without
+// interpolation.
+// aSource contains info about source image data, and the result will be stored
+// in aDestination, whose size needs to be >= Y plane size * 3 / 2.
+static
+void
+ConvertPlanarYCbCrToNV12(const PlanarYCbCrData* aSource, uint8_t* aDestination)
+{
+  // Fill Y plane.
+  uint8_t* y = aSource->mYChannel;
+  IntSize ySize = aSource->mYSize;
+
+  // Y plane.
+  for (int i = 0; i < ySize.height; i++) {
+    memcpy(aDestination, y, ySize.width);
+    aDestination += ySize.width;
+    y += aSource->mYStride;
+  }
+
+  // Fill interleaved UV plane.
+  uint8_t* u = aSource->mCbChannel;
+  uint8_t* v = aSource->mCrChannel;
+  IntSize uvSize = aSource->mCbCrSize;
+  // Subsample to 4:2:0 if source is 4:4:4 or 4:2:2.
+  // Y plane width & height should be multiple of U/V plane width & height.
+  MOZ_ASSERT(ySize.width % uvSize.width == 0 &&
+             ySize.height % uvSize.height == 0);
+  size_t uvWidth = ySize.width / 2;
+  size_t uvHeight = ySize.height / 2;
+  size_t horiSubsample = uvSize.width / uvWidth;
+  size_t uPixStride = horiSubsample * (1 + aSource->mCbSkip);
+  size_t vPixStride = horiSubsample * (1 + aSource->mCrSkip);
+  size_t lineStride = uvSize.height / uvHeight * aSource->mCbCrStride;
+
+  for (int i = 0; i < uvHeight; i++) {
+    // 1st pixel per line.
+    uint8_t* uSrc = u;
+    uint8_t* vSrc = v;
+    for (int j = 0; j < uvWidth; j++) {
+      *aDestination++ = *uSrc;
+      *aDestination++ = *vSrc;
+      // Pick next source pixel.
+      uSrc += uPixStride;
+      vSrc += vPixStride;
+    }
+    // Pick next source line.
+    u += lineStride;
+    v += lineStride;
+  }
+}
+
+nsresult
+OMXVideoEncoder::Encode(const Image* aImage, int aWidth, int aHeight,
+                        int64_t aTimestamp, int aInputFlags)
+{
+  MOZ_ASSERT(mStarted, "Configure() should be called before Encode().");
+
+  NS_ENSURE_TRUE(aWidth == mWidth && aHeight == mHeight && aTimestamp >= 0,
+                 NS_ERROR_INVALID_ARG);
+
+  status_t result;
+
+  // Dequeue an input buffer.
+  uint32_t index;
+  result = mCodec->dequeueInputBuffer(&index, INPUT_BUFFER_TIMEOUT_US);
+  NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+  const sp<ABuffer>& inBuf = mInputBufs.itemAt(index);
+  uint8_t* dst = inBuf->data();
+  size_t dstSize = inBuf->capacity();
+
+  size_t yLen = aWidth * aHeight;
+  size_t uvLen = yLen / 2;
+
+  // Buffer should be large enough to hold input image data.
+  MOZ_ASSERT(dstSize >= yLen + uvLen);
+
+  inBuf->setRange(0, yLen + uvLen);
+
+  if (!aImage) {
+    // Generate muted/black image directly in buffer.
+    dstSize = yLen + uvLen;
+    // Fill Y plane.
+    memset(dst, 0x10, yLen);
+    // Fill UV plane.
+    memset(dst + yLen, 0x80, uvLen);
+  } else {
+    Image* img = const_cast<Image*>(aImage);
+    ImageFormat format = img->GetFormat();
+
+    MOZ_ASSERT(aWidth == img->GetSize().width &&
+               aHeight == img->GetSize().height);
+
+    if (format == GRALLOC_PLANAR_YCBCR) {
+      // Get graphic buffer pointer.
+      void* imgPtr = nullptr;
+      GrallocImage* nativeImage = static_cast<GrallocImage*>(img);
+      SurfaceDescriptor handle = nativeImage->GetSurfaceDescriptor();
+      SurfaceDescriptorGralloc gralloc = handle.get_SurfaceDescriptorGralloc();
+      sp<GraphicBuffer> graphicBuffer = GrallocBufferActor::GetFrom(gralloc);
+      graphicBuffer->lock(GraphicBuffer::USAGE_SW_READ_MASK, &imgPtr);
+      uint8_t* src = static_cast<uint8_t*>(imgPtr);
+
+      // Only support NV21 for now.
+      MOZ_ASSERT(graphicBuffer->getPixelFormat() ==
+                 HAL_PIXEL_FORMAT_YCrCb_420_SP);
+
+      // Build PlanarYCbCrData for NV21 buffer.
+      PlanarYCbCrData nv21;
+      // Y plane.
+      nv21.mYChannel = src;
+      nv21.mYSize.width = aWidth;
+      nv21.mYSize.height = aHeight;
+      nv21.mYStride = aWidth;
+      nv21.mYSkip = 0;
+      // Interleaved VU plane.
+      nv21.mCrChannel = src + yLen;
+      nv21.mCrSkip = 1;
+      nv21.mCbChannel = nv21.mCrChannel + 1;
+      nv21.mCbSkip = 1;
+      nv21.mCbCrStride = aWidth;
+      // 4:2:0.
+      nv21.mCbCrSize.width = aWidth / 2;
+      nv21.mCbCrSize.height = aHeight / 2;
+
+      ConvertPlanarYCbCrToNV12(&nv21, dst);
+
+      graphicBuffer->unlock();
+    } else if (format == PLANAR_YCBCR) {
+      ConvertPlanarYCbCrToNV12(static_cast<PlanarYCbCrImage*>(img)->GetData(),
+                             dst);
+    } else {
+      // TODO: support RGB to YUV color conversion.
+      NS_ERROR("Unsupported input image type.");
+    }
+  }
+
+  // Queue this input buffer.
+  result = mCodec->queueInputBuffer(index, 0, dstSize, aTimestamp, aInputFlags);
+
+  return result == OK ? NS_OK : NS_ERROR_FAILURE;
+}
+
+status_t
+OMXVideoEncoder::AppendDecoderConfig(nsTArray<uint8_t>* aOutputBuf,
+                                     ABuffer* aData)
+{
+  // AVC/H.264 decoder config descriptor is needed to construct MP4 'avcC' box
+  // (defined in ISO/IEC 14496-15 5.2.4.1.1).
+  return GenerateAVCDescriptorBlob(aData, aOutputBuf);
+}
+
+// Override to replace NAL unit start code with 4-bytes unit length.
+// See ISO/IEC 14496-15 5.2.3.
+void OMXVideoEncoder::AppendFrame(nsTArray<uint8_t>* aOutputBuf,
+                                  const uint8_t* aData, size_t aSize)
+{
+  uint8_t length[] = {
+    (aSize >> 24) & 0xFF,
+    (aSize >> 16) & 0xFF,
+    (aSize >> 8) & 0xFF,
+    aSize & 0xFF,
+  };
+  aOutputBuf->SetCapacity(aSize);
+  aOutputBuf->AppendElements(length, sizeof(length));
+  aOutputBuf->AppendElements(aData + sizeof(length), aSize);
+}
+
+nsresult
+OMXAudioEncoder::Configure(int aChannels, int aSamplingRate)
+{
+  MOZ_ASSERT(!mStarted);
+
+  NS_ENSURE_TRUE(aChannels > 0 && aSamplingRate > 0, NS_ERROR_INVALID_ARG);
+
+  // Set up configuration parameters for AAC encoder.
+  sp<AMessage> format = new AMessage;
+  // Fixed values.
+  format->setString("mime", MEDIA_MIMETYPE_AUDIO_AAC);
+  format->setInt32("bitrate", kAACBitrate);
+  format->setInt32("aac-profile", OMX_AUDIO_AACObjectLC);
+  // Input values.
+  format->setInt32("channel-count", aChannels);
+  format->setInt32("sample-rate", aSamplingRate);
+
+  status_t result = mCodec->configure(format, nullptr, nullptr,
+                                      MediaCodec::CONFIGURE_FLAG_ENCODE);
+  NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+  mChannels = aChannels;
+  mSampleDuration = 1000000 / aSamplingRate;
+  result = Start();
+
+  return result == OK ? NS_OK : NS_ERROR_FAILURE;
+}
+
+class InputBufferHelper MOZ_FINAL {
+public:
+  InputBufferHelper(sp<MediaCodec>& aCodec, Vector<sp<ABuffer> >& aBuffers)
+    : mCodec(aCodec)
+    , mBuffers(aBuffers)
+    , mIndex(0)
+    , mData(nullptr)
+    , mOffset(0)
+    , mCapicity(0)
+  {}
+
+  ~InputBufferHelper()
+  {
+    // Unflushed data in buffer.
+    MOZ_ASSERT(!mData);
+  }
+
+  status_t Dequeue()
+  {
+    // Shouldn't have dequeued buffer.
+    MOZ_ASSERT(!mData);
+
+    status_t result = mCodec->dequeueInputBuffer(&mIndex,
+                                                 INPUT_BUFFER_TIMEOUT_US);
+    NS_ENSURE_TRUE(result == OK, result);
+    sp<ABuffer> inBuf = mBuffers.itemAt(mIndex);
+    mData = inBuf->data();
+    mCapicity = inBuf->capacity();
+    mOffset = 0;
+
+    return OK;
+  }
+
+  uint8_t* GetPointer() { return mData + mOffset; }
+
+  const size_t AvailableSize() { return mCapicity - mOffset; }
+
+  void IncreaseOffset(size_t aValue)
+  {
+    // Should never out of bound.
+    MOZ_ASSERT(mOffset + aValue <= mCapicity);
+    mOffset += aValue;
+  }
+
+  status_t Enqueue(int64_t aTimestamp, int aFlags)
+  {
+    // Should have dequeued buffer.
+    MOZ_ASSERT(mData);
+
+    // Queue this buffer.
+    status_t result = mCodec->queueInputBuffer(mIndex, 0, mOffset, aTimestamp,
+                                               aFlags);
+    NS_ENSURE_TRUE(result == OK, result);
+    mData = nullptr;
+
+    return OK;
+  }
+
+private:
+  sp<MediaCodec>& mCodec;
+  Vector<sp<ABuffer> >& mBuffers;
+  size_t mIndex;
+  uint8_t* mData;
+  size_t mCapicity;
+  size_t mOffset;
+};
+
+nsresult
+OMXAudioEncoder::Encode(const AudioSegment& aSegment, int aInputFlags)
+{
+#ifndef MOZ_SAMPLE_TYPE_S16
+#error MediaCodec accepts only 16-bit PCM data.
+#endif
+
+  MOZ_ASSERT(mStarted, "Configure() should be called before Encode().");
+
+  size_t numSamples = aSegment.GetDuration();
+
+  // Get input buffer.
+  InputBufferHelper buffer(mCodec, mInputBufs);
+  status_t result = buffer.Dequeue();
+  NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+  size_t samplesCopied = 0; // Number of copied samples.
+
+  if (numSamples > 0) {
+    // Copy input PCM data to input buffer until queue is empty.
+    AudioSegment::ChunkIterator iter(const_cast<AudioSegment&>(aSegment));
+    while (!iter.IsEnded()) {
+      AudioChunk chunk = *iter;
+      size_t samplesToCopy = chunk.GetDuration(); // Number of samples to copy.
+      size_t bytesToCopy = samplesToCopy * mChannels * sizeof(AudioDataValue);
+
+      if (bytesToCopy > buffer.AvailableSize()) {
+        // Not enough space left in input buffer. Send it to encoder and get a
+        // new one.
+        // Don't signal EOS since there is more data to copy.
+        result = buffer.Enqueue(mTimestamp, aInputFlags & ~BUFFER_EOS);
+        NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+        mTimestamp += samplesCopied * mSampleDuration;
+        samplesCopied = 0;
+
+        result = buffer.Dequeue();
+        NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+      }
+
+      AudioDataValue* dst = reinterpret_cast<AudioDataValue*>(buffer.GetPointer());
+      if (!chunk.IsNull()) {
+        // Append the interleaved data to input buffer.
+        AudioTrackEncoder::InterleaveTrackData(chunk, samplesToCopy, mChannels,
+                                               dst);
+      } else {
+        // Silence.
+        memset(dst, 0, bytesToCopy);
+      }
+
+      samplesCopied += samplesToCopy;
+      buffer.IncreaseOffset(bytesToCopy);
+      iter.Next();
+    }
+  } else if (aInputFlags & BUFFER_EOS) {
+    // No audio data left in segment but we still have to feed something to
+    // MediaCodec in order to notify EOS.
+    size_t bytesToCopy = mChannels * sizeof(AudioDataValue);
+    memset(buffer.GetPointer(), 0, bytesToCopy);
+    buffer.IncreaseOffset(bytesToCopy);
+    samplesCopied = 1;
+  }
+
+  if (samplesCopied > 0) {
+    result = buffer.Enqueue(mTimestamp, aInputFlags);
+    NS_ENSURE_TRUE(result == OK, NS_ERROR_FAILURE);
+
+    mTimestamp += samplesCopied * mSampleDuration;
+  }
+
+  return NS_OK;
+}
+
+// Generate decoder config descriptor (defined in ISO/IEC 14496-1 8.3.4.1) for
+// AAC. The hard-coded bytes are copied from
+// MPEG4Writer::Track::writeMp4aEsdsBox() implementation in libstagefright.
+status_t
+OMXAudioEncoder::AppendDecoderConfig(nsTArray<uint8_t>* aOutputBuf,
+                                     ABuffer* aData)
+{
+  MOZ_ASSERT(aData);
+
+  const size_t csdSize = aData->size();
+
+  // See
+  // http://wiki.multimedia.cx/index.php?title=Understanding_AAC#Packaging.2FEncapsulation_And_Setup_Data
+  // AAC decoder specific descriptor contains 2 bytes.
+  NS_ENSURE_TRUE(csdSize == 2, ERROR_MALFORMED);
+  // Encoder output must be consistent with kAACFrameDuration:
+  // 14th bit (frame length flag) == 0 => 1024 (kAACFrameDuration) samples.
+  NS_ENSURE_TRUE((aData->data()[1] & 0x04) == 0, ERROR_MALFORMED);
+
+  // Decoder config descriptor
+  const uint8_t decConfig[] = {
+    0x04,                   // Decoder config descriptor tag.
+    15 + csdSize,           // Size: following bytes + csd size.
+    0x40,                   // Object type: MPEG-4 audio.
+    0x15,                   // Stream type: audio, reserved: 1.
+    0x00, 0x03, 0x00,       // Buffer size: 768 (kAACFrameSize).
+    0x00, 0x01, 0x77, 0x00, // Max bitrate: 96000 (kAACBitrate).
+    0x00, 0x01, 0x77, 0x00, // Avg bitrate: 96000 (kAACBitrate).
+    0x05,                   // Decoder specific descriptor tag.
+    csdSize,                // Data size.
+  };
+  // SL config descriptor.
+  const uint8_t slConfig[] = {
+    0x06, // SL config descriptor tag.
+    0x01, // Size.
+    0x02, // Fixed value.
+  };
+
+  aOutputBuf->SetCapacity(sizeof(decConfig) + csdSize + sizeof(slConfig));
+  aOutputBuf->AppendElements(decConfig, sizeof(decConfig));
+  aOutputBuf->AppendElements(aData->data(), csdSize);
+  aOutputBuf->AppendElements(slConfig, sizeof(slConfig));
+
+  return OK;
+}
+
+nsresult
+OMXCodecWrapper::GetNextEncodedFrame(nsTArray<uint8_t>* aOutputBuf,
+                                     int64_t* aOutputTimestamp,
+                                     int* aOutputFlags, int64_t aTimeOut)
+{
+  MOZ_ASSERT(mStarted,
+             "Configure() should be called before GetNextEncodedFrame().");
+
+  // Dequeue a buffer from output buffers.
+  size_t index = 0;
+  size_t outOffset = 0;
+  size_t outSize = 0;
+  int64_t outTimeUs = 0;
+  uint32_t outFlags = 0;
+  bool retry = false;
+  do {
+    status_t result = mCodec->dequeueOutputBuffer(&index, &outOffset, &outSize,
+                                                  &outTimeUs, &outFlags,
+                                                  aTimeOut);
+    switch (result) {
+      case OK:
+        break;
+      case INFO_OUTPUT_BUFFERS_CHANGED:
+        // Update our references to new buffers.
+        result = mCodec->getOutputBuffers(&mOutputBufs);
+        // Get output from a new buffer.
+        retry = true;
+        break;
+      case INFO_FORMAT_CHANGED:
+        // It's okay: for encoder, MediaCodec reports this only to inform caller
+        // that there will be a codec config buffer next.
+        return NS_OK;
+      case -EAGAIN:
+        // Output buffer not available. Caller can try again later.
+        return NS_OK;
+      default:
+        CODEC_ERROR("MediaCodec error:%d", result);
+        MOZ_ASSERT(false, "MediaCodec error.");
+        return NS_ERROR_FAILURE;
+    }
+  } while (retry);
+
+  if (aOutputBuf) {
+    aOutputBuf->Clear();
+    const sp<ABuffer> omxBuf = mOutputBufs.itemAt(index);
+    if (outFlags & MediaCodec::BUFFER_FLAG_CODECCONFIG) {
+      // Codec specific data.
+      if (AppendDecoderConfig(aOutputBuf, omxBuf.get()) != OK) {
+        mCodec->releaseOutputBuffer(index);
+        return NS_ERROR_FAILURE;
+      }
+    } else {
+      AppendFrame(aOutputBuf, omxBuf->data(), omxBuf->size());
+    }
+  }
+  mCodec->releaseOutputBuffer(index);
+
+  if (aOutputTimestamp) {
+    *aOutputTimestamp = outTimeUs;
+  }
+
+  if (aOutputFlags) {
+    *aOutputFlags = outFlags;
+  }
+
+  return NS_OK;
+}
+
+}
new file mode 100644
--- /dev/null
+++ b/content/media/omx/OMXCodecWrapper.h
@@ -0,0 +1,266 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OMXCodecWrapper_h_
+#define OMXCodecWrapper_h_
+
+#include <gui/Surface.h>
+#include <stagefright/foundation/ABuffer.h>
+#include <stagefright/foundation/AMessage.h>
+#include <stagefright/MediaCodec.h>
+
+#include "AudioSegment.h"
+#include "GonkNativeWindow.h"
+#include "GonkNativeWindowClient.h"
+
+namespace android {
+
+class OMXAudioEncoder;
+class OMXVideoEncoder;
+
+/**
+ * This class (and its subclasses) wraps the video and audio codec from
+ * MediaCodec API in libstagefright. Currently only AVC/H.264 video encoder and
+ * AAC audio encoder are supported.
+ *
+ * OMXCodecWrapper has static creator functions that returns actual codec
+ * instances for different types of codec supported and serves as superclass to
+ * provide a function to read encoded data as byte array from codec. Two
+ * subclasses, OMXAudioEncoder and OMXVideoEncoder, respectively provides
+ * functions for encoding data from audio and video track.
+ *
+ * A typical usage is as follows:
+ * - Call one of the creator function Create...() to get either a
+ *   OMXAudioEncoder or OMXVideoEncoder object.
+ * - Configure codec by providing characteristics of input raw data, such as
+ *   video frame width and height, using Configure().
+ * - Send raw data (and notify end of stream) with Encode().
+ * - Get encoded data through GetNextEncodedFrame().
+ * - Repeat previous 2 steps until end of stream.
+ * - Destroy the object.
+ *
+ * The lifecycle of underlying OMX codec is binded with construction and
+ * destruction of OMXCodecWrapper and subclass objects. For some types of
+ * codecs, such as HW accelerated AVC/H.264 encoder, there can be only one
+ * instance system-wise at a time, attempting to create another instance will
+ * fail.
+ */
+class OMXCodecWrapper
+{
+public:
+  // Codec types.
+  enum CodecType {
+    AAC_ENC, // AAC encoder.
+    AVC_ENC, // AVC/H.264 encoder.
+    TYPE_COUNT
+  };
+
+  // Input and output flags.
+  enum {
+    // For Encode() and Encode, it indicates the end of input stream;
+    // For GetNextEncodedFrame(), it indicates the end of output
+    // stream.
+    BUFFER_EOS = MediaCodec::BUFFER_FLAG_EOS,
+    // For GetNextEncodedFrame(). It indicates the output buffer is an I-frame.
+    BUFFER_SYNC_FRAME = MediaCodec::BUFFER_FLAG_SYNCFRAME,
+    // For GetNextEncodedFrame(). It indicates that the output buffer contains
+    // codec specific configuration info. (SPS & PPS for AVC/H.264;
+    // DecoderSpecificInfo for AAC)
+    BUFFER_CODEC_CONFIG = MediaCodec::BUFFER_FLAG_CODECCONFIG,
+  };
+
+  // Hard-coded values for AAC DecoderConfigDescriptor in libstagefright.
+  // See MPEG4Writer::Track::writeMp4aEsdsBox()
+  // Exposed for the need of MP4 container writer.
+  enum {
+    kAACBitrate = 96000,      // kbps
+    kAACFrameSize = 768,      // bytes
+    kAACFrameDuration = 1024, // How many samples per AAC frame.
+  };
+
+  /** Create a AAC audio encoder. Returns nullptr when failed. */
+  static OMXAudioEncoder* CreateAACEncoder();
+
+  /** Create a AVC/H.264 video encoder. Returns nullptr when failed. */
+  static OMXVideoEncoder* CreateAVCEncoder();
+
+  virtual ~OMXCodecWrapper();
+
+  /**
+   * Get the next available encoded data from MediaCodec. The data will be
+   * copied into aOutputBuf array, with its timestamp (in microseconds) in
+   * aOutputTimestamp.
+   * Wait at most aTimeout microseconds to dequeue a output buffer.
+   */
+  nsresult GetNextEncodedFrame(nsTArray<uint8_t>* aOutputBuf,
+                               int64_t* aOutputTimestamp, int* aOutputFlags,
+                               int64_t aTimeOut);
+
+protected:
+  /**
+   * See whether the object has been initialized successfully and is ready to
+   * use.
+   */
+  virtual bool IsValid() { return mCodec != nullptr; }
+
+  /**
+   * Construct codec specific configuration blob with given data aData generated
+   * by media codec and append it into aOutputBuf. Needed by MP4 container
+   * writer for generating decoder config box. Returns OK if succeed.
+   */
+  virtual status_t AppendDecoderConfig(nsTArray<uint8_t>* aOutputBuf,
+                                       ABuffer* aData) = 0;
+
+  /**
+   * Append encoded frame data generated by media codec (stored in aData and
+   * is aSize bytes long) into aOutputBuf. Subclasses can override this function
+   * to process the data for specific container writer.
+   */
+  virtual void AppendFrame(nsTArray<uint8_t>* aOutputBuf,
+                           const uint8_t* aData, size_t aSize)
+  {
+    aOutputBuf->AppendElements(aData, aSize);
+  }
+
+private:
+  // Hide these. User should always use creator functions to get a media codec.
+  OMXCodecWrapper() MOZ_DELETE;
+  OMXCodecWrapper(const OMXCodecWrapper&) MOZ_DELETE;
+  OMXCodecWrapper& operator=(const OMXCodecWrapper&) MOZ_DELETE;
+
+  /**
+   * Create a media codec of given type. It will be a AVC/H.264 video encoder if
+   * aCodecType is CODEC_AVC_ENC, or AAC audio encoder if aCodecType is
+   * CODEC_AAC_ENC.
+   */
+  OMXCodecWrapper(CodecType aCodecType);
+
+  // For subclasses to access hidden constructor and implementation details.
+  friend class OMXAudioEncoder;
+  friend class OMXVideoEncoder;
+
+  /**
+   * Start the media codec.
+   */
+  status_t Start();
+
+  /**
+   * Stop the media codec.
+   */
+  status_t Stop();
+
+  // The actual codec instance provided by libstagefright.
+  sp<MediaCodec> mCodec;
+
+  // A dedicate message loop with its own thread used by MediaCodec.
+  sp<ALooper> mLooper;
+
+  Vector<sp<ABuffer> > mInputBufs;  // MediaCodec buffers to hold input data.
+  Vector<sp<ABuffer> > mOutputBufs; // MediaCodec buffers to hold output data.
+
+  bool mStarted; // Has MediaCodec been started?
+};
+
+/**
+ * Audio encoder.
+ */
+class OMXAudioEncoder MOZ_FINAL : public OMXCodecWrapper
+{
+public:
+  /**
+   * Configure audio codec parameters and start media codec. It must be called
+   * before calling Encode() and GetNextEncodedFrame().
+   */
+  nsresult Configure(int aChannelCount, int aSampleRate);
+
+  /**
+   * Encode 16-bit PCM audio samples stored in aSegment. To notify end of
+   * stream, set aInputFlags to BUFFER_EOS.
+   */
+  nsresult Encode(const mozilla::AudioSegment& aSegment, int aInputFlags = 0);
+
+protected:
+  virtual status_t AppendDecoderConfig(nsTArray<uint8_t>* aOutputBuf,
+                                       ABuffer* aData) MOZ_OVERRIDE;
+
+private:
+  // Hide these. User should always use creator functions to get a media codec.
+  OMXAudioEncoder() MOZ_DELETE;
+  OMXAudioEncoder(const OMXAudioEncoder&) MOZ_DELETE;
+  OMXAudioEncoder& operator=(const OMXAudioEncoder&) MOZ_DELETE;
+
+  /**
+   * Create a audio codec. It will be a AAC encoder if aCodecType is
+   * CODEC_AAC_ENC.
+   */
+  OMXAudioEncoder(CodecType aCodecType)
+    : OMXCodecWrapper(aCodecType)
+    , mChannels(0)
+    , mTimestamp(0)
+    , mSampleDuration(0) {}
+
+  // For creator function to access hidden constructor.
+  friend class OMXCodecWrapper;
+
+  // Number of audio channels.
+  size_t mChannels;
+  // The total duration of audio samples that have been encoded in microseconds.
+  int64_t mTimestamp;
+  // Time per audio sample in microseconds.
+  int64_t mSampleDuration;
+};
+
+/**
+ * Video encoder.
+ */
+class OMXVideoEncoder MOZ_FINAL : public OMXCodecWrapper
+{
+public:
+  /**
+   * Configure video codec parameters and start media codec. It must be called
+   * before calling Encode() and GetNextEncodedFrame().
+   */
+  nsresult Configure(int aWidth, int aHeight, int aFrameRate);
+
+  /**
+   * Encode a aWidth pixels wide and aHeight pixels tall video frame of
+   * semi-planar YUV420 format stored in the buffer of aImage. aTimestamp gives
+   * the frame timestamp/presentation time (in microseconds). To notify end of
+   * stream, set aInputFlags to BUFFER_EOS.
+   */
+  nsresult Encode(const mozilla::layers::Image* aImage, int aWidth, int aHeight,
+                  int64_t aTimestamp, int aInputFlags = 0);
+
+protected:
+  virtual status_t AppendDecoderConfig(nsTArray<uint8_t>* aOutputBuf,
+                                       ABuffer* aData) MOZ_OVERRIDE;
+
+  // AVC/H.264 encoder replaces NAL unit start code with the unit length as
+  // specified in ISO/IEC 14496-15 5.2.3.
+  virtual void AppendFrame(nsTArray<uint8_t>* aOutputBuf,
+                           const uint8_t* aData, size_t aSize) MOZ_OVERRIDE;
+
+private:
+  // Hide these. User should always use creator functions to get a media codec.
+  OMXVideoEncoder() MOZ_DELETE;
+  OMXVideoEncoder(const OMXVideoEncoder&) MOZ_DELETE;
+  OMXVideoEncoder& operator=(const OMXVideoEncoder&) MOZ_DELETE;
+
+  /**
+   * Create a video codec. It will be a AVC/H.264 encoder if aCodecType is
+   * CODEC_AVC_ENC.
+   */
+  OMXVideoEncoder(CodecType aCodecType)
+    : OMXCodecWrapper(aCodecType), mWidth(0), mHeight(0) {}
+
+  // For creator function to access hidden constructor.
+  friend class OMXCodecWrapper;
+
+  int mWidth;
+  int mHeight;
+};
+
+} // namespace android
+#endif // OMXCodecWrapper_h_
--- a/content/media/omx/moz.build
+++ b/content/media/omx/moz.build
@@ -11,16 +11,25 @@ EXPORTS += [
 
 SOURCES += [
     'MediaOmxDecoder.cpp',
     'MediaOmxReader.cpp',
     'OMXCodecProxy.cpp',
     'OmxDecoder.cpp',
 ]
 
+if CONFIG['MOZ_OMX_ENCODER']:
+    EXPORTS += [
+        'OMXCodecWrapper.h',
+    ]
+    SOURCES += [
+        'OMXCodecDescriptorUtil.cpp',
+        'OMXCodecWrapper.cpp',
+    ]
+
 if 'rtsp' in CONFIG['NECKO_PROTOCOLS']:
     EXPORTS += [
         'RtspOmxDecoder.h',
         'RtspOmxReader.h',
     ]
     SOURCES += [
         'RtspOmxDecoder.cpp',
         'RtspOmxReader.cpp',
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1819,16 +1819,24 @@ Navigator::HasNfcSupport(JSContext* /* u
 
 /* static */
 bool
 Navigator::HasNfcPeerSupport(JSContext* /* unused */, JSObject* aGlobal)
 {
   nsCOMPtr<nsPIDOMWindow> win = GetWindowFromGlobal(aGlobal);
   return win && CheckPermission(win, "nfc-write");
 }
+
+/* static */
+bool
+Navigator::HasNfcManagerSupport(JSContext* /* unused */, JSObject* aGlobal)
+{
+  nsCOMPtr<nsPIDOMWindow> win = GetWindowFromGlobal(aGlobal);
+  return win && CheckPermission(win, "nfc-manager");
+}
 #endif // MOZ_NFC
 
 #ifdef MOZ_TIME_MANAGER
 /* static */
 bool
 Navigator::HasTimeSupport(JSContext* /* unused */, JSObject* aGlobal)
 {
   nsCOMPtr<nsPIDOMWindow> win = GetWindowFromGlobal(aGlobal);
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -267,16 +267,17 @@ public:
   static bool HasBluetoothSupport(JSContext* /* unused */, JSObject* aGlobal);
 #endif // MOZ_B2G_BT
 #ifdef MOZ_B2G_FM
   static bool HasFMRadioSupport(JSContext* /* unused */, JSObject* aGlobal);
 #endif // MOZ_B2G_FM
 #ifdef MOZ_NFC
   static bool HasNfcSupport(JSContext* /* unused */, JSObject* aGlobal);
   static bool HasNfcPeerSupport(JSContext* /* unused */, JSObject* aGlobal);
+  static bool HasNfcManagerSupport(JSContext* /* unused */, JSObject* aGlobal);
 #endif // MOZ_NFC
 #ifdef MOZ_TIME_MANAGER
   static bool HasTimeSupport(JSContext* /* unused */, JSObject* aGlobal);
 #endif // MOZ_TIME_MANAGER
 #ifdef MOZ_MEDIA_NAVIGATOR
   static bool HasUserMediaSupport(JSContext* /* unused */,
                                   JSObject* /* unused */);
 #endif // MOZ_MEDIA_NAVIGATOR
--- a/dom/contacts/ContactManager.js
+++ b/dom/contacts/ContactManager.js
@@ -29,25 +29,40 @@ const CONTACTS_SENDMORE_MINIMUM = 5;
 // Keep in sync with the interfaces.
 const PROPERTIES = [
   "name", "honorificPrefix", "givenName", "additionalName", "familyName",
   "honorificSuffix", "nickname", "photo", "category", "org", "jobTitle",
   "bday", "note", "anniversary", "sex", "genderIdentity", "key", "adr", "email",
   "url", "impp", "tel"
 ];
 
+let mozContactInitWarned = false;
+