merge mozilla-inbound to mozilla-central. r=merge a=merge
authorSebastian Hengst <archaeopteryx@coole-files.de>
Fri, 21 Jul 2017 12:56:44 +0200
changeset 418732 9f01176142b34bea03868fa4a684c4e57c414dff
parent 418634 0faada5c2f308f101ab7c54a87b3dce80b97d0e3 (current diff)
parent 418731 bb33237155278066423e2fdc3de2785eb44a75e7 (diff)
child 418733 1acc7a08d914694e4f9eb244cb193c8ca6d27865
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge, merge
milestone56.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 mozilla-inbound to mozilla-central. r=merge a=merge MozReview-Commit-ID: IWRTFZdtzaE
browser/base/content/browser.js
browser/base/content/test/general/aboutHome_content_script.js
browser/base/content/test/general/browser_aboutCertError.js
browser/base/content/test/general/browser_aboutHealthReport.js
browser/base/content/test/general/browser_aboutHome.js
browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js
browser/base/content/test/general/browser_aboutNetError.js
browser/base/content/test/general/browser_aboutSupport.js
browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js
browser/base/content/test/general/healthreport_pingData.js
browser/base/content/test/general/healthreport_testRemoteCommands.html
browser/base/content/test/general/test_bug959531.html
browser/extensions/formautofill/test/unit/xpcshell.ini
dom/base/nsFrameLoader.cpp
dom/ipc/ContentChild.cpp
dom/ipc/TabParent.cpp
gfx/thebes/gfxPlatformGtk.cpp
gfx/thebes/gfxPlatformGtk.h
mobile/android/tests/browser/robocop/robocop_autophone.ini
taskcluster/ci/test/tests.yml
testing/web-platform/meta/MANIFEST.json
testing/web-platform/meta/content-security-policy/_unapproved/script-nonces-hidden-meta.html.ini
testing/web-platform/meta/content-security-policy/_unapproved/script-nonces-hidden.html.ini
testing/web-platform/meta/content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html.ini
testing/web-platform/meta/content-security-policy/_unapproved/svgscript-nonces-hidden.html.ini
testing/web-platform/meta/content-security-policy/embedded-enforcement/embedding_csp-header-invalid-format.html.ini
testing/web-platform/meta/content-security-policy/embedded-enforcement/embedding_csp-header.html.ini
testing/web-platform/meta/cssom-view/HTMLBody-ScrollArea_quirksmode.html.ini
testing/web-platform/meta/cssom/CSSStyleRule.html.ini
testing/web-platform/meta/html/browsers/windows/browsing-context-names/choose-_blank-003.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.html.ini
testing/web-platform/meta/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.html.ini
testing/web-platform/meta/html/semantics/selectors/pseudo-classes/enabled.html.ini
testing/web-platform/meta/html/webappapis/scripting/events/inline-event-handler-ordering.html.ini
testing/web-platform/meta/html/webappapis/scripting/events/invalid-uncompiled-raw-handler-compiled-late.html.ini
testing/web-platform/meta/intersection-observer/multiple-targets.html.ini
testing/web-platform/meta/service-workers/service-worker/fetch-request-fallback.https.html.ini
testing/web-platform/tests/conformance-checkers/html-svg/animate-elem-77-t-novalid.html
testing/web-platform/tests/conformance-checkers/html-svg/linking-a-10-f-novalid.html
testing/web-platform/tests/conformance-checkers/html/elements/picture/srcset-microsyntax-leading-dot-x-novalid.html
testing/web-platform/tests/conformance-checkers/xhtml/elements/menu/001-haswarn.xhtml
testing/web-platform/tests/conformance-checkers/xhtml/elements/menu/001-novalid.xhtml
testing/web-platform/tests/content-security-policy/_unapproved/script-nonces-hidden-meta.html
testing/web-platform/tests/content-security-policy/_unapproved/script-nonces-hidden.html
testing/web-platform/tests/content-security-policy/_unapproved/script-nonces-hidden.html.headers
testing/web-platform/tests/content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html
testing/web-platform/tests/content-security-policy/_unapproved/svgscript-nonces-hidden.html
testing/web-platform/tests/content-security-policy/_unapproved/svgscript-nonces-hidden.html.headers
testing/web-platform/tests/content-security-policy/base-uri/base-uri_iframe_sandbox.sub.html.headers
testing/web-platform/tests/content-security-policy/embedded-enforcement/embedding_csp-header-invalid-format.html
testing/web-platform/tests/content-security-policy/embedded-enforcement/embedding_csp-header.html
testing/web-platform/tests/content-security-policy/embedded-enforcement/support/echo-embedding-csp.py
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.drawImage.canvas.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.drawImage.image.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.fillStyle.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.strokeStyle.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.canvas.timing.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.create.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.cross.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.image.fillStyle.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.pattern.image.strokeStyle.html
testing/web-platform/tests/html/semantics/embedded-content/the-canvas-element/security.reset.html
testing/web-platform/tests/html/semantics/forms/the-form-element/form-action-url.html
testing/web-platform/tests/html/semantics/forms/the-form-element/resources/form-action-url-iframe.html
testing/web-platform/tests/html/semantics/interactive-elements/context-menus/contextmenu-event-manual.htm
testing/web-platform/tests/html/semantics/interactive-elements/the-command-element/.gitkeep
testing/web-platform/tests/html/semantics/interactive-elements/the-menu-element/contains.json
testing/web-platform/tests/html/semantics/interactive-elements/the-menu-element/menuitem-label.html
testing/web-platform/tests/html/webappapis/scripting/events/event-handler-onauxclick.html
testing/web-platform/tests/resources/examples/apisample-error-worker.js
testing/web-platform/tests/resources/examples/apisample-worker.js
testing/web-platform/tests/resources/examples/apisample.htm
testing/web-platform/tests/resources/examples/apisample10.html
testing/web-platform/tests/resources/examples/apisample11.html
testing/web-platform/tests/resources/examples/apisample12.html
testing/web-platform/tests/resources/examples/apisample13.html
testing/web-platform/tests/resources/examples/apisample14.html
testing/web-platform/tests/resources/examples/apisample15.html
testing/web-platform/tests/resources/examples/apisample16.html
testing/web-platform/tests/resources/examples/apisample17.html
testing/web-platform/tests/resources/examples/apisample18.html
testing/web-platform/tests/resources/examples/apisample19.html
testing/web-platform/tests/resources/examples/apisample2.htm
testing/web-platform/tests/resources/examples/apisample3.htm
testing/web-platform/tests/resources/examples/apisample4.htm
testing/web-platform/tests/resources/examples/apisample5.htm
testing/web-platform/tests/resources/examples/apisample6.html
testing/web-platform/tests/resources/examples/apisample7.html
testing/web-platform/tests/resources/examples/apisample8.html
testing/web-platform/tests/resources/examples/apisample9.html
testing/web-platform/tests/service-workers/service-worker/resources/opaque-response-preloaded-iframe.html
testing/web-platform/tests/webusb/resources/check-availability.html
testing/web-platform/tests/webusb/resources/featurepolicytest.js
testing/web-platform/tests/workers/interfaces.idl
toolkit/components/telemetry/Scalars.yaml
widget/cocoa/nsChildView.mm
xpcom/glue/tests/gtest/TestFileUtils.cpp
xpcom/glue/tests/gtest/moz.build
--- a/browser/app/macbuild/Contents/MacOS-files.in
+++ b/browser/app/macbuild/Contents/MacOS-files.in
@@ -4,10 +4,11 @@
 /firefox-bin
 /gtest/***
 #if defined(MOZ_ASAN) || defined(MOZ_TSAN)
 /llvm-symbolizer
 #endif
 /pingsender
 /pk12util
 /ssltunnel
+/webrtc-gtest
 /xpcshell
 /XUL
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -263,17 +263,17 @@ pref("browser.shell.didSkipDefaultBrowse
 pref("browser.shell.defaultBrowserCheckCount", 0);
 pref("browser.defaultbrowser.notificationbar", false);
 
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
 pref("browser.startup.homepage",            "chrome://branding/locale/browserconfig.properties");
 // Whether we should skip the homepage when opening the first-run page
-pref("browser.startup.firstrunSkipsHomepage", false);
+pref("browser.startup.firstrunSkipsHomepage", true);
 
 pref("browser.slowStartup.notificationDisabled", false);
 pref("browser.slowStartup.timeThreshold", 30000);
 pref("browser.slowStartup.maxSamples", 5);
 
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
@@ -1053,17 +1053,17 @@ pref("dom.ipc.plugins.sandbox-level.flas
 
 #if defined(MOZ_CONTENT_SANDBOX)
 // This controls the strength of the Windows content process sandbox for testing
 // purposes. This will require a restart.
 // On windows these levels are:
 // See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
 // SetSecurityLevelForContentProcess() for what the different settings mean.
 #if defined(NIGHTLY_BUILD)
-pref("security.sandbox.content.level", 2);
+pref("security.sandbox.content.level", 3);
 #else
 pref("security.sandbox.content.level", 1);
 #endif
 
 // This controls the depth of stack trace that is logged when Windows sandbox
 // logging is turned on.  This is only currently available for the content
 // process because the only other sandbox (for GMP) has too strict a policy to
 // allow stack tracing.  This does not require a restart to take effect.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4994,17 +4994,17 @@ var CombinedStopReload = {
   setAnimationImageHeightRelativeToToolbarButtonHeight() {
     let dwu = window.getInterface(Ci.nsIDOMWindowUtils);
     let toolbarItem = this.stopReloadContainer.closest(".customization-target > toolbaritem");
     let bounds = dwu.getBoundsWithoutFlushing(toolbarItem);
     toolbarItem.style.setProperty("--toolbarbutton-height", bounds.height + "px");
   },
 
   switchToStop(aRequest, aWebProgress) {
-    if (!this._initialized)
+    if (!this._initialized || !this._shouldSwitch(aRequest))
       return;
 
     let shouldAnimate = AppConstants.MOZ_PHOTON_ANIMATIONS &&
                         aRequest instanceof Ci.nsIRequest &&
                         aWebProgress.isTopLevel &&
                         aWebProgress.isLoadingDocument &&
                         this.animate;
 
@@ -5014,17 +5014,18 @@ var CombinedStopReload = {
       this.stopReloadContainer.setAttribute("animate", "true");
     } else {
       this.stopReloadContainer.removeAttribute("animate");
     }
     this.reload.setAttribute("displaystop", "true");
   },
 
   switchToReload(aRequest, aWebProgress) {
-    if (!this._initialized)
+    if (!this._initialized || !this._shouldSwitch(aRequest) ||
+        !this.reload.hasAttribute("displaystop"))
       return;
 
     let shouldAnimate = AppConstants.MOZ_PHOTON_ANIMATIONS &&
                         aRequest instanceof Ci.nsIRequest &&
                         aWebProgress.isTopLevel &&
                         !aWebProgress.isLoadingDocument &&
                         this.animate;
 
@@ -5053,16 +5054,29 @@ var CombinedStopReload = {
     this.reload.disabled = true;
     this._timer = setTimeout(function(self) {
       self._timer = 0;
       self.reload.disabled = XULBrowserWindow.reloadCommand
                                              .getAttribute("disabled") == "true";
     }, 650, this);
   },
 
+  _shouldSwitch(aRequest) {
+    if (!aRequest ||
+        !aRequest.originalURI ||
+        aRequest.originalURI.spec.startsWith("about:reader"))
+      return true;
+
+    if (aRequest.originalURI.schemeIs("chrome") ||
+        aRequest.originalURI.schemeIs("about"))
+      return false;
+
+    return true;
+  },
+
   _cancelTransition() {
     if (this._timer) {
       clearTimeout(this._timer);
       this._timer = 0;
     }
   }
 };
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/POSTSearchEngine.xml
@@ -0,0 +1,6 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+  <ShortName>POST Search</ShortName>
+  <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/base/content/test/about/print_postdata.sjs">
+    <Param name="searchterms" value="{searchTerms}"/>
+  </Url>
+</OpenSearchDescription>
rename from browser/base/content/test/general/aboutHome_content_script.js
rename to browser/base/content/test/about/aboutHome_content_script.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/browser.ini
@@ -0,0 +1,21 @@
+[DEFAULT]
+support-files =
+  aboutHome_content_script.js
+  head.js
+  healthreport_pingData.js
+  healthreport_testRemoteCommands.html
+  print_postdata.sjs
+  searchSuggestionEngine.sjs
+  searchSuggestionEngine.xml
+  test_bug959531.html
+  POSTSearchEngine.xml
+
+[browser_aboutCertError.js]
+[browser_aboutStopReload.js]
+[browser_aboutNetError.js]
+[browser_aboutSupport.js]
+[browser_aboutSupport_newtab_security_state.js]
+[browser_aboutHealthReport.js]
+skip-if = os == "linux" # Bug 924307
+[browser_aboutHome.js]
+[browser_aboutHome_wrapsCorrectly.js]
rename from browser/base/content/test/general/browser_aboutCertError.js
rename to browser/base/content/test/about/browser_aboutCertError.js
rename from browser/base/content/test/general/browser_aboutHealthReport.js
rename to browser/base/content/test/about/browser_aboutHealthReport.js
--- a/browser/base/content/test/general/browser_aboutHealthReport.js
+++ b/browser/base/content/test/about/browser_aboutHealthReport.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 
-const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
-const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
+const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/about/";
+const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/about/";
 
 const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
 const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
 
 const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
 
 registerCleanupFunction(function() {
   // Ensure we don't pollute prefs for next tests.
rename from browser/base/content/test/general/browser_aboutHome.js
rename to browser/base/content/test/about/browser_aboutHome.js
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/about/browser_aboutHome.js
@@ -7,17 +7,17 @@ requestLongerTimeout(4);
 ignoreAllUncaughtExceptions();
 
 XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils",
   "resource:///modules/AboutHome.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
   "resource://gre/modules/AppConstants.jsm");
 
 const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/" +
-  "content/test/general/aboutHome_content_script.js";
+  "content/test/about/aboutHome_content_script.js";
 var gRightsVersion = Services.prefs.getIntPref("browser.rights.version");
 
 registerCleanupFunction(function() {
   // Ensure we don't pollute prefs for next tests.
   Services.prefs.clearUserPref("network.cookies.cookieBehavior");
   Services.prefs.clearUserPref("network.cookie.lifetimePolicy");
   Services.prefs.clearUserPref("browser.rights.override");
   Services.prefs.clearUserPref("browser.rights." + gRightsVersion + ".shown");
@@ -264,28 +264,28 @@ add_task(async function() {
 
         Services.search.defaultEngine = currEngine;
         try {
           Services.search.removeEngine(engine);
         } catch (ex) {}
         resolve();
       };
       Services.obs.addObserver(searchObserver, "browser-search-engine-modified");
-      Services.search.addEngine("http://test:80/browser/browser/base/content/test/general/POSTSearchEngine.xml",
+      Services.search.addEngine("http://test:80/browser/browser/base/content/test/about/POSTSearchEngine.xml",
                                 null, null, false);
     });
   });
 });
 
 add_task(async function() {
   info("Make sure that a page can't imitate about:home");
 
   await BrowserTestUtils.withNewTab({ gBrowser, url: "about:home" }, async function(browser) {
     let promise = BrowserTestUtils.browserLoaded(browser);
-    browser.loadURI("https://example.com/browser/browser/base/content/test/general/test_bug959531.html");
+    browser.loadURI("https://example.com/browser/browser/base/content/test/about/test_bug959531.html");
     await promise;
 
     await ContentTask.spawn(browser, null, async function() {
       let button = content.document.getElementById("settings");
       ok(button, "Found settings button in test page");
       button.click();
     });
 
rename from browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js
rename to browser/base/content/test/about/browser_aboutHome_wrapsCorrectly.js
rename from browser/base/content/test/general/browser_aboutNetError.js
rename to browser/base/content/test/about/browser_aboutNetError.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/browser_aboutStopReload.js
@@ -0,0 +1,90 @@
+async function waitForNoAnimation(elt) {
+  return BrowserTestUtils.waitForCondition(() => !elt.hasAttribute("animate"));
+}
+
+async function getAnimatePromise(elt) {
+  return BrowserTestUtils.waitForAttribute("animate", elt)
+    .then(() => Assert.ok(true, `${elt.id} should animate`));
+}
+
+function stopReloadMutationCallback() {
+  Assert.ok(false, "stop-reload's animate attribute should not have been mutated");
+}
+
+add_task(async function checkDontShowStopOnNewTab() {
+  let stopReloadContainer = document.getElementById("stop-reload-button");
+  let stopReloadContainerObserver = new MutationObserver(stopReloadMutationCallback);
+
+  await waitForNoAnimation(stopReloadContainer);
+  stopReloadContainerObserver.observe(stopReloadContainer, { attributeFilter: ["animate"]});
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+                                                        opening: "about:robots",
+                                                        waitForStateStop: true});
+  await BrowserTestUtils.removeTab(tab);
+
+  Assert.ok(true, "Test finished: stop-reload does not animate when navigating to local URI on new tab");
+  stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDontShowStopFromLocalURI() {
+  let stopReloadContainer = document.getElementById("stop-reload-button");
+  let stopReloadContainerObserver = new MutationObserver(stopReloadMutationCallback);
+
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+                                                        opening: "about:robots",
+                                                        waitForStateStop: true});
+  await waitForNoAnimation(stopReloadContainer);
+  stopReloadContainerObserver.observe(stopReloadContainer, { attributeFilter: ["animate"]});
+  await BrowserTestUtils.loadURI(tab.linkedBrowser, "about:mozilla");
+  await BrowserTestUtils.removeTab(tab);
+
+  Assert.ok(true, "Test finished: stop-reload does not animate when navigating between local URIs");
+  stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDontShowStopFromNonLocalURI() {
+  let stopReloadContainer = document.getElementById("stop-reload-button");
+  let stopReloadContainerObserver = new MutationObserver(stopReloadMutationCallback);
+
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+                                                        opening: "https://example.com",
+                                                        waitForStateStop: true});
+  await waitForNoAnimation(stopReloadContainer);
+  stopReloadContainerObserver.observe(stopReloadContainer, { attributeFilter: ["animate"]});
+  await BrowserTestUtils.loadURI(tab.linkedBrowser, "about:mozilla");
+  await BrowserTestUtils.removeTab(tab);
+
+  Assert.ok(true, "Test finished: stop-reload does not animate when navigating to local URI from non-local URI");
+  stopReloadContainerObserver.disconnect();
+});
+
+add_task(async function checkDoShowStopOnNewTab() {
+  let stopReloadContainer = document.getElementById("stop-reload-button");
+  let animatePromise = getAnimatePromise(stopReloadContainer);
+
+  await waitForNoAnimation(stopReloadContainer);
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+                                                        opening: "https://example.com",
+                                                        waitForStateStop: true});
+  await animatePromise;
+  await waitForNoAnimation(stopReloadContainer);
+  await BrowserTestUtils.removeTab(tab);
+
+  info("Test finished: stop-reload animates when navigating to non-local URI on new tab");
+});
+
+add_task(async function checkDoShowStopFromLocalURI() {
+  let stopReloadContainer = document.getElementById("stop-reload-button");
+
+  await waitForNoAnimation(stopReloadContainer);
+  let tab = await BrowserTestUtils.openNewForegroundTab({gBrowser,
+                                                        opening: "about:robots",
+                                                        waitForStateStop: true});
+  let animatePromise = getAnimatePromise(stopReloadContainer);
+  await BrowserTestUtils.loadURI(tab.linkedBrowser, "https://example.com");
+  await animatePromise;
+  await waitForNoAnimation(stopReloadContainer);
+  await BrowserTestUtils.removeTab(tab);
+
+  info("Test finished: stop-reload animates when navigating to non-local URI from local URI");
+});
rename from browser/base/content/test/general/browser_aboutSupport.js
rename to browser/base/content/test/about/browser_aboutSupport.js
rename from browser/base/content/test/general/browser_aboutSupport_newtab_security_state.js
rename to browser/base/content/test/about/browser_aboutSupport_newtab_security_state.js
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/head.js
@@ -0,0 +1,155 @@
+/* eslint-env mozilla/frame-script */
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+  retryTimes = typeof retryTimes !== "undefined" ? retryTimes : 30;
+  var tries = 0;
+  var interval = setInterval(function() {
+    if (tries >= retryTimes) {
+      ok(false, errorMsg);
+      moveOn();
+    }
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
+      moveOn();
+    }
+    tries++;
+  }, 100);
+  var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+  return new Promise(resolve => {
+    waitForCondition(aConditionFn, resolve, "Condition didn't pass.");
+  });
+}
+
+function whenTabLoaded(aTab, aCallback) {
+  promiseTabLoadEvent(aTab).then(aCallback);
+}
+
+function promiseTabLoaded(aTab) {
+  return new Promise(resolve => {
+    whenTabLoaded(aTab, resolve);
+  });
+}
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ *        The tab to load into.
+ * @param [optional] url
+ *        The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url) {
+  info("Wait tab event: load");
+
+  function handle(loadedUrl) {
+    if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+      info(`Skipping spurious load event for ${loadedUrl}`);
+      return false;
+    }
+
+    info("Tab event received: load");
+    return true;
+  }
+
+  let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+  if (url)
+    BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+  return loaded;
+}
+
+/**
+ * Waits for the next top-level document load in the current browser.  The URI
+ * of the document is compared against aExpectedURL.  The load is then stopped
+ * before it actually starts.
+ *
+ * @param aExpectedURL
+ *        The URL of the document that is expected to load.
+ * @param aStopFromProgressListener
+ *        Whether to cancel the load directly from the progress listener. Defaults to true.
+ *        If you're using this method to avoid hitting the network, you want the default (true).
+ *        However, the browser UI will behave differently for loads stopped directly from
+ *        the progress listener (effectively in the middle of a call to loadURI) and so there
+ *        are cases where you may want to avoid stopping the load directly from within the
+ *        progress listener callback.
+ * @return promise
+ */
+function waitForDocLoadAndStopIt(aExpectedURL, aBrowser = gBrowser.selectedBrowser, aStopFromProgressListener = true) {
+  function content_script(contentStopFromProgressListener) {
+    let { interfaces: Ci, utils: Cu } = Components;
+    Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+    let wp = docShell.QueryInterface(Ci.nsIWebProgress);
+
+    function stopContent(now, uri) {
+      if (now) {
+        /* Hammer time. */
+        content.stop();
+
+        /* Let the parent know we're done. */
+        sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
+      } else {
+        setTimeout(stopContent.bind(null, true, uri), 0);
+      }
+    }
+
+    let progressListener = {
+      onStateChange(webProgress, req, flags, status) {
+        dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
+
+        if (webProgress.isTopLevel &&
+            flags & Ci.nsIWebProgressListener.STATE_START) {
+          wp.removeProgressListener(progressListener);
+
+          let chan = req.QueryInterface(Ci.nsIChannel);
+          dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
+
+          stopContent(contentStopFromProgressListener, chan.originalURI.spec);
+        }
+      },
+      QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
+    };
+    wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
+
+    /**
+     * As |this| is undefined and we can't extend |docShell|, adding an unload
+     * event handler is the easiest way to ensure the weakly referenced
+     * progress listener is kept alive as long as necessary.
+     */
+    addEventListener("unload", function() {
+      try {
+        wp.removeProgressListener(progressListener);
+      } catch (e) { /* Will most likely fail. */ }
+    });
+  }
+
+  return new Promise((resolve, reject) => {
+    function complete({ data }) {
+      is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
+      mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+      resolve();
+    }
+
+    let mm = aBrowser.messageManager;
+    mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
+    mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+    info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+  });
+}
+
+function promiseDisableOnboardingTours() {
+  return SpecialPowers.pushPrefEnv({set: [["browser.onboarding.enabled", false]]});
+}
rename from browser/base/content/test/general/healthreport_pingData.js
rename to browser/base/content/test/about/healthreport_pingData.js
rename from browser/base/content/test/general/healthreport_testRemoteCommands.html
rename to browser/base/content/test/about/healthreport_testRemoteCommands.html
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/print_postdata.sjs
@@ -0,0 +1,22 @@
+const CC = Components.Constructor;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+                             "nsIBinaryInputStream",
+                             "setInputStream");
+
+function handleRequest(request, response) {
+  response.setHeader("Content-Type", "text/plain", false);
+  if (request.method == "GET") {
+    response.write(request.queryString);
+  } else {
+    var body = new BinaryInputStream(request.bodyInputStream);
+
+    var avail;
+    var bytes = [];
+
+    while ((avail = body.available()) > 0)
+      Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+    var data = String.fromCharCode.apply(null, bytes);
+    response.bodyOutputStream.write(data, data.length);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/searchSuggestionEngine.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+  let suffixes = ["foo", "bar"];
+  let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+  resp.setHeader("Content-Type", "application/json", false);
+  resp.write(JSON.stringify(data));
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/about/searchSuggestionEngine.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/about/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"/>
+</SearchPlugin>
rename from browser/base/content/test/general/test_bug959531.html
rename to browser/base/content/test/about/test_bug959531.html
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -6,17 +6,16 @@
 ###############################################################################
 
 [DEFAULT]
 support-files =
   POSTSearchEngine.xml
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
-  aboutHome_content_script.js
   audio.ogg
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
   browser_bug970746.xhtml
   browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
@@ -44,18 +43,16 @@ support-files =
   file_bug970276_favicon2.ico
   file_documentnavigation_frameset.html
   file_double_close_tab.html
   file_favicon_change.html
   file_favicon_change_not_in_document.html
   file_fullscreen-window-open.html
   file_with_link_to_http.html
   head.js
-  healthreport_pingData.js
-  healthreport_testRemoteCommands.html
   moz.png
   navigating_window_with_download.html
   offlineQuotaNotification.cacheManifest
   offlineQuotaNotification.html
   page_style_sample.html
   pinning_headers.sjs
   ssl_error_reports.sjs
   print_postdata.sjs
@@ -63,17 +60,16 @@ support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine2.xml
   subtst_contextmenu.html
   subtst_contextmenu_input.html
   subtst_contextmenu_xul.xul
   test_bug462673.html
   test_bug628179.html
   test_bug839103.html
-  test_bug959531.html
   test_process_flags_chrome.html
   title_test.svg
   unknownContentType_file.pif
   unknownContentType_file.pif^headers^
   video.ogg
   web_video.html
   web_video1.ogv
   web_video1.ogv^headers^
@@ -87,32 +83,16 @@ support-files =
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
   !/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
   !/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
 
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutCertError.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutNetError.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutSupport.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutSupport_newtab_security_state.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutHealthReport.js]
-skip-if = os == "linux" # Bug 924307
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutHome.js]
-skip-if = true # Bug 1374537
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_aboutHome_wrapsCorrectly.js]
-# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_addKeywordSearch.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_alltabslistener.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_audioTabIcon.js]
 tags = audiochannel
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_backButtonFitts.js]
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -355,94 +355,16 @@ function promiseHistoryClearedState(aURI
         callbackDone();
       });
     });
 
   });
 }
 
 /**
- * Waits for the next top-level document load in the current browser.  The URI
- * of the document is compared against aExpectedURL.  The load is then stopped
- * before it actually starts.
- *
- * @param aExpectedURL
- *        The URL of the document that is expected to load.
- * @param aStopFromProgressListener
- *        Whether to cancel the load directly from the progress listener. Defaults to true.
- *        If you're using this method to avoid hitting the network, you want the default (true).
- *        However, the browser UI will behave differently for loads stopped directly from
- *        the progress listener (effectively in the middle of a call to loadURI) and so there
- *        are cases where you may want to avoid stopping the load directly from within the
- *        progress listener callback.
- * @return promise
- */
-function waitForDocLoadAndStopIt(aExpectedURL, aBrowser = gBrowser.selectedBrowser, aStopFromProgressListener = true) {
-  function content_script(contentStopFromProgressListener) {
-    let { interfaces: Ci, utils: Cu } = Components;
-    Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-    let wp = docShell.QueryInterface(Ci.nsIWebProgress);
-
-    function stopContent(now, uri) {
-      if (now) {
-        /* Hammer time. */
-        content.stop();
-
-        /* Let the parent know we're done. */
-        sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
-      } else {
-        setTimeout(stopContent.bind(null, true, uri), 0);
-      }
-    }
-
-    let progressListener = {
-      onStateChange(webProgress, req, flags, status) {
-        dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
-
-        if (webProgress.isTopLevel &&
-            flags & Ci.nsIWebProgressListener.STATE_START) {
-          wp.removeProgressListener(progressListener);
-
-          let chan = req.QueryInterface(Ci.nsIChannel);
-          dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
-
-          stopContent(contentStopFromProgressListener, chan.originalURI.spec);
-        }
-      },
-      QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
-    };
-    wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
-
-    /**
-     * As |this| is undefined and we can't extend |docShell|, adding an unload
-     * event handler is the easiest way to ensure the weakly referenced
-     * progress listener is kept alive as long as necessary.
-     */
-    addEventListener("unload", function() {
-      try {
-        wp.removeProgressListener(progressListener);
-      } catch (e) { /* Will most likely fail. */ }
-    });
-  }
-
-  return new Promise((resolve, reject) => {
-    function complete({ data }) {
-      is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
-      mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
-      resolve();
-    }
-
-    let mm = aBrowser.messageManager;
-    mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
-    mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
-    info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
-  });
-}
-
-/**
  * Waits for the next load to complete in any browser or the given browser.
  * If a <tabbrowser> is given it waits for a load in any of its browsers.
  *
  * @return promise
  */
 function waitForDocLoadComplete(aBrowser = gBrowser) {
   return new Promise(resolve => {
     let listener = {
@@ -824,12 +746,8 @@ function getCertExceptionDialog(aLocatio
 
       if (childDoc.location.href == aLocation) {
         return childDoc;
       }
     }
   }
   return undefined;
 }
-
-function promiseDisableOnboardingTours() {
-  return SpecialPowers.pushPrefEnv({set: [["browser.onboarding.enabled", false]]});
-}
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -10,16 +10,17 @@ MOCHITEST_MANIFESTS += [
     'content/test/general/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'content/test/chrome/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
+    'content/test/about/browser.ini',
     'content/test/alerts/browser.ini',
     'content/test/captivePortal/browser.ini',
     'content/test/contextMenu/browser.ini',
     'content/test/forms/browser.ini',
     'content/test/general/browser.ini',
     'content/test/newtab/browser.ini',
     'content/test/pageinfo/browser.ini',
     'content/test/performance/browser.ini',
--- a/browser/components/downloads/DownloadsViewUI.jsm
+++ b/browser/components/downloads/DownloadsViewUI.jsm
@@ -220,21 +220,22 @@ this.DownloadsViewUI.DownloadElementShel
         stateLabel = s.fileMovedOrMissing;
         hoverStatus = stateLabel;
       } else if (this.download.succeeded) {
         // For completed downloads, show the file size (e.g. "1.5 MB").
         if (this.download.target.size !== undefined) {
           let [size, unit] =
             DownloadUtils.convertByteUnits(this.download.target.size);
           stateLabel = s.sizeWithUnits(size, unit);
+          status = s.statusSeparator(s.stateCompleted, stateLabel);
         } else {
           // History downloads may not have a size defined.
           stateLabel = s.sizeUnknown;
+          status = s.stateCompleted;
         }
-        status = s.stateCompleted;
         hoverStatus = status;
       } else if (this.download.canceled) {
         stateLabel = s.stateCanceled;
       } else if (this.download.error.becauseBlockedByParentalControls) {
         stateLabel = s.stateBlockedParentalControls;
       } else if (this.download.error.becauseBlockedByReputationCheck) {
         stateLabel = this.rawBlockedTitleAndDetails[0];
       } else {
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -24,22 +24,24 @@ add_task(async function testBrowserActio
 
   await extension.startup();
 
   let browser = await openPanel(extension, undefined, true);
 
   async function checkSize(expected) {
     let dims = await promiseContentDimensions(browser);
 
-    is(dims.window.innerHeight, expected, `Panel window should be ${expected}px tall`);
+    Assert.lessOrEqual(Math.abs(dims.window.innerHeight - expected), 1,
+                       `Panel window should be ${expected}px tall (was ${dims.window.innerHeight})`);
     is(dims.body.clientHeight, dims.body.scrollHeight,
       "Panel body should be tall enough to fit its contents");
 
     // Tolerate if it is 1px too wide, as that may happen with the current resizing method.
-    ok(Math.abs(dims.window.innerWidth - expected) <= 1, `Panel window should be ${expected}px wide`);
+    Assert.lessOrEqual(Math.abs(dims.window.innerWidth - expected), 1,
+                       `Panel window should be ${expected}px wide`);
     is(dims.body.clientWidth, dims.body.scrollWidth,
       "Panel body should be wide enough to fit its contents");
   }
 
   /* eslint-disable mozilla/no-cpows-in-tests */
   function setSize(size) {
     content.document.body.style.height = `${size}px`;
     content.document.body.style.width = `${size}px`;
@@ -165,17 +167,17 @@ async function testPopupSize(standardsMo
 
     let panelRect = panel.getBoundingClientRect();
     if (arrowSide == "top") {
       ok(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
       ok(panelRect.bottom >= origPanelRect.bottom, `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`);
 
       let screenBottom = browserWin.screen.availTop + browserWin.screen.availHeight;
       let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom;
-      ok(panelBottom <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`);
+      ok(Math.round(panelBottom) <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`);
     } else {
       ok(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
       ok(panelRect.top <= origPanelRect.top, `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`);
 
       let panelTop = browserWin.mozInnerScreenY + panelRect.top;
       ok(panelTop >= browserWin.screen.availTop, `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`);
     }
   };
@@ -215,32 +217,32 @@ async function testPopupSize(standardsMo
 
   dims = await alterContent(browser, setClass, "big");
   let win = dims.window;
 
   ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
 
   is(win.innerWidth, innerWidth, "Window width should not change");
   ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
-  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+  Assert.lessOrEqual(win.scrollMaxY, 1, "Document should not be vertically scrollable");
 
   checkPanelPosition();
 
 
   info("Increase body children's width and height. " +
        "Expect them to wrap, and the frame to grow vertically rather than widen.");
 
   dims = await alterContent(browser, setClass, "bigger");
   win = dims.window;
 
   ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
 
   is(win.innerWidth, innerWidth, "Window width should not change");
   ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
-  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+  Assert.lessOrEqual(win.scrollMaxY, 1, "Document should not be vertically scrollable");
 
   checkPanelPosition();
 
 
   info("Increase body height beyond the height of the screen. " +
        "Expect the panel to grow to accommodate, but not larger than the height of the screen.");
 
   dims = await alterContent(browser, setClass, "huge");
@@ -259,17 +261,17 @@ async function testPopupSize(standardsMo
   info("Restore original styling. Expect original dimensions.");
   dims = await alterContent(browser, setClass, "");
   win = dims.window;
 
   is(getHeight(), height, "Browser height should return to its original value");
 
   is(win.innerWidth, innerWidth, "Window width should not change");
   is(win.innerHeight, innerHeight, "Window height should return to its original value");
-  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+  Assert.lessOrEqual(win.scrollMaxY, 1, "Document should not be vertically scrollable");
 
   checkPanelPosition();
 
   await closeBrowserAction(extension, browserWin);
 
   if (overflowView) {
     overflowView.style.removeProperty("min-height");
   }
--- a/browser/components/preferences/in-content-new/sync.js
+++ b/browser/components/preferences/in-content-new/sync.js
@@ -21,29 +21,27 @@ const FXA_PAGE_LOGGED_IN = 1;
 // We are in a successful verified state - everything should work!
 const FXA_LOGIN_VERIFIED = 0;
 // We have logged in to an unverified account.
 const FXA_LOGIN_UNVERIFIED = 1;
 // We are logged in locally, but the server rejected our credentials.
 const FXA_LOGIN_FAILED = 2;
 
 var gSyncPane = {
-  prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
-              "engine.tabs", "engine.history"],
-
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
   },
 
   set page(val) {
     document.getElementById("weavePrefsDeck").selectedIndex = val;
   },
 
   init() {
     this._setupEventListeners();
+    this._adjustForPrefs();
 
     // If the Service hasn't finished initializing, wait for it.
     let xps = Components.classes["@mozilla.org/weave/service;1"]
                                 .getService(Components.interfaces.nsISupports)
                                 .wrappedJSObject;
 
     if (xps.ready) {
       this._init();
@@ -68,16 +66,42 @@ var gSyncPane = {
     };
 
     Services.obs.addObserver(onReady, "weave:service:ready");
     window.addEventListener("unload", onUnload);
 
     xps.ensureLoaded();
   },
 
+  // make whatever tweaks we need based on preferences.
+  _adjustForPrefs() {
+    // These 2 engines are unique in that there are prefs that make the
+    // entire engine unavailable (which is distinct from "disabled").
+    let enginePrefs = [
+      ["services.sync.engine.addresses.available", "engine.addresses"],
+      ["services.sync.engine.creditcards.available", "engine.creditcards"],
+    ];
+    let numHidden = 0;
+    for (let [availablePref, prefName] of enginePrefs) {
+      if (!Services.prefs.getBoolPref(availablePref)) {
+        let checkbox = document.querySelector("[preference=\"" + prefName + "\"]");
+        checkbox.hidden = true;
+        numHidden += 1;
+      }
+    }
+    // If we hid both, the list of prefs is unbalanced, so move "history" to
+    // the second column. (If we only moved one, it's still unbalanced, but
+    // there's an odd number of engines so that can't be avoided)
+    if (numHidden == 2) {
+      let history = document.querySelector("[preference=\"engine.history\"]");
+      let addons = document.querySelector("[preference=\"engine.addons\"]");
+      addons.parentNode.insertBefore(history, addons);
+    }
+  },
+
   _showLoadPage(xps) {
     let username = Services.prefs.getCharPref("services.sync.username", "");
     if (!username) {
       this.page = FXA_PAGE_LOGGED_OUT;
       return;
     }
 
     // Use cached values while we wait for the up-to-date values
--- a/browser/components/preferences/in-content-new/sync.xul
+++ b/browser/components/preferences/in-content-new/sync.xul
@@ -18,16 +18,22 @@
               name="services.sync.engine.tabs"
               type="bool"/>
   <preference id="engine.prefs"
               name="services.sync.engine.prefs"
               type="bool"/>
   <preference id="engine.passwords"
               name="services.sync.engine.passwords"
               type="bool"/>
+  <preference id="engine.addresses"
+              name="services.sync.engine.addresses"
+              type="bool"/>
+  <preference id="engine.creditcards"
+              name="services.sync.engine.creditcards"
+              type="bool"/>
 </preferences>
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content-new/sync.js"/>
 
 <hbox id="firefoxAccountCategory"
       class="searchCategory"
       hidden="true"
@@ -156,27 +162,33 @@
                         accesskey="&engine.tabs.accesskey;"
                         preference="engine.tabs"/>
               <checkbox label="&engine.bookmarks.label;"
                         accesskey="&engine.bookmarks.accesskey;"
                         preference="engine.bookmarks"/>
               <checkbox label="&engine.logins.label;"
                         accesskey="&engine.logins.accesskey;"
                         preference="engine.passwords"/>
-            </vbox>
-            <vbox align="start" flex="1">
               <checkbox label="&engine.history.label;"
                         accesskey="&engine.history.accesskey;"
                         preference="engine.history"/>
+            </vbox>
+            <vbox align="start" flex="1">
               <checkbox label="&engine.addons.label;"
                         accesskey="&engine.addons.accesskey;"
                         preference="engine.addons"/>
               <checkbox label="&engine.prefs.label;"
                         accesskey="&engine.prefs.accesskey;"
                         preference="engine.prefs"/>
+              <checkbox label="&engine.addresses.label;"
+                        accesskey="&engine.addresses.accesskey;"
+                        preference="engine.addresses"/>
+              <checkbox label="&engine.creditcards.label;"
+                        accesskey="&engine.creditcards.accesskey;"
+                        preference="engine.creditcards"/>
             </vbox>
             <spacer/>
           </hbox>
         </groupbox>
       </vbox>
       <vbox>
         <html:img  class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
       </vbox>
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -21,29 +21,27 @@ const FXA_PAGE_LOGGED_IN = 1;
 // We are in a successful verified state - everything should work!
 const FXA_LOGIN_VERIFIED = 0;
 // We have logged in to an unverified account.
 const FXA_LOGIN_UNVERIFIED = 1;
 // We are logged in locally, but the server rejected our credentials.
 const FXA_LOGIN_FAILED = 2;
 
 var gSyncPane = {
-  prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
-              "engine.tabs", "engine.history"],
-
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
   },
 
   set page(val) {
     document.getElementById("weavePrefsDeck").selectedIndex = val;
   },
 
   init() {
     this._setupEventListeners();
+    this._adjustForPrefs();
 
     // If the Service hasn't finished initializing, wait for it.
     let xps = Components.classes["@mozilla.org/weave/service;1"]
                                 .getService(Components.interfaces.nsISupports)
                                 .wrappedJSObject;
 
     if (xps.ready) {
       this._init();
@@ -68,16 +66,42 @@ var gSyncPane = {
     };
 
     Services.obs.addObserver(onReady, "weave:service:ready");
     window.addEventListener("unload", onUnload);
 
     xps.ensureLoaded();
   },
 
+  // make whatever tweaks we need based on preferences.
+  _adjustForPrefs() {
+    // These 2 engines are unique in that there are prefs that make the
+    // entire engine unavailable (which is distinct from "disabled").
+    let enginePrefs = [
+      ["services.sync.engine.addresses.available", "engine.addresses"],
+      ["services.sync.engine.creditcards.available", "engine.creditcards"],
+    ];
+    let numHidden = 0;
+    for (let [availablePref, prefName] of enginePrefs) {
+      if (!Services.prefs.getBoolPref(availablePref)) {
+        let checkbox = document.querySelector("[preference=\"" + prefName + "\"]");
+        checkbox.hidden = true;
+        numHidden += 1;
+      }
+    }
+    // If we hid both, the list of prefs is unbalanced, so move "history" to
+    // the second column. (If we only moved one, it's still unbalanced, but
+    // there's an odd number of engines so that can't be avoided)
+    if (numHidden == 2) {
+      let history = document.querySelector("[preference=\"engine.history\"]");
+      let addons = document.querySelector("[preference=\"engine.addons\"]");
+      addons.parentNode.insertBefore(history, addons);
+    }
+  },
+
   _showLoadPage(xps) {
     let username = Services.prefs.getCharPref("services.sync.username", "");
     if (!username) {
       this.page = FXA_PAGE_LOGGED_OUT;
       return;
     }
 
     // Use cached values while we wait for the up-to-date values
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -18,16 +18,22 @@
               name="services.sync.engine.tabs"
               type="bool"/>
   <preference id="engine.prefs"
               name="services.sync.engine.prefs"
               type="bool"/>
   <preference id="engine.passwords"
               name="services.sync.engine.passwords"
               type="bool"/>
+  <preference id="engine.addresses"
+              name="services.sync.engine.addresses"
+              type="bool"/>
+  <preference id="engine.creditcards"
+              name="services.sync.engine.creditcards"
+              type="bool"/>
 </preferences>
 
 <script type="application/javascript"
         src="chrome://browser/content/preferences/in-content/sync.js"/>
 
 <hbox id="header-sync"
       class="header"
       hidden="true"
@@ -156,27 +162,33 @@
                         accesskey="&engine.tabs.accesskey;"
                         preference="engine.tabs"/>
               <checkbox label="&engine.bookmarks.label;"
                         accesskey="&engine.bookmarks.accesskey;"
                         preference="engine.bookmarks"/>
               <checkbox label="&engine.logins.label;"
                         accesskey="&engine.logins.accesskey;"
                         preference="engine.passwords"/>
-            </vbox>
-            <vbox align="start" flex="1">
               <checkbox label="&engine.history.label;"
                         accesskey="&engine.history.accesskey;"
                         preference="engine.history"/>
+            </vbox>
+            <vbox align="start" flex="1">
               <checkbox label="&engine.addons.label;"
                         accesskey="&engine.addons.accesskey;"
                         preference="engine.addons"/>
               <checkbox label="&engine.prefs.label;"
                         accesskey="&engine.prefs.accesskey;"
                         preference="engine.prefs"/>
+              <checkbox label="&engine.addresses.label;"
+                        accesskey="&engine.addresses.accesskey;"
+                        preference="engine.addresses"/>
+              <checkbox label="&engine.creditcards.label;"
+                        accesskey="&engine.creditcards.accesskey;"
+                        preference="engine.creditcards"/>
             </vbox>
             <spacer/>
           </hbox>
         </groupbox>
       </vbox>
       <vbox>
         <html:img  class="fxaSyncIllustration" src="chrome://browser/skin/fxa/sync-illustration.svg"/>
       </vbox>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/FormAutofillSync.jsm
@@ -0,0 +1,404 @@
+/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+this.EXPORTED_SYMBOLS = ["AddressesEngine", "CreditCardsEngine"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Log",
+                                  "resource://gre/modules/Log.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "profileStorage",
+                                  "resource://formautofill/ProfileStorage.jsm");
+
+// A helper to sanitize address and creditcard records suitable for logging.
+function sanitizeStorageObject(ob) {
+  if (!ob) {
+    return null;
+  }
+  const whitelist = ["timeCreated", "timeLastUsed", "timeLastModified"];
+  let result = {};
+  for (let key of Object.keys(ob)) {
+    let origVal = ob[key];
+    if (whitelist.includes(key)) {
+      result[key] = origVal;
+    } else if (typeof origVal == "string") {
+      result[key] = "X".repeat(origVal.length);
+    } else {
+      result[key] = typeof(origVal); // *shrug*
+    }
+  }
+  return result;
+}
+
+
+function AutofillRecord(collection, id) {
+  CryptoWrapper.call(this, collection, id);
+}
+
+AutofillRecord.prototype = {
+  __proto__: CryptoWrapper.prototype,
+
+  toEntry() {
+    return Object.assign({
+      guid: this.id,
+    }, this.entry);
+  },
+
+  fromEntry(entry) {
+    this.id = entry.guid;
+    this.entry = entry;
+    // The GUID is already stored in record.id, so we nuke it from the entry
+    // itself to save a tiny bit of space. The profileStorage clones profiles,
+    // so nuking in-place is OK.
+    delete this.entry.guid;
+  },
+
+  cleartextToString() {
+    // And a helper so logging a *Sync* record auto sanitizes.
+    let record = this.cleartext;
+    return JSON.stringify({entry: sanitizeStorageObject(record.entry)});
+  },
+};
+
+// Profile data is stored in the "entry" object of the record.
+Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);
+
+function FormAutofillStore(name, engine) {
+  Store.call(this, name, engine);
+}
+
+FormAutofillStore.prototype = {
+  __proto__: Store.prototype,
+
+  _subStorageName: null, // overridden below.
+  _storage: null,
+
+  get storage() {
+    if (!this._storage) {
+      this._storage = profileStorage[this._subStorageName];
+    }
+    return this._storage;
+  },
+
+  async getAllIDs() {
+    let result = {};
+    for (let {guid} of this.storage.getAll({includeDeleted: true})) {
+      result[guid] = true;
+    }
+    return result;
+  },
+
+  async changeItemID(oldID, newID) {
+    this.storage.changeGUID(oldID, newID);
+  },
+
+  // Note: this function intentionally returns false in cases where we only have
+  // a (local) tombstone - and profileStorage.get() filters them for us.
+  async itemExists(id) {
+    return Boolean(this.storage.get(id));
+  },
+
+  async applyIncoming(remoteRecord) {
+    if (remoteRecord.deleted) {
+      this._log.trace("Deleting record", remoteRecord);
+      this.storage.remove(remoteRecord.id, {sourceSync: true});
+      return;
+    }
+
+    if (await this.itemExists(remoteRecord.id)) {
+      // We will never get a tombstone here, so we are updating a real record.
+      await this._doUpdateRecord(remoteRecord);
+      return;
+    }
+
+    // No matching local record. Try to dedupe a NEW local record.
+    let localDupeID = this.storage.findDuplicateGUID(remoteRecord.toEntry());
+    if (localDupeID) {
+      this._log.trace(`Deduping local record ${localDupeID} to remote`, remoteRecord);
+      // Change the local GUID to match the incoming record, then apply the
+      // incoming record.
+      await this.changeItemID(localDupeID, remoteRecord.id);
+      await this._doUpdateRecord(remoteRecord);
+      return;
+    }
+
+    // We didn't find a dupe, either, so must be a new record (or possibly
+    // a non-deleted version of an item we have a tombstone for, which add()
+    // handles for us.)
+    this._log.trace("Add record", remoteRecord);
+    let entry = remoteRecord.toEntry();
+    this.storage.add(entry, {sourceSync: true});
+  },
+
+  async createRecord(id, collection) {
+    this._log.trace("Create record", id);
+    let record = new AutofillRecord(collection, id);
+    let entry = this.storage.get(id, {
+      rawData: true,
+    });
+    if (entry) {
+      record.fromEntry(entry);
+    } else {
+      // We should consider getting a more authortative indication it's actually deleted.
+      this._log.debug(`Failed to get autofill record with id "${id}", assuming deleted`);
+      record.deleted = true;
+    }
+    return record;
+  },
+
+  async _doUpdateRecord(record) {
+    this._log.trace("Updating record", record);
+
+    let entry = record.toEntry();
+    let {forkedGUID} = this.storage.reconcile(entry);
+    if (this._log.level <= Log.Level.Debug) {
+      let forkedRecord = forkedGUID ? this.storage.get(forkedGUID) : null;
+      let reconciledRecord = this.storage.get(record.id);
+      this._log.debug("Updated local record", {
+        forked: sanitizeStorageObject(forkedRecord),
+        updated: sanitizeStorageObject(reconciledRecord),
+      });
+    }
+  },
+
+  // NOTE: Because we re-implement the incoming/reconcilliation logic we leave
+  // the |create|, |remove| and |update| methods undefined - the base
+  // implementation throws, which is what we want to happen so we can identify
+  // any places they are "accidentally" called.
+};
+
+function FormAutofillTracker(name, engine) {
+  Tracker.call(this, name, engine);
+}
+
+FormAutofillTracker.prototype = {
+  __proto__: Tracker.prototype,
+  observe: function observe(subject, topic, data) {
+    Tracker.prototype.observe.call(this, subject, topic, data);
+    if (topic != "formautofill-storage-changed") {
+      return;
+    }
+    if (subject && subject.wrappedJSObject && subject.wrappedJSObject.sourceSync) {
+      return;
+    }
+    switch (data) {
+      case "add":
+      case "update":
+      case "remove":
+        this.score += SCORE_INCREMENT_XLARGE;
+        break;
+      default:
+        this._log.debug("unrecognized autofill notification", data);
+        break;
+    }
+  },
+
+  // `_ignore` checks the change source for each observer notification, so we
+  // don't want to let the engine ignore all changes during a sync.
+  get ignoreAll() {
+    return false;
+  },
+
+  // Define an empty setter so that the engine doesn't throw a `TypeError`
+  // setting a read-only property.
+  set ignoreAll(value) {},
+
+  startTracking() {
+    Services.obs.addObserver(this, "formautofill-storage-changed");
+  },
+
+  stopTracking() {
+    Services.obs.removeObserver(this, "formautofill-storage-changed");
+  },
+
+  // We never want to persist changed IDs, as the changes are already stored
+  // in ProfileStorage
+  persistChangedIDs: false,
+
+  // Ensure we aren't accidentally using the base persistence.
+  get changedIDs() {
+    throw new Error("changedIDs isn't meaningful for this engine");
+  },
+
+  set changedIDs(obj) {
+    throw new Error("changedIDs isn't meaningful for this engine");
+  },
+
+  addChangedID(id, when) {
+    throw new Error("Don't add IDs to the autofill tracker");
+  },
+
+  removeChangedID(id) {
+    throw new Error("Don't remove IDs from the autofill tracker");
+  },
+
+  // This method is called at various times, so we override with a no-op
+  // instead of throwing.
+  clearChangedIDs() {},
+};
+
+// This uses the same conventions as BookmarkChangeset in
+// services/sync/modules/engines/bookmarks.js. Specifically,
+// - "synced" means the item has already been synced (or we have another reason
+//   to ignore it), and should be ignored in most methods.
+class AutofillChangeset extends Changeset {
+  constructor() {
+    super();
+  }
+
+  getModifiedTimestamp(id) {
+    throw new Error("Don't use timestamps to resolve autofill merge conflicts");
+  }
+
+  has(id) {
+    let change = this.changes[id];
+    if (change) {
+      return !change.synced;
+    }
+    return false;
+  }
+
+  delete(id) {
+    let change = this.changes[id];
+    if (change) {
+      // Mark the change as synced without removing it from the set. We do this
+      // so that we can update ProfileStorage in `trackRemainingChanges`.
+      change.synced = true;
+    }
+  }
+}
+
+function FormAutofillEngine(service, name) {
+  SyncEngine.call(this, name, service);
+}
+
+FormAutofillEngine.prototype = {
+  __proto__: SyncEngine.prototype,
+
+  // the priority for this engine is == addons, so will happen after bookmarks
+  // prefs and tabs, but before forms, history, etc.
+  syncPriority: 5,
+
+  // We don't use SyncEngine.initialize() for this, as we initialize even if
+  // the engine is disabled, and we don't want to be the loader of
+  // ProfileStorage in this case.
+  async _syncStartup() {
+    await profileStorage.initialize();
+    await SyncEngine.prototype._syncStartup.call(this);
+  },
+
+  // We handle reconciliation in the store, not the engine.
+  async _reconcile() {
+    return true;
+  },
+
+  emptyChangeset() {
+    return new AutofillChangeset();
+  },
+
+  async _uploadOutgoing() {
+    this._modified.replace(this._store.storage.pullSyncChanges());
+    await SyncEngine.prototype._uploadOutgoing.call(this);
+  },
+
+  // Typically, engines populate the changeset before downloading records.
+  // However, we handle conflict resolution in the store, so we can wait
+  // to pull changes until we're ready to upload.
+  async pullAllChanges() {
+    return {};
+  },
+
+  async pullNewChanges() {
+    return {};
+  },
+
+  async trackRemainingChanges() {
+    this._store.storage.pushSyncChanges(this._modified.changes);
+  },
+
+  _deleteId(id) {
+    this._noteDeletedId(id);
+  },
+
+  async _resetClient() {
+    this._store.storage.resetSync();
+  },
+};
+
+// The concrete engines
+
+function AddressesRecord(collection, id) {
+  AutofillRecord.call(this, collection, id);
+}
+
+AddressesRecord.prototype = {
+  __proto__: AutofillRecord.prototype,
+  _logName: "Sync.Record.Addresses",
+};
+
+function AddressesStore(name, engine) {
+  FormAutofillStore.call(this, name, engine);
+}
+
+AddressesStore.prototype = {
+  __proto__: FormAutofillStore.prototype,
+  _subStorageName: "addresses",
+};
+
+function AddressesEngine(service) {
+  FormAutofillEngine.call(this, service, "Addresses");
+}
+
+AddressesEngine.prototype = {
+  __proto__: FormAutofillEngine.prototype,
+  _trackerObj: FormAutofillTracker,
+  _storeObj: AddressesStore,
+  _recordObj: AddressesRecord,
+
+  get prefName() {
+    return "addresses";
+  },
+};
+
+function CreditCardsRecord(collection, id) {
+  AutofillRecord.call(this, collection, id);
+}
+
+CreditCardsRecord.prototype = {
+  __proto__: AutofillRecord.prototype,
+  _logName: "Sync.Record.CreditCards",
+};
+
+function CreditCardsStore(name, engine) {
+  FormAutofillStore.call(this, name, engine);
+}
+
+CreditCardsStore.prototype = {
+  __proto__: FormAutofillStore.prototype,
+  _subStorageName: "creditCards",
+};
+
+function CreditCardsEngine(service) {
+  FormAutofillEngine.call(this, service, "CreditCards");
+}
+
+CreditCardsEngine.prototype = {
+  __proto__: FormAutofillEngine.prototype,
+  _trackerObj: FormAutofillTracker,
+  _storeObj: CreditCardsStore,
+  _recordObj: CreditCardsRecord,
+  get prefName() {
+    return "creditcards";
+  },
+};
--- a/browser/extensions/formautofill/ProfileStorage.jsm
+++ b/browser/extensions/formautofill/ProfileStorage.jsm
@@ -79,17 +79,20 @@
  *     }
  *   ]
  * }
  *
  * Sync Metadata:
  *
  * Records may also have a _sync field, which consists of:
  * {
- *   changeCounter, // integer - the number of changes made since a last sync.
+ *   changeCounter,    // integer - the number of changes made since the last
+ *                     // sync.
+ *   lastSyncedFields, // object - hashes of the original values for fields
+ *                     // changed since the last sync.
  * }
  *
  * Records with such a field have previously been synced. Records without such
  * a field are yet to be synced, so are treated specially in some cases (eg,
  * they don't need a tombstone, de-duping logic treats them as special etc).
  * Records without the field are always considered "dirty" from Sync's POV
  * (meaning they will be synced on the next sync), at which time they will gain
  * this new field.
@@ -125,16 +128,19 @@ XPCOMUtils.defineLazyGetter(this, "REGIO
   let countries = Services.strings.createBundle("chrome://global/locale/regionNames.properties").getSimpleEnumeration();
   while (countries.hasMoreElements()) {
     let country = countries.getNext().QueryInterface(Components.interfaces.nsIPropertyElement);
     regionNames[country.key.toUpperCase()] = country.value;
   }
   return regionNames;
 });
 
+const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
+                                          "nsICryptoHash", "initWithString");
+
 const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";
 
 const STORAGE_SCHEMA_VERSION = 1;
 const ADDRESS_SCHEMA_VERSION = 1;
 const CREDIT_CARD_SCHEMA_VERSION = 1;
 
 const VALID_ADDRESS_FIELDS = [
   "given-name",
@@ -188,16 +194,27 @@ const INTERNAL_FIELDS = [
   "guid",
   "version",
   "timeCreated",
   "timeLastUsed",
   "timeLastModified",
   "timesUsed",
 ];
 
+function sha512(string) {
+  if (string == null) {
+    return null;
+  }
+  let encoder = new TextEncoder("utf-8");
+  let bytes = encoder.encode(string);
+  let hash = new CryptoHash("sha512");
+  hash.update(bytes, bytes.length);
+  return hash.finish(/* base64 */ true);
+}
+
 /**
  * Class that manipulates records in a specified collection.
  *
  * Note that it is responsible for converting incoming data to a consistent
  * format in the storage. For example, computed fields will be transformed to
  * the original fields and 2-digit years will be calculated into 4 digits.
  */
 class AutofillRecords {
@@ -236,16 +253,26 @@ class AutofillRecords {
    *
    * @returns {number}
    *          The current schema version number.
    */
   get version() {
     return this._schemaVersion;
   }
 
+  // Ensures that we don't try to apply synced records with newer schema
+  // versions. This is a temporary measure to ensure we don't accidentally
+  // bump the schema version without a syncing strategy in place (bug 1377204).
+  _ensureMatchingVersion(record) {
+    if (record.version != this.version) {
+      throw new Error(`Got unknown record version ${
+        record.version}; want ${this.version}`);
+    }
+  }
+
   /**
    * Adds a new record.
    *
    * @param {Object} record
    *        The new record for saving.
    * @param {boolean} [options.sourceSync = false]
    *        Did sync generate this addition?
    * @returns {string}
@@ -263,26 +290,22 @@ class AutofillRecords {
       if (index > -1) {
         let existing = this._store.data[this._collectionName][index];
         if (existing.deleted) {
           this._store.data[this._collectionName].splice(index, 1);
         } else {
           throw new Error(`Record ${record.guid} already exists`);
         }
       }
-      let recordToSave = Object.assign({}, record, {
-        // `timeLastUsed` and `timesUsed` are always local.
-        timeLastUsed: 0,
-        timesUsed: 0,
-      });
+      let recordToSave = this._clone(record);
       return this._saveRecord(recordToSave, {sourceSync});
     }
 
     if (record.deleted) {
-      return this._saveRecord(record, {sourceSync});
+      return this._saveRecord(record);
     }
 
     let recordToSave = this._clone(record);
     this._normalizeRecord(recordToSave);
 
     recordToSave.guid = this._generateGUID();
     recordToSave.version = this.version;
 
@@ -307,16 +330,17 @@ class AutofillRecords {
         throw new Error("a record with this GUID already exists");
       }
       recordToSave = {
         guid: record.guid,
         timeLastModified: record.timeLastModified || Date.now(),
         deleted: true,
       };
     } else {
+      this._ensureMatchingVersion(record);
       recordToSave = record;
     }
 
     if (sourceSync) {
       let sync = this._getSyncMetaData(recordToSave, true);
       sync.changeCounter = 0;
     }
 
@@ -352,22 +376,28 @@ class AutofillRecords {
 
     let recordFound = this._findByGUID(guid);
     if (!recordFound) {
       throw new Error("No matching record.");
     }
 
     let recordToUpdate = this._clone(record);
     this._normalizeRecord(recordToUpdate);
+
     for (let field of this.VALID_FIELDS) {
-      if (recordToUpdate[field] !== undefined) {
-        recordFound[field] = recordToUpdate[field];
+      let oldValue = recordFound[field];
+      let newValue = recordToUpdate[field];
+
+      if (newValue != null) {
+        recordFound[field] = newValue;
       } else {
         delete recordFound[field];
       }
+
+      this._maybeStoreLastSyncedField(recordFound, field, oldValue);
     }
 
     recordFound.timeLastModified = Date.now();
     let syncMetadata = this._getSyncMetaData(recordFound);
     if (syncMetadata) {
       syncMetadata.changeCounter += 1;
     }
 
@@ -375,17 +405,18 @@ class AutofillRecords {
     this._computeFields(recordFound);
 
     this._store.saveSoon();
     Services.obs.notifyObservers(null, "formautofill-storage-changed", "update");
   }
 
   /**
    * Notifies the storage of the use of the specified record, so we can update
-   * the metadata accordingly.
+   * the metadata accordingly. This does not bump the Sync change counter, since
+   * we don't sync `timesUsed` or `timeLastUsed`.
    *
    * @param  {string} guid
    *         Indicates which record to be notified.
    */
   notifyUsed(guid) {
     this.log.debug("notifyUsed:", guid);
 
     let recordFound = this._findByGUID(guid);
@@ -453,23 +484,24 @@ class AutofillRecords {
    * @param   {boolean} [options.rawData = false]
    *          Returns a raw record without modifications and the computed fields
    *          (this includes private fields)
    * @returns {Object}
    *          A clone of the record.
    */
   get(guid, {rawData = false} = {}) {
     this.log.debug("get:", guid, rawData);
+
     let recordFound = this._findByGUID(guid);
     if (!recordFound) {
       return null;
     }
 
     // The record is cloned to avoid accidental modifications from outside.
-    let clonedRecord = this._clone(recordFound, {rawData});
+    let clonedRecord = this._clone(recordFound);
     if (rawData) {
       this._stripComputedFields(clonedRecord);
     } else {
       this._recordReadProcessor(clonedRecord);
     }
     return clonedRecord;
   }
 
@@ -483,17 +515,17 @@ class AutofillRecords {
    * @returns {Array.<Object>}
    *          An array containing clones of all records.
    */
   getAll({rawData = false, includeDeleted = false} = {}) {
     this.log.debug("getAll", rawData, includeDeleted);
 
     let records = this._store.data[this._collectionName].filter(r => !r.deleted || includeDeleted);
     // Records are cloned to avoid accidental modifications from outside.
-    let clonedRecords = records.map(r => this._clone(r, {rawData}));
+    let clonedRecords = records.map(r => this._clone(r));
     clonedRecords.forEach(record => {
       if (rawData) {
         this._stripComputedFields(record);
       } else {
         this._recordReadProcessor(record);
       }
     });
     return clonedRecords;
@@ -525,16 +557,277 @@ class AutofillRecords {
     this.log.debug("getByFilter:", "Returning", result.length, "result(s)");
     return result;
   }
 
   /**
    * Functions intended to be used in the support of Sync.
    */
 
+  /**
+   * Stores a hash of the last synced value for a field in a locally updated
+   * record. We use this value to rebuild the shared parent, or base, when
+   * reconciling incoming records that may have changed on another device.
+   *
+   * Storing the hash of the values that we last wrote to the Sync server lets
+   * us determine if a remote change conflicts with a local change. If the
+   * hashes for the base, current local value, and remote value all differ, we
+   * have a conflict.
+   *
+   * These fields are not themselves synced, and will be removed locally as
+   * soon as we have successfully written the record to the Sync server - so
+   * it is expected they will not remain for long, as changes which cause a
+   * last synced field to be written will itself cause a sync.
+   *
+   * We also skip this for updates made by Sync, for internal fields, for
+   * records that haven't been uploaded yet, and for fields which have already
+   * been changed since the last sync.
+   *
+   * @param   {Object} record
+   *          The updated local record.
+   * @param   {string} field
+   *          The field name.
+   * @param   {string} lastSyncedValue
+   *          The last synced field value.
+   */
+  _maybeStoreLastSyncedField(record, field, lastSyncedValue) {
+    let sync = this._getSyncMetaData(record);
+    if (!sync) {
+      // The record hasn't been uploaded yet, so we can't end up with merge
+      // conflicts.
+      return;
+    }
+    let alreadyChanged = field in sync.lastSyncedFields;
+    if (alreadyChanged) {
+      // This field was already changed multiple times since the last sync.
+      return;
+    }
+    let newValue = record[field];
+    if (lastSyncedValue != newValue) {
+      sync.lastSyncedFields[field] = sha512(lastSyncedValue);
+    }
+  }
+
+  /**
+   * Attempts a three-way merge between a changed local record, an incoming
+   * remote record, and the shared parent that we synthesize from the last
+   * synced fields - see _maybeStoreLastSyncedField.
+   *
+   * @param   {Object} localRecord
+   *          The changed local record, currently in storage.
+   * @param   {Object} remoteRecord
+   *          The remote record.
+   * @returns {Object|null}
+   *          The merged record, or `null` if there are conflicts and the
+   *          records can't be merged.
+   */
+  _mergeSyncedRecords(localRecord, remoteRecord) {
+    let sync = this._getSyncMetaData(localRecord, true);
+
+    // Copy all internal fields from the remote record. We'll update their
+    // values in `_replaceRecordAt`.
+    let mergedRecord = {};
+    for (let field of INTERNAL_FIELDS) {
+      if (remoteRecord[field] != null) {
+        mergedRecord[field] = remoteRecord[field];
+      }
+    }
+
+    for (let field of this.VALID_FIELDS) {
+      let isLocalSame = false;
+      let isRemoteSame = false;
+      if (field in sync.lastSyncedFields) {
+        // If the field has changed since the last sync, compare hashes to
+        // determine if the local and remote values are different. Hashing is
+        // expensive, but we don't expect this to happen frequently.
+        let lastSyncedValue = sync.lastSyncedFields[field];
+        isLocalSame = lastSyncedValue == sha512(localRecord[field]);
+        isRemoteSame = lastSyncedValue == sha512(remoteRecord[field]);
+      } else {
+        // Otherwise, if the field hasn't changed since the last sync, we know
+        // it's the same locally.
+        isLocalSame = true;
+        isRemoteSame = localRecord[field] == remoteRecord[field];
+      }
+
+      let value;
+      if (isLocalSame && isRemoteSame) {
+        // Local and remote are the same; doesn't matter which one we pick.
+        value = localRecord[field];
+      } else if (isLocalSame && !isRemoteSame) {
+        value = remoteRecord[field];
+      } else if (!isLocalSame && isRemoteSame) {
+        // We don't need to bump the change counter when taking the local
+        // change, because the counter must already be > 0 if we're attempting
+        // a three-way merge.
+        value = localRecord[field];
+      } else if (localRecord[field] == remoteRecord[field]) {
+        // Shared parent doesn't match either local or remote, but the values
+        // are identical, so there's no conflict.
+        value = localRecord[field];
+      } else {
+        // Both local and remote changed to different values. We'll need to fork
+        // the local record to resolve the conflict.
+        return null;
+      }
+
+      if (value != null) {
+        mergedRecord[field] = value;
+      }
+    }
+
+    return mergedRecord;
+  }
+
+  /**
+   * Replaces a local record with a remote or merged record, copying internal
+   * fields and Sync metadata.
+   *
+   * @param   {number} index
+   * @param   {Object} remoteRecord
+   * @param   {boolean} [options.keepSyncMetadata = false]
+   *          Should we copy Sync metadata? This is true if `remoteRecord` is a
+   *          merged record with local changes that we need to upload. Passing
+   *          `keepSyncMetadata` retains the record's change counter and
+   *          last synced fields, so that we don't clobber the local change if
+   *          the sync is interrupted after the record is merged, but before
+   *          it's uploaded.
+   */
+  _replaceRecordAt(index, remoteRecord, {keepSyncMetadata = false} = {}) {
+    let localRecord = this._store.data[this._collectionName][index];
+    let newRecord = this._clone(remoteRecord);
+
+    this._stripComputedFields(newRecord);
+
+    this._store.data[this._collectionName][index] = newRecord;
+
+    if (keepSyncMetadata) {
+      // It's safe to move the Sync metadata from the old record to the new
+      // record, since we always clone records when we return them, and we
+      // never hand out references to the metadata object via public methods.
+      newRecord._sync = localRecord._sync;
+    } else {
+      // As a side effect, `_getSyncMetaData` marks the record as syncing if the
+      // existing `localRecord` is a dupe of `remoteRecord`, and we're replacing
+      // local with remote.
+      let sync = this._getSyncMetaData(newRecord, true);
+      sync.changeCounter = 0;
+    }
+
+    if (!newRecord.timeCreated ||
+        localRecord.timeCreated < newRecord.timeCreated) {
+      newRecord.timeCreated = localRecord.timeCreated;
+    }
+
+    if (!newRecord.timeLastModified ||
+        localRecord.timeLastModified > newRecord.timeLastModified) {
+      newRecord.timeLastModified = localRecord.timeLastModified;
+    }
+
+    // Copy local-only fields from the existing local record.
+    for (let field of ["timeLastUsed", "timesUsed"]) {
+      if (localRecord[field] != null) {
+        newRecord[field] = localRecord[field];
+      }
+    }
+
+    this._computeFields(newRecord);
+  }
+
+  /**
+   * Clones a local record, giving the clone a new GUID and Sync metadata. The
+   * original record remains unchanged in storage.
+   *
+   * @param   {Object} localRecord
+   *          The local record.
+   * @returns {string}
+   *          A clone of the local record with a new GUID.
+   */
+  _forkLocalRecord(localRecord) {
+    let forkedLocalRecord = this._clone(localRecord);
+
+    this._stripComputedFields(forkedLocalRecord);
+
+    forkedLocalRecord.guid = this._generateGUID();
+    this._store.data[this._collectionName].push(forkedLocalRecord);
+
+    // Give the record fresh Sync metadata and bump its change counter as a
+    // side effect. This also excludes the forked record from de-duping on the
+    // next sync, if the current sync is interrupted before the record can be
+    // uploaded.
+    this._getSyncMetaData(forkedLocalRecord, true);
+
+    this._computeFields(forkedLocalRecord);
+
+    return forkedLocalRecord;
+  }
+
+  /**
+   * Reconciles an incoming remote record into the matching local record. This
+   * method is only used by Sync; other callers should use `merge`.
+   *
+   * @param   {Object} remoteRecord
+   *          The incoming record. `remoteRecord` must not be a tombstone, and
+   *          must have a matching local record with the same GUID. Use
+   *          `add` to insert remote records that don't exist locally, and
+   *          `remove` to apply remote tombstones.
+   * @returns {Object}
+   *          A `{forkedGUID}` tuple. `forkedGUID` is `null` if the merge
+   *          succeeded without conflicts, or a new GUID referencing the
+   *          existing locally modified record if the conflicts could not be
+   *          resolved.
+   */
+  reconcile(remoteRecord) {
+    this._ensureMatchingVersion(remoteRecord);
+    if (remoteRecord.deleted) {
+      throw new Error(`Can't reconcile tombstone ${remoteRecord.guid}`);
+    }
+
+    let localIndex = this._findIndexByGUID(remoteRecord.guid);
+    if (localIndex < 0) {
+      throw new Error(`Record ${remoteRecord.guid} not found`);
+    }
+
+    let localRecord = this._store.data[this._collectionName][localIndex];
+    let sync = this._getSyncMetaData(localRecord, true);
+
+    let forkedGUID = null;
+
+    if (sync.changeCounter === 0) {
+      // Local not modified. Replace local with remote.
+      this._replaceRecordAt(localIndex, remoteRecord, {
+        keepSyncMetadata: false,
+      });
+    } else {
+      let mergedRecord = this._mergeSyncedRecords(localRecord, remoteRecord);
+      if (mergedRecord) {
+        // Local and remote modified, but we were able to merge. Replace the
+        // local record with the merged record.
+        this._replaceRecordAt(localIndex, mergedRecord, {
+          keepSyncMetadata: true,
+        });
+      } else {
+        // Merge conflict. Fork the local record, then replace the original
+        // with the merged record.
+        let forkedLocalRecord = this._forkLocalRecord(localRecord);
+        forkedGUID = forkedLocalRecord.guid;
+        this._replaceRecordAt(localIndex, remoteRecord, {
+          keepSyncMetadata: false,
+        });
+      }
+    }
+
+    this._store.saveSoon();
+    Services.obs.notifyObservers({wrappedJSObject: {
+      sourceSync: true,
+    }}, "formautofill-storage-changed", "reconcile");
+
+    return {forkedGUID};
+  }
+
   _removeSyncedRecord(guid) {
     let index = this._findIndexByGUID(guid, {includeDeleted: true});
     if (index == -1) {
       // Removing a record we don't know about. It may have been synced and
       // removed by another device before we saw it. Store the tombstone in
       // case the server is later wiped and we need to reupload everything.
       let tombstone = {
         guid,
@@ -623,16 +916,21 @@ class AutofillRecords {
       }
       let recordFound = this._findByGUID(guid, {includeDeleted: true});
       if (!recordFound) {
         this.log.warn("No profile found to persist changes for guid " + guid);
         continue;
       }
       let sync = this._getSyncMetaData(recordFound, true);
       sync.changeCounter = Math.max(0, sync.changeCounter - counter);
+      if (sync.changeCounter === 0) {
+        // Clear the shared parent fields once we've uploaded all pending
+        // changes, since the server now matches what we have locally.
+        sync.lastSyncedFields = {};
+      }
     }
     this._store.saveSoon();
   }
 
   /**
    * Reset all sync metadata for all items.
    *
    * This is called when Sync is disconnected from this device. All sync
@@ -685,34 +983,104 @@ class AutofillRecords {
   // Used to get, and optionally create, sync metadata. Brand new records will
   // *not* have sync meta-data - it will be created when they are first
   // synced.
   _getSyncMetaData(record, forceCreate = false) {
     if (!record._sync && forceCreate) {
       // create default metadata and indicate we need to save.
       record._sync = {
         changeCounter: 1,
+        lastSyncedFields: {},
       };
       this._store.saveSoon();
     }
     return record._sync;
   }
 
   /**
+   * Finds a local record with matching common fields and a different GUID.
+   * Sync uses this method to find and update unsynced local records with
+   * fields that match incoming remote records. This avoids creating
+   * duplicate profiles with the same information.
+   *
+   * @param   {Object} record
+   *          The remote record.
+   * @returns {string|null}
+   *          The GUID of the matching local record, or `null` if no records
+   *          match.
+   */
+  findDuplicateGUID(record) {
+    if (!record.guid) {
+      throw new Error("Record missing GUID");
+    }
+    this._ensureMatchingVersion(record);
+    if (record.deleted) {
+      // Tombstones don't carry enough info to de-dupe, and we should have
+      // handled them separately when applying the record.
+      throw new Error("Tombstones can't have duplicates");
+    }
+    let records = this._store.data[this._collectionName];
+    for (let profile of records) {
+      if (profile.deleted) {
+        continue;
+      }
+      if (profile.guid == record.guid) {
+        throw new Error(`Record ${record.guid} already exists`);
+      }
+      if (this._getSyncMetaData(profile)) {
+        // This record has already been uploaded, so it can't be a dupe of
+        // another incoming item.
+        continue;
+      }
+      let keys = new Set(Object.keys(record));
+      for (let key of Object.keys(profile)) {
+        keys.add(key);
+      }
+      // Ignore internal and computed fields when matching records. Internal
+      // fields are synced, but almost certainly have different values than the
+      // local record, and we'll update them in `reconcile`. Computed fields
+      // aren't synced at all.
+      for (let field of INTERNAL_FIELDS) {
+        keys.delete(field);
+      }
+      for (let field of this.VALID_COMPUTED_FIELDS) {
+        keys.delete(field);
+      }
+      if (!keys.size) {
+        // This shouldn't ever happen; a valid record will always have fields
+        // that aren't computed or internal. Sync can't do anything about that,
+        // so we ignore the dubious local record instead of throwing.
+        continue;
+      }
+      let same = true;
+      for (let key of keys) {
+        // For now, we ensure that both (or neither) records have the field
+        // with matching values. This doesn't account for the version yet
+        // (bug 1377204).
+        same = key in profile == key in record && profile[key] == record[key];
+        if (!same) {
+          break;
+        }
+      }
+      if (same) {
+        return profile.guid;
+      }
+    }
+    return null;
+  }
+
+  /**
    * Internal helper functions.
    */
 
-  _clone(record, {rawData = false} = {}) {
-    let result = Object.assign({}, record);
-    if (rawData) {
-      return result;
-    }
-    for (let key of Object.keys(result)) {
-      if (key.startsWith("_")) {
-        delete result[key];
+  _clone(record) {
+    let result = {};
+    for (let key in record) {
+      if (!key.startsWith("_")) {
+        result[key] = record[key];
       }
     }
     return result;
   }
 
   _findByGUID(guid, {includeDeleted = false} = {}) {
     let found = this._findIndexByGUID(guid, {includeDeleted});
     return found < 0 ? undefined : this._store.data[this._collectionName][found];
@@ -755,16 +1123,22 @@ class AutofillRecords {
       }
       if (typeof record[key] !== "string" &&
           typeof record[key] !== "number") {
         throw new Error(`"${key}" contains invalid data type.`);
       }
     }
   }
 
+  // A test-only helper.
+  _nukeAllRecords() {
+    this._store.data[this._collectionName] = [];
+    // test-only, so there's no good reason to request a save!
+  }
+
   _stripComputedFields(record) {
     this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
   }
 
   // An interface to be inherited.
   _recordReadProcessor(record) {}
 
   // An interface to be inherited.
--- a/browser/extensions/formautofill/test/unit/head.js
+++ b/browser/extensions/formautofill/test/unit/head.js
@@ -1,52 +1,53 @@
 /**
  * Provides infrastructure for automated formautofill components tests.
  */
 
 /* exported getTempFile, loadFormAutofillContent, runHeuristicsTest, sinon,
- *          initProfileStorage
+ *          initProfileStorage, getSyncChangeCounter, objectMatches
  */
 
 "use strict";
 
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/ObjectUtils.jsm");
 Cu.import("resource://gre/modules/FormLikeFactory.jsm");
 Cu.import("resource://testing-common/MockDocument.jsm");
 Cu.import("resource://testing-common/TestUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                   "resource://gre/modules/DownloadPaths.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 
 do_get_profile();
 
 // ================================================
 // Load mocking/stubbing library, sinon
 // docs: http://sinonjs.org/releases/v2.3.2/
 Cu.import("resource://gre/modules/Timer.jsm");
-const {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
-const loader = new Loader.Loader({
+var {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
+var loader = new Loader.Loader({
   paths: {
     "": "resource://testing-common/",
   },
   globals: {
     setTimeout,
     setInterval,
     clearTimeout,
     clearInterval,
   },
 });
-const require = Loader.Require(loader, {id: ""});
-const sinon = require("sinon-2.3.2");
+var require = Loader.Require(loader, {id: ""});
+var sinon = require("sinon-2.3.2");
 // ================================================
 
 // Load our bootstrap extension manifest so we can access our chrome/resource URIs.
 const EXTENSION_ID = "formautofill@mozilla.org";
 let extensionDir = Services.dirsvc.get("GreD", Ci.nsIFile);
 extensionDir.append("browser");
 extensionDir.append("features");
 extensionDir.append(EXTENSION_ID);
@@ -146,16 +147,64 @@ function runHeuristicsTest(patterns, fix
           expectedField.elementWeakRef = field.elementWeakRef;
           Assert.deepEqual(field, expectedField);
         });
       });
     });
   });
 }
 
+/**
+ * Returns the Sync change counter for a profile storage record. Synced records
+ * store additional metadata for tracking changes and resolving merge conflicts.
+ * Deleting a synced record replaces the record with a tombstone.
+ *
+ * @param   {AutofillRecords} records
+ *          The `AutofillRecords` instance to query.
+ * @param   {string} guid
+ *          The GUID of the record or tombstone.
+ * @returns {number}
+ *          The change counter, or -1 if the record doesn't exist or hasn't
+ *          been synced yet.
+ */
+function getSyncChangeCounter(records, guid) {
+  let record = records._findByGUID(guid, {includeDeleted: true});
+  if (!record) {
+    return -1;
+  }
+  let sync = records._getSyncMetaData(record);
+  if (!sync) {
+    return -1;
+  }
+  return sync.changeCounter;
+}
+
+/**
+ * Performs a partial deep equality check to determine if an object contains
+ * the given fields.
+ *
+ * @param   {Object} object
+ *          The object to check. Unlike `ObjectUtils.deepEqual`, properties in
+ *          `object` that are not in `fields` will be ignored.
+ * @param   {Object} fields
+ *          The fields to match.
+ * @returns {boolean}
+ *          Does `object` contain `fields` with matching values?
+ */
+function objectMatches(object, fields) {
+  let actual = {};
+  for (let key in fields) {
+    if (!object.hasOwnProperty(key)) {
+      return false;
+    }
+    actual[key] = object[key];
+  }
+  return ObjectUtils.deepEqual(actual, fields);
+}
+
 add_task(async function head_initialize() {
   Services.prefs.setBoolPref("extensions.formautofill.experimental", true);
   Services.prefs.setBoolPref("extensions.formautofill.heuristics.enabled", true);
   Services.prefs.setBoolPref("dom.forms.autocomplete.experimental", true);
 
   // Clean up after every test.
   do_register_cleanup(function head_cleanup() {
     Services.prefs.clearUserPref("extensions.formautofill.experimental");
--- a/browser/extensions/formautofill/test/unit/test_addressRecords.js
+++ b/browser/extensions/formautofill/test/unit/test_addressRecords.js
@@ -149,23 +149,16 @@ add_task(async function test_getAll() {
   addresses = profileStorage.addresses.getAll({rawData: true});
   do_check_eq(addresses[0].name, undefined);
   do_check_eq(addresses[0]["address-line1"], undefined);
   do_check_eq(addresses[0]["address-line2"], undefined);
 
   // Modifying output shouldn't affect the storage.
   addresses[0].organization = "test";
   do_check_record_matches(profileStorage.addresses.getAll()[0], TEST_ADDRESS_1);
-
-  // Test with rawData.
-  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which is what we are checking.
-  addresses = profileStorage.addresses.getAll({rawData: true});
-  Assert.ok(addresses[0]._sync && addresses[1]._sync);
-  Assert.equal(addresses[0]._sync.changeCounter, 1);
-  Assert.equal(addresses[1]._sync.changeCounter, 1);
 });
 
 add_task(async function test_get() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
   let addresses = profileStorage.addresses.getAll();
   let guid = addresses[0].guid;
@@ -179,21 +172,16 @@ add_task(async function test_get() {
   do_check_eq(address["address-line1"], undefined);
   do_check_eq(address["address-line2"], undefined);
 
   // Modifying output shouldn't affect the storage.
   address.organization = "test";
   do_check_record_matches(profileStorage.addresses.get(guid), TEST_ADDRESS_1);
 
   do_check_eq(profileStorage.addresses.get("INVALID_GUID"), null);
-
-  // rawData should include _sync, which should have a value of 1
-  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which is what we are checking.
-  let {_sync} = profileStorage.addresses.get(guid, {rawData: true});
-  do_check_eq(_sync.changeCounter, 1);
 });
 
 add_task(async function test_getByFilter() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
   let filter = {info: {fieldName: "street-address"}, searchString: "Some"};
   let addresses = profileStorage.addresses.getByFilter(filter);
@@ -266,17 +254,17 @@ add_task(async function test_update() {
 
   profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below.
 
   let address = profileStorage.addresses.get(guid, {rawData: true});
 
   do_check_eq(address.country, undefined);
   do_check_neq(address.timeLastModified, timeLastModified);
   do_check_record_matches(address, TEST_ADDRESS_3);
-  do_check_eq(address._sync.changeCounter, 1);
+  do_check_eq(getSyncChangeCounter(profileStorage.addresses, guid), 1);
 
   Assert.throws(
     () => profileStorage.addresses.update("INVALID_GUID", TEST_ADDRESS_3),
     /No matching record\./
   );
 
   Assert.throws(
     () => profileStorage.addresses.update(guid, TEST_ADDRESS_WITH_INVALID_FIELD),
@@ -288,27 +276,34 @@ add_task(async function test_notifyUsed(
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
   let addresses = profileStorage.addresses.getAll();
   let guid = addresses[1].guid;
   let timeLastUsed = addresses[1].timeLastUsed;
   let timesUsed = addresses[1].timesUsed;
 
+  profileStorage.addresses.pullSyncChanges(); // force sync metadata, which we check below.
+  let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+
   let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
                                           (subject, data) => data == "notifyUsed");
 
   profileStorage.addresses.notifyUsed(guid);
   await onChanged;
 
   let address = profileStorage.addresses.get(guid);
 
   do_check_eq(address.timesUsed, timesUsed + 1);
   do_check_neq(address.timeLastUsed, timeLastUsed);
 
+  // Using a record should not bump its change counter.
+  do_check_eq(getSyncChangeCounter(profileStorage.addresses, guid),
+    changeCounter);
+
   Assert.throws(() => profileStorage.addresses.notifyUsed("INVALID_GUID"),
     /No matching record\./);
 });
 
 add_task(async function test_remove() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_reconcile.js
@@ -0,0 +1,573 @@
+"use strict";
+
+const TEST_STORE_FILE_NAME = "test-profile.json";
+
+// NOTE: a guide to reading these test-cases:
+// parent: What the local record looked like the last time we wrote the
+//         record to the Sync server.
+// local:  What the local record looks like now. IOW, the differences between
+//         'parent' and 'local' are changes recently made which we wish to sync.
+// remote: An incoming record we need to apply (ie, a record that was possibly
+//         changed on a remote device)
+//
+// To further help understanding this, a few of the testcases are annotated.
+const RECONCILE_TESTCASES = [
+  {
+    description: "Local change",
+    parent: {
+      // So when we last wrote the record to the server, it had these values.
+      "guid": "2bbd2d8fbc6b",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      // The current local record - by comparing against parent we can see that
+      // only the given-name has changed locally.
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      // This is the incoming record. It has the same values as "parent", so
+      // we can deduce the record hasn't actually been changed remotely so we
+      // can safely ignore the incoming record and write our local changes.
+      "guid": "2bbd2d8fbc6b",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "2bbd2d8fbc6b",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "Remote change",
+    parent: {
+      "guid": "e3680e9f890d",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "e3680e9f890d",
+      "version": 1,
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "e3680e9f890d",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "New local field",
+    parent: {
+      "guid": "0cba738b1be0",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    }],
+    remote: {
+      "guid": "0cba738b1be0",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "0cba738b1be0",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+  },
+  {
+    description: "New remote field",
+    parent: {
+      "guid": "be3ef97f8285",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "be3ef97f8285",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+    reconciled: {
+      "guid": "be3ef97f8285",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+  },
+  {
+    description: "Deleted field locally",
+    parent: {
+      "guid": "9627322248ec",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "9627322248ec",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+    reconciled: {
+      "guid": "9627322248ec",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "Deleted field remotely",
+    parent: {
+      "guid": "7d7509f3eeb2",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    }],
+    remote: {
+      "guid": "7d7509f3eeb2",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "7d7509f3eeb2",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "Local and remote changes to unrelated fields",
+    parent: {
+      // The last time we wrote this to the server, country was NZ.
+      "guid": "e087a06dfc57",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    },
+    local: [{
+      // The current local record - so locally we've changed given-name to Skip.
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "country": "NZ",
+    }],
+    remote: {
+      // Remotely, we've changed the country to AU.
+      "guid": "e087a06dfc57",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "AU",
+    },
+    reconciled: {
+      "guid": "e087a06dfc57",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "country": "AU",
+    },
+  },
+  {
+    description: "Multiple local changes",
+    parent: {
+      "guid": "340a078c596f",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+    },
+    local: [{
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    }, {
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "organization": "Mozilla",
+    }],
+    remote: {
+      "guid": "340a078c596f",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "tel": "123456",
+      "country": "AU",
+    },
+    reconciled: {
+      "guid": "340a078c596f",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "organization": "Mozilla",
+      "country": "AU",
+    },
+  },
+  {
+    // Local and remote diverged from the shared parent, but the values are the
+    // same, so we shouldn't fork.
+    description: "Same change to local and remote",
+    parent: {
+      "guid": "0b3a72a1bea2",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "0b3a72a1bea2",
+      "version": 1,
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "0b3a72a1bea2",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "Conflicting changes to single field",
+    parent: {
+      // This is what we last wrote to the sync server.
+      "guid": "62068784d089",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    local: [{
+      // The current version of the local record - the given-name has changed locally.
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      // An incoming record has a different given-name than any of the above!
+      "guid": "62068784d089",
+      "version": 1,
+      "given-name": "Kip",
+      "family-name": "Hammond",
+    },
+    forked: {
+      // So we've forked the local record to a new GUID (and the next sync is
+      // going to write this as a new record)
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      // And we've updated the local version of the record to be the remote version.
+      guid: "62068784d089",
+      "given-name": "Kip",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    description: "Conflicting changes to multiple fields",
+    parent: {
+      "guid": "244dbb692e94",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    },
+    local: [{
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "country": "AU",
+    }],
+    remote: {
+      "guid": "244dbb692e94",
+      "version": 1,
+      "given-name": "Kip",
+      "family-name": "Hammond",
+      "country": "CA",
+    },
+    forked: {
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "country": "AU",
+    },
+    reconciled: {
+      "guid": "244dbb692e94",
+      "given-name": "Kip",
+      "family-name": "Hammond",
+      "country": "CA",
+    },
+  },
+  {
+    description: "Field deleted locally, changed remotely",
+    parent: {
+      "guid": "6fc45e03d19a",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "AU",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "6fc45e03d19a",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    },
+    forked: {
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    reconciled: {
+      "guid": "6fc45e03d19a",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    },
+  },
+  {
+    description: "Field changed locally, deleted remotely",
+    parent: {
+      "guid": "fff9fa27fa18",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "AU",
+    },
+    local: [{
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    }],
+    remote: {
+      "guid": "fff9fa27fa18",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+    forked: {
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "country": "NZ",
+    },
+    reconciled: {
+      "guid": "fff9fa27fa18",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    },
+  },
+  {
+    // Created, last modified should be synced; last used and times used should
+    // be local. Remote created time older than local, remote modified time
+    // newer than local.
+    description: "Created, last modified time reconciliation without local changes",
+    parent: {
+      "guid": "5113f329c42f",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "timeCreated": 1234,
+      "timeLastModified": 5678,
+      "timeLastUsed": 5678,
+      "timesUsed": 6,
+    },
+    local: [],
+    remote: {
+      "guid": "5113f329c42f",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "timeCreated": 1200,
+      "timeLastModified": 5700,
+      "timeLastUsed": 5700,
+      "timesUsed": 3,
+    },
+    reconciled: {
+      "guid": "5113f329c42f",
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "timeCreated": 1200,
+      "timeLastModified": 5700,
+      "timeLastUsed": 5678,
+      "timesUsed": 6,
+    },
+  },
+  {
+    // Local changes, remote created time newer than local, remote modified time
+    // older than local.
+    description: "Created, last modified time reconciliation with local changes",
+    parent: {
+      "guid": "791e5608b80a",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "timeCreated": 1234,
+      "timeLastModified": 5678,
+      "timeLastUsed": 5678,
+      "timesUsed": 6,
+    },
+    local: [{
+      "given-name": "Skip",
+      "family-name": "Hammond",
+    }],
+    remote: {
+      "guid": "791e5608b80a",
+      "version": 1,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "timeCreated": 1300,
+      "timeLastModified": 5000,
+      "timeLastUsed": 5000,
+      "timesUsed": 3,
+    },
+    reconciled: {
+      "guid": "791e5608b80a",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "timeCreated": 1234,
+      "timeLastUsed": 5678,
+      "timesUsed": 6,
+    },
+  },
+];
+
+add_task(async function test_reconcile_unknown_version() {
+  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
+
+  // Cross-version reconciliation isn't supported yet. See bug 1377204.
+  throws(() => {
+    profileStorage.addresses.reconcile({
+      "guid": "31d83d2725ec",
+      "version": 2,
+      "given-name": "Mark",
+      "family-name": "Hammond",
+    });
+  }, /Got unknown record version/);
+});
+
+add_task(async function test_reconcile_idempotent() {
+  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
+
+  let guid = "de1ba7b094fe";
+  profileStorage.addresses.add({
+    guid,
+    version: 1,
+    "given-name": "Mark",
+    "family-name": "Hammond",
+  }, {sourceSync: true});
+  profileStorage.addresses.update(guid, {
+    "given-name": "Skip",
+    "family-name": "Hammond",
+    "organization": "Mozilla",
+  });
+
+  let remote = {
+    guid,
+    "version": 1,
+    "given-name": "Mark",
+    "family-name": "Hammond",
+    "tel": "123456",
+  };
+
+  {
+    let {forkedGUID} = profileStorage.addresses.reconcile(remote);
+    let updatedRecord = profileStorage.addresses.get(guid, {
+      rawData: true,
+    });
+
+    ok(!forkedGUID, "First merge should not fork record");
+    ok(objectMatches(updatedRecord, {
+      "guid": "de1ba7b094fe",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "organization": "Mozilla",
+      "tel": "123456",
+    }), "First merge should merge local and remote changes");
+  }
+
+  {
+    let {forkedGUID} = profileStorage.addresses.reconcile(remote);
+    let updatedRecord = profileStorage.addresses.get(guid, {
+      rawData: true,
+    });
+
+    ok(!forkedGUID, "Second merge should not fork record");
+    ok(objectMatches(updatedRecord, {
+      "guid": "de1ba7b094fe",
+      "given-name": "Skip",
+      "family-name": "Hammond",
+      "organization": "Mozilla",
+      "tel": "123456",
+    }), "Second merge should not change record");
+  }
+});
+
+add_task(async function test_reconcile_three_way_merge() {
+  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
+
+  for (let test of RECONCILE_TESTCASES) {
+    do_print(test.description);
+
+    profileStorage.addresses.add(test.parent, {sourceSync: true});
+
+    for (let updatedRecord of test.local) {
+      profileStorage.addresses.update(test.parent.guid, updatedRecord);
+    }
+
+    let localRecord = profileStorage.addresses.get(test.parent.guid, {
+      rawData: true,
+    });
+
+    let {forkedGUID} = profileStorage.addresses.reconcile(test.remote);
+    let reconciledRecord = profileStorage.addresses.get(test.parent.guid, {
+      rawData: true,
+    });
+    if (forkedGUID) {
+      let forkedRecord = profileStorage.addresses.get(forkedGUID, {
+        rawData: true,
+      });
+
+      notEqual(forkedRecord.guid, reconciledRecord.guid);
+      equal(forkedRecord.timeLastModified, localRecord.timeLastModified);
+      ok(objectMatches(forkedRecord, test.forked),
+        `${test.description} should fork record`);
+    } else {
+      ok(!test.forked, `${test.description} should not fork record`);
+    }
+
+    ok(objectMatches(reconciledRecord, test.reconciled));
+  }
+});
--- a/browser/extensions/formautofill/test/unit/test_storage_syncfields.js
+++ b/browser/extensions/formautofill/test/unit/test_storage_syncfields.js
@@ -40,216 +40,214 @@ function findGUID(storage, guid, options
   equal(records.length, 1);
   return records[0];
 }
 
 add_task(async function test_changeCounter() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1]);
 
-  let address = profileStorage.addresses.getAll({rawData: true})[0];
+  let [address] = profileStorage.addresses.getAll();
   // new records don't get the sync metadata.
-  ok(!address._sync);
+  equal(getSyncChangeCounter(profileStorage.addresses, address.guid), -1);
   // But we can force one.
   profileStorage.addresses.pullSyncChanges();
-  address = profileStorage.addresses.getAll({rawData: true})[0];
-  equal(address._sync.changeCounter, 1);
+  equal(getSyncChangeCounter(profileStorage.addresses, address.guid), 1);
 });
 
 add_task(async function test_pushChanges() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
   profileStorage.addresses.pullSyncChanges(); // force sync metadata for all items
 
-  let [, address] = profileStorage.addresses.getAll({rawData: true});
+  let [, address] = profileStorage.addresses.getAll();
   let guid = address.guid;
+  let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
 
   // Pretend we're doing a sync now, and an update occured mid-sync.
   let changes = {
     [guid]: {
       profile: address,
-      counter: address._sync.changeCounter,
+      counter: changeCounter,
       modified: address.timeLastModified,
       synced: true,
     },
   };
 
   let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
                                           (subject, data) => data == "update");
   profileStorage.addresses.update(guid, TEST_ADDRESS_3);
   await onChanged;
 
-  address = profileStorage.addresses.get(guid, {rawData: true});
-  do_check_eq(address._sync.changeCounter, 2);
+  changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+  do_check_eq(changeCounter, 2);
 
   profileStorage.addresses.pushSyncChanges(changes);
-  address = profileStorage.addresses.get(guid, {rawData: true});
+  address = profileStorage.addresses.get(guid);
+  changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
 
   // Counter should still be 1, since our sync didn't record the mid-sync change
-  do_check_eq(address._sync.changeCounter, 1, "Counter shouldn't be zero because it didn't record update");
+  do_check_eq(changeCounter, 1, "Counter shouldn't be zero because it didn't record update");
 
   // now, push a new set of changes, which should make the changeCounter 0
   profileStorage.addresses.pushSyncChanges({
     [guid]: {
       profile: address,
-      counter: address._sync.changeCounter,
+      counter: changeCounter,
       modified: address.timeLastModified,
       synced: true,
     },
   });
-  address = profileStorage.addresses.get(guid, {rawData: true});
 
-  do_check_eq(address._sync.changeCounter, 0);
+  changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+  do_check_eq(changeCounter, 0);
 });
 
 async function checkingSyncChange(action, callback) {
   let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
                                           (subject, data) => data == action);
   await callback();
   let [subject] = await onChanged;
   ok(subject.wrappedJSObject.sourceSync, "change notification should have source sync");
 }
 
 add_task(async function test_add_sourceSync() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   // Hardcode a guid so that we don't need to generate a dynamic regex
   let guid = "aaaaaaaaaaaa";
-  let testAddr = Object.assign({guid}, TEST_ADDRESS_1);
+  let testAddr = Object.assign({guid, version: 1}, TEST_ADDRESS_1);
 
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(testAddr, {sourceSync: true}));
 
-  let added = profileStorage.addresses.get(guid, {rawData: true});
-  equal(added._sync.changeCounter, 0);
+  let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+  equal(changeCounter, 0);
 
   Assert.throws(() =>
     profileStorage.addresses.add({guid, deleted: true}, {sourceSync: true}),
     /Record aaaaaaaaaaaa already exists/
   );
 });
 
 add_task(async function test_add_tombstone_sourceSync() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   let guid = profileStorage.addresses._generateGUID();
   let testAddr = {guid, deleted: true};
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(testAddr, {sourceSync: true}));
 
   let added = findGUID(profileStorage.addresses, guid,
-    {rawData: true, includeDeleted: true});
+    {includeDeleted: true});
   ok(added);
-  equal(added._sync.changeCounter, 0);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 0);
   ok(added.deleted);
 
   // Adding same record again shouldn't throw (or change anything)
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(testAddr, {sourceSync: true}));
 
   added = findGUID(profileStorage.addresses, guid,
-    {rawData: true, includeDeleted: true});
-  equal(added._sync.changeCounter, 0);
+    {includeDeleted: true});
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 0);
   ok(added.deleted);
 });
 
 add_task(async function test_add_resurrects_tombstones() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   let guid = profileStorage.addresses._generateGUID();
 
   // Add a tombstone.
   profileStorage.addresses.add({guid, deleted: true});
 
   // You can't re-add an item with an explicit GUID.
-  let resurrected = Object.assign({}, TEST_ADDRESS_1, {guid});
+  let resurrected = Object.assign({}, TEST_ADDRESS_1, {guid, version: 1});
   Assert.throws(() => profileStorage.addresses.add(resurrected),
-                /"guid" is not a valid field/);
+                /"(guid|version)" is not a valid field/);
 
   // But Sync can!
   let guid3 = profileStorage.addresses.add(resurrected, {sourceSync: true});
   equal(guid, guid3);
 
   let got = profileStorage.addresses.get(guid);
   equal(got["given-name"], TEST_ADDRESS_1["given-name"]);
 });
 
 add_task(async function test_remove_sourceSync_localChanges() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, [TEST_ADDRESS_1]);
   profileStorage.addresses.pullSyncChanges(); // force sync metadata
 
-  let [{guid, _sync}] = profileStorage.addresses.getAll({rawData: true});
+  let [{guid}] = profileStorage.addresses.getAll();
 
-  equal(_sync.changeCounter, 1);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);
   // try and remove a record stored locally with local changes
   await checkingSyncChange("remove", () =>
     profileStorage.addresses.remove(guid, {sourceSync: true}));
 
-  let record = profileStorage.addresses.get(guid, {
-    rawData: true,
-  });
+  let record = profileStorage.addresses.get(guid);
   ok(record);
-  equal(record._sync.changeCounter, 1);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 1);
 });
 
 add_task(async function test_remove_sourceSync_unknown() {
   // remove a record not stored locally
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   let guid = profileStorage.addresses._generateGUID();
   await checkingSyncChange("remove", () =>
     profileStorage.addresses.remove(guid, {sourceSync: true}));
 
   let tombstone = findGUID(profileStorage.addresses, guid, {
-    rawData: true,
     includeDeleted: true,
   });
   ok(tombstone.deleted);
-  equal(tombstone._sync.changeCounter, 0);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 0);
 });
 
 add_task(async function test_remove_sourceSync_unchanged() {
   // Remove a local record without a change counter.
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   let guid = profileStorage.addresses._generateGUID();
-  let addr = Object.assign({guid}, TEST_ADDRESS_1);
+  let addr = Object.assign({guid, version: 1}, TEST_ADDRESS_1);
   // add a record with sourceSync to guarantee changeCounter == 0
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(addr, {sourceSync: true}));
 
-  let added = profileStorage.addresses.get(guid, {rawData: true});
-  equal(added._sync.changeCounter, 0);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 0);
 
   await checkingSyncChange("remove", () =>
     profileStorage.addresses.remove(guid, {sourceSync: true}));
 
   let tombstone = findGUID(profileStorage.addresses, guid, {
-    rawData: true,
     includeDeleted: true,
   });
   ok(tombstone.deleted);
-  equal(tombstone._sync.changeCounter, 0);
+  equal(getSyncChangeCounter(profileStorage.addresses, guid), 0);
 });
 
 add_task(async function test_pullSyncChanges() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
   let startAddresses = profileStorage.addresses.getAll();
   equal(startAddresses.length, 2);
   // All should start without sync metadata
-  for (let addr of profileStorage.addresses._store.data.addresses) {
-    ok(!addr._sync);
+  for (let {guid} of profileStorage.addresses._store.data.addresses) {
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+    equal(changeCounter, -1);
   }
   profileStorage.addresses.pullSyncChanges(); // force sync metadata
 
   let addedDirectGUID = profileStorage.addresses._generateGUID();
-  let testAddr = Object.assign({guid: addedDirectGUID}, TEST_ADDRESS_3);
+  let testAddr = Object.assign({guid: addedDirectGUID, version: 1},
+                               TEST_ADDRESS_1, TEST_ADDRESS_3);
 
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(testAddr, {sourceSync: true}));
 
   let tombstoneGUID = profileStorage.addresses._generateGUID();
   await checkingSyncChange("add", () =>
     profileStorage.addresses.add(
       {guid: tombstoneGUID, deleted: true},
@@ -258,17 +256,16 @@ add_task(async function test_pullSyncCha
   let onChanged = TestUtils.topicObserved("formautofill-storage-changed",
                                           (subject, data) => data == "remove");
 
   profileStorage.addresses.remove(startAddresses[0].guid);
   await onChanged;
 
   let addresses = profileStorage.addresses.getAll({
     includeDeleted: true,
-    rawData: true,
   });
 
   // Should contain changes with a change counter
   let changes = profileStorage.addresses.pullSyncChanges();
   equal(Object.keys(changes).length, 2);
 
   ok(changes[startAddresses[0].guid].profile.deleted);
   equal(changes[startAddresses[0].guid].counter, 2);
@@ -280,52 +277,54 @@ add_task(async function test_pullSyncCha
   ok(!changes[addedDirectGUID], "Missing because it was added with sourceSync");
 
   for (let address of addresses) {
     let change = changes[address.guid];
     if (!change) {
       continue;
     }
     equal(change.profile.guid, address.guid);
-    equal(change.counter, address._sync.changeCounter);
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses,
+      change.profile.guid);
+    equal(change.counter, changeCounter);
     ok(!change.synced);
   }
 });
 
 add_task(async function test_pullPushChanges() {
   // round-trip changes between pull and push
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
   let psa = profileStorage.addresses;
 
   let guid1 = psa.add(TEST_ADDRESS_1);
   let guid2 = psa.add(TEST_ADDRESS_2);
   let guid3 = psa.add(TEST_ADDRESS_3);
 
   let changes = psa.pullSyncChanges();
 
-  equal(psa.get(guid1, {rawData: true})._sync.changeCounter, 1);
-  equal(psa.get(guid2, {rawData: true})._sync.changeCounter, 1);
-  equal(psa.get(guid3, {rawData: true})._sync.changeCounter, 1);
+  equal(getSyncChangeCounter(psa, guid1), 1);
+  equal(getSyncChangeCounter(psa, guid2), 1);
+  equal(getSyncChangeCounter(psa, guid3), 1);
 
   // between the pull and the push we change the second.
   psa.update(guid2, Object.assign({}, TEST_ADDRESS_2, {country: "AU"}));
-  equal(psa.get(guid2, {rawData: true})._sync.changeCounter, 2);
+  equal(getSyncChangeCounter(psa, guid2), 2);
   // and update the changeset to indicated we did update the first 2, but failed
   // to update the 3rd for some reason.
   changes[guid1].synced = true;
   changes[guid2].synced = true;
 
   psa.pushSyncChanges(changes);
 
   // first was synced correctly.
-  equal(psa.get(guid1, {rawData: true})._sync.changeCounter, 0);
+  equal(getSyncChangeCounter(psa, guid1), 0);
   // second was synced correctly, but it had a change while syncing.
-  equal(psa.get(guid2, {rawData: true})._sync.changeCounter, 1);
+  equal(getSyncChangeCounter(psa, guid2), 1);
   // 3rd wasn't marked as having synced.
-  equal(psa.get(guid3, {rawData: true})._sync.changeCounter, 1);
+  equal(getSyncChangeCounter(psa, guid3), 1);
 });
 
 add_task(async function test_changeGUID() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME, []);
 
   let newguid = () => profileStorage.addresses._generateGUID();
 
   let guid_synced = profileStorage.addresses.add(TEST_ADDRESS_1);
@@ -358,30 +357,62 @@ add_task(async function test_changeGUID(
   equal(profileStorage.addresses.getAll({includeDeleted: true}).length, 3);
 
   ok(profileStorage.addresses.get(guid_synced), "synced item still exists.");
   ok(profileStorage.addresses.get(guid_u2), "guid we didn't touch still exists.");
   ok(profileStorage.addresses.get(targetguid), "target guid exists.");
   ok(!profileStorage.addresses.get(guid_u1), "old guid no longer exists.");
 });
 
+add_task(async function test_findDuplicateGUID() {
+  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
+                                                [TEST_ADDRESS_1]);
+
+  let [record] = profileStorage.addresses.getAll({rawData: true});
+  Assert.throws(() => profileStorage.addresses.findDuplicateGUID(record),
+                /Record \w+ already exists/,
+                "Should throw if the GUID already exists");
+
+  // Add a malformed record, passing `sourceSync` to work around the record
+  // normalization logic that would prevent this.
+  let timeLastModified = Date.now();
+  let timeCreated = timeLastModified - 60 * 1000;
+
+  profileStorage.addresses.add({
+    guid: profileStorage.addresses._generateGUID(),
+    version: 1,
+    timeCreated,
+    timeLastModified,
+  }, {sourceSync: true});
+
+  strictEqual(profileStorage.addresses.findDuplicateGUID({
+    guid: profileStorage.addresses._generateGUID(),
+    version: 1,
+    timeCreated,
+    timeLastModified,
+  }), null, "Should ignore internal fields and malformed records");
+});
+
 add_task(async function test_reset() {
   let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME,
                                                 [TEST_ADDRESS_1, TEST_ADDRESS_2]);
 
-  let addresses = profileStorage.addresses.getAll({rawData: true});
+  let addresses = profileStorage.addresses.getAll();
   // All should start without sync metadata
-  for (let addr of addresses) {
-    ok(!addr._sync);
+  for (let {guid} of addresses) {
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+    equal(changeCounter, -1);
   }
   // pullSyncChanges should create the metadata.
   profileStorage.addresses.pullSyncChanges();
-  addresses = profileStorage.addresses.getAll({rawData: true});
-  for (let addr of addresses) {
-    ok(addr._sync);
+  addresses = profileStorage.addresses.getAll();
+  for (let {guid} of addresses) {
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+    equal(changeCounter, 1);
   }
   // and resetSync should wipe it.
   profileStorage.addresses.resetSync();
-  addresses = profileStorage.addresses.getAll({rawData: true});
-  for (let addr of addresses) {
-    ok(!addr._sync);
+  addresses = profileStorage.addresses.getAll();
+  for (let {guid} of addresses) {
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses, guid);
+    equal(changeCounter, -1);
   }
 });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/test/unit/test_sync.js
@@ -0,0 +1,699 @@
+/**
+ * Tests sync functionality.
+ */
+
+/* import-globals-from ../../../../../services/sync/tests/unit/head_appinfo.js */
+/* import-globals-from ../../../../../services/common/tests/unit/head_helpers.js */
+/* import-globals-from ../../../../../services/sync/tests/unit/head_helpers.js */
+/* import-globals-from ../../../../../services/sync/tests/unit/head_http_server.js */
+
+"use strict";
+
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+
+let {sanitizeStorageObject, AutofillRecord, AddressesEngine} =
+  Cu.import("resource://formautofill/FormAutofillSync.jsm", {});
+
+
+Services.prefs.setCharPref("extensions.formautofill.loglevel", "Trace");
+initTestLogging("Trace");
+
+const TEST_STORE_FILE_NAME = "test-profile.json";
+
+const TEST_PROFILE_1 = {
+  "given-name": "Timothy",
+  "additional-name": "John",
+  "family-name": "Berners-Lee",
+  organization: "World Wide Web Consortium",
+  "street-address": "32 Vassar Street\nMIT Room 32-G524",
+  "address-level2": "Cambridge",
+  "address-level1": "MA",
+  "postal-code": "02139",
+  country: "US",
+  tel: "+16172535702",
+  email: "timbl@w3.org",
+};
+
+const TEST_PROFILE_2 = {
+  "street-address": "Some Address",
+  country: "US",
+};
+
+function expectLocalProfiles(profileStorage, expected) {
+  let profiles = profileStorage.addresses.getAll({
+    rawData: true,
+    includeDeleted: true,
+  });
+  expected.sort((a, b) => a.guid.localeCompare(b.guid));
+  profiles.sort((a, b) => a.guid.localeCompare(b.guid));
+  try {
+    deepEqual(profiles.map(p => p.guid), expected.map(p => p.guid));
+    for (let i = 0; i < expected.length; i++) {
+      let thisExpected = expected[i];
+      let thisGot = profiles[i];
+      // always check "deleted".
+      equal(thisExpected.deleted, thisGot.deleted);
+      ok(objectMatches(thisGot, thisExpected));
+    }
+  } catch (ex) {
+    do_print("Comparing expected profiles:");
+    do_print(JSON.stringify(expected, undefined, 2));
+    do_print("against actual profiles:");
+    do_print(JSON.stringify(profiles, undefined, 2));
+    throw ex;
+  }
+}
+
+async function setup() {
+  let profileStorage = await initProfileStorage(TEST_STORE_FILE_NAME);
+  // should always start with no profiles.
+  Assert.equal(profileStorage.addresses.getAll({includeDeleted: true}).length, 0);
+
+  Services.prefs.setCharPref("services.sync.log.logger.engine.addresses", "Trace");
+  let engine = new AddressesEngine(Service);
+  await engine.initialize();
+  // Avoid accidental automatic sync due to our own changes
+  Service.scheduler.syncThreshold = 10000000;
+  let server = serverForUsers({"foo": "password"}, {
+    meta: {global: {engines: {addresses: {version: engine.version, syncID: engine.syncID}}}},
+    addresses: {},
+  });
+
+  Service.engineManager._engines.addresses = engine;
+  engine.enabled = true;
+  engine._store._storage = profileStorage.addresses;
+
+  generateNewKeys(Service.collectionKeys);
+
+  await SyncTestingInfrastructure(server);
+
+  let collection = server.user("foo").collection("addresses");
+
+  return {profileStorage, server, collection, engine};
+}
+
+async function cleanup(server) {
+  let promiseStartOver = promiseOneObserver("weave:service:start-over:finish");
+  await Service.startOver();
+  await promiseStartOver;
+  await promiseStopServer(server);
+}
+
+add_task(async function test_log_sanitization() {
+  let sanitized = sanitizeStorageObject(TEST_PROFILE_1);
+  // all strings have been mangled.
+  for (let key of Object.keys(TEST_PROFILE_1)) {
+    let val = TEST_PROFILE_1[key];
+    if (typeof val == "string") {
+      notEqual(sanitized[key], val);
+    }
+  }
+  // And check that stringifying a sync record is sanitized.
+  let record = new AutofillRecord("collection", "some-id");
+  record.entry = TEST_PROFILE_1;
+  let serialized = record.toString();
+  // None of the string values should appear in the output.
+  for (let key of Object.keys(TEST_PROFILE_1)) {
+    let val = TEST_PROFILE_1[key];
+    if (typeof val == "string") {
+      ok(!serialized.includes(val), `"${val}" shouldn't be in: ${serialized}`);
+    }
+  }
+});
+
+add_task(async function test_outgoing() {
+  let {profileStorage, server, collection, engine} = await setup();
+  try {
+    equal(engine._tracker.score, 0);
+    let existingGUID = profileStorage.addresses.add(TEST_PROFILE_1);
+    // And a deleted item.
+    let deletedGUID = profileStorage.addresses._generateGUID();
+    profileStorage.addresses.add({guid: deletedGUID, deleted: true});
+
+    expectLocalProfiles(profileStorage, [
+      {
+        guid: existingGUID,
+      },
+      {
+        guid: deletedGUID,
+        deleted: true,
+      },
+    ]);
+
+    // The tracker should have a score recorded for the 2 additions we had.
+    equal(engine._tracker.score, SCORE_INCREMENT_XLARGE * 2);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    Assert.equal(collection.count(), 2);
+    Assert.ok(collection.wbo(existingGUID));
+    Assert.ok(collection.wbo(deletedGUID));
+
+    expectLocalProfiles(profileStorage, [
+      {
+        guid: existingGUID,
+      },
+      {
+        guid: deletedGUID,
+        deleted: true,
+      },
+    ]);
+
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, existingGUID), 0);
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedGUID), 0);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_incoming_new() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let profileID = Utils.makeGUID();
+    let deletedID = Utils.makeGUID();
+
+    server.insertWBO("foo", "addresses", new ServerWBO(profileID, encryptPayload({
+      id: profileID,
+      entry: Object.assign({
+        version: 1,
+      }, TEST_PROFILE_1),
+    }), Date.now() / 1000));
+    server.insertWBO("foo", "addresses", new ServerWBO(deletedID, encryptPayload({
+      id: deletedID,
+      deleted: true,
+    }), Date.now() / 1000));
+
+    // The tracker should start with no score.
+    equal(engine._tracker.score, 0);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    expectLocalProfiles(profileStorage, [
+      {
+        guid: profileID,
+      }, {
+        guid: deletedID,
+        deleted: true,
+      },
+    ]);
+
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, profileID), 0);
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, deletedID), 0);
+
+    // The sync applied new records - ensure our tracker knew it came from
+    // sync and didn't bump the score.
+    equal(engine._tracker.score, 0);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_incoming_existing() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid1 = profileStorage.addresses.add(TEST_PROFILE_1);
+    let guid2 = profileStorage.addresses.add(TEST_PROFILE_2);
+
+    // an initial sync so we don't think they are locally modified.
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // now server records that modify the existing items.
+    let modifiedEntry1 = Object.assign({}, TEST_PROFILE_1, {
+      "version": 1,
+      "given-name": "NewName",
+    });
+
+    server.insertWBO("foo", "addresses", new ServerWBO(guid1, encryptPayload({
+      id: guid1,
+      entry: modifiedEntry1,
+    }), engine.lastSync + 10));
+    server.insertWBO("foo", "addresses", new ServerWBO(guid2, encryptPayload({
+      id: guid2,
+      deleted: true,
+    }), engine.lastSync + 10));
+
+    await engine.sync();
+
+    expectLocalProfiles(profileStorage, [
+      Object.assign({}, modifiedEntry1, {guid: guid1}),
+      {guid: guid2, deleted: true},
+    ]);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_tombstones() {
+  let {profileStorage, server, collection, engine} = await setup();
+  try {
+    let existingGUID = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    Assert.equal(collection.count(), 1);
+    let payload = collection.payloads()[0];
+    equal(payload.id, existingGUID);
+    equal(payload.deleted, undefined);
+
+    profileStorage.addresses.remove(existingGUID);
+    await engine.sync();
+
+    // should still exist, but now be a tombstone.
+    Assert.equal(collection.count(), 1);
+    payload = collection.payloads()[0];
+    equal(payload.id, existingGUID);
+    equal(payload.deleted, true);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_applyIncoming_both_deleted() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Delete synced record locally.
+    profileStorage.addresses.remove(guid);
+
+    // Delete same record remotely.
+    let collection = server.user("foo").collection("addresses");
+    collection.insert(guid, encryptPayload({
+      id: guid,
+      deleted: true,
+    }), engine.lastSync + 10);
+
+    await engine.sync();
+
+    ok(!profileStorage.addresses.get(guid),
+      "Should not return record for locally deleted item");
+
+    let localRecords = profileStorage.addresses.getAll({
+      includeDeleted: true,
+    });
+    equal(localRecords.length, 1, "Only tombstone should exist locally");
+
+    equal(collection.count(), 1,
+      "Only tombstone should exist on server");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_applyIncoming_nonexistent_tombstone() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid = profileStorage.addresses._generateGUID();
+    let collection = server.user("foo").collection("addresses");
+    collection.insert(guid, encryptPayload({
+      id: guid,
+      deleted: true,
+    }), Date.now() / 1000);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    ok(!profileStorage.addresses.get(guid),
+      "Should not return record for uknown deleted item");
+    let localTombstone = profileStorage.addresses.getAll({
+      includeDeleted: true,
+    }).find(record => record.guid == guid);
+    ok(localTombstone, "Should store tombstone for unknown item");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_applyIncoming_incoming_deleted() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Delete the record remotely.
+    let collection = server.user("foo").collection("addresses");
+    collection.insert(guid, encryptPayload({
+      id: guid,
+      deleted: true,
+    }), engine.lastSync + 10);
+
+    await engine.sync();
+
+    ok(!profileStorage.addresses.get(guid), "Should delete unmodified item locally");
+
+    let localTombstone = profileStorage.addresses.getAll({
+      includeDeleted: true,
+    }).find(record => record.guid == guid);
+    ok(localTombstone, "Should keep local tombstone for remotely deleted item");
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, guid), 0,
+      "Local tombstone should be marked as syncing");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_applyIncoming_incoming_restored() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // Upload the record to the server.
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Removing a synced record should write a tombstone.
+    profileStorage.addresses.remove(guid);
+
+    // Modify the deleted record remotely.
+    let collection = server.user("foo").collection("addresses");
+    let serverPayload = JSON.parse(JSON.parse(collection.payload(guid)).ciphertext);
+    serverPayload.entry["street-address"] = "I moved!";
+    collection.insert(guid, encryptPayload(serverPayload), engine.lastSync + 10);
+
+    // Sync again.
+    await engine.sync();
+
+    // We should replace our tombstone with the server's version.
+    let localRecord = profileStorage.addresses.get(guid);
+    ok(objectMatches(localRecord, {
+      "given-name": "Timothy",
+      "family-name": "Berners-Lee",
+      "street-address": "I moved!",
+    }));
+
+    let maybeNewServerPayload = JSON.parse(JSON.parse(collection.payload(guid)).ciphertext);
+    deepEqual(maybeNewServerPayload, serverPayload, "Should not change record on server");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_applyIncoming_outgoing_restored() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // Upload the record to the server.
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Modify the local record.
+    let localCopy = Object.assign({}, TEST_PROFILE_1);
+    localCopy["street-address"] = "I moved!";
+    profileStorage.addresses.update(guid, localCopy);
+
+    // Replace the record with a tombstone on the server.
+    let collection = server.user("foo").collection("addresses");
+    collection.insert(guid, encryptPayload({
+      id: guid,
+      deleted: true,
+    }), engine.lastSync + 10);
+
+    // Sync again.
+    await engine.sync();
+
+    // We should resurrect the record on the server.
+    let serverPayload = JSON.parse(JSON.parse(collection.payload(guid)).ciphertext);
+    ok(!serverPayload.deleted, "Should resurrect record on server");
+    ok(objectMatches(serverPayload.entry, {
+      "given-name": "Timothy",
+      "family-name": "Berners-Lee",
+      "street-address": "I moved!",
+    }));
+
+    let localRecord = profileStorage.addresses.get(guid);
+    ok(localRecord, "Modified record should not be deleted locally");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+// Unlike most sync engines, we want "both modified" to inspect the records,
+// and if materially different, create a duplicate.
+add_task(async function test_reconcile_both_modified_identical() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // create a record locally.
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // and an identical record on the server.
+    server.insertWBO("foo", "addresses", new ServerWBO(guid, encryptPayload({
+      id: guid,
+      entry: TEST_PROFILE_1,
+    }), Date.now() / 1000));
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    expectLocalProfiles(profileStorage, [{guid}]);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_incoming_dupes() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // Create a profile locally, then sync to upload the new profile to the
+    // server.
+    let guid1 = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Create another profile locally, but don't sync it yet.
+    profileStorage.addresses.add(TEST_PROFILE_2);
+
+    // Now create two records on the server with the same contents as our local
+    // profiles, but different GUIDs.
+    let guid1_dupe = Utils.makeGUID();
+    server.insertWBO("foo", "addresses", new ServerWBO(guid1_dupe, encryptPayload({
+      id: guid1_dupe,
+      entry: Object.assign({
+        version: 1,
+      }, TEST_PROFILE_1),
+    }), engine.lastSync + 10));
+    let guid2_dupe = Utils.makeGUID();
+    server.insertWBO("foo", "addresses", new ServerWBO(guid2_dupe, encryptPayload({
+      id: guid2_dupe,
+      entry: Object.assign({
+        version: 1,
+      }, TEST_PROFILE_2),
+    }), engine.lastSync + 10));
+
+    // Sync again. We should download `guid1_dupe` and `guid2_dupe`, then
+    // reconcile changes.
+    await engine.sync();
+
+    expectLocalProfiles(profileStorage, [
+      // We uploaded `guid1` during the first sync. Even though its contents
+      // are the same as `guid1_dupe`, we keep both.
+      Object.assign({}, TEST_PROFILE_1, {guid: guid1}),
+      Object.assign({}, TEST_PROFILE_1, {guid: guid1_dupe}),
+      // However, we didn't upload `guid2` before downloading `guid2_dupe`, so
+      // we *should* dedupe `guid2` to `guid2_dupe`.
+      Object.assign({}, TEST_PROFILE_2, {guid: guid2_dupe}),
+    ]);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_dedupe_identical_unsynced() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // create a record locally.
+    let localGuid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // and an identical record on the server but different GUID.
+    let remoteGuid = Utils.makeGUID();
+    notEqual(localGuid, remoteGuid);
+    server.insertWBO("foo", "addresses", new ServerWBO(remoteGuid, encryptPayload({
+      id: remoteGuid,
+      entry: Object.assign({
+        version: 1,
+      }, TEST_PROFILE_1),
+    }), Date.now() / 1000));
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // Should have 1 item locally with GUID changed to the remote one.
+    // There's no tombstone as the original was unsynced.
+    expectLocalProfiles(profileStorage, [
+      {
+        guid: remoteGuid,
+      },
+    ]);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_dedupe_identical_synced() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // create a record locally.
+    let localGuid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // sync it - it will no longer be a candidate for de-duping.
+    engine.lastSync = 0;
+    await engine.sync();
+
+    // and an identical record on the server but different GUID.
+    let remoteGuid = Utils.makeGUID();
+    server.insertWBO("foo", "addresses", new ServerWBO(remoteGuid, encryptPayload({
+      id: remoteGuid,
+      entry: Object.assign({
+        version: 1,
+      }, TEST_PROFILE_1),
+    }), engine.lastSync + 10));
+
+    await engine.sync();
+
+    // Should have 2 items locally, since the first was synced.
+    expectLocalProfiles(profileStorage, [
+      {guid: localGuid},
+      {guid: remoteGuid},
+    ]);
+  } finally {
+    await cleanup(server);
+  }
+});
+
+add_task(async function test_dedupe_multiple_candidates() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // It's possible to have duplicate local profiles, with the same fields but
+    // different GUIDs. After a node reassignment, or after disconnecting and
+    // reconnecting to Sync, we might dedupe a local record A to a remote record
+    // B, if we see B before we download and apply A. Since A and B are dupes,
+    // that's OK. We'll write a tombstone for A when we dedupe A to B, and
+    // overwrite that tombstone when we see A.
+
+    let localRecord = {
+      "given-name": "Mark",
+      "family-name": "Hammond",
+      "organization": "Mozilla",
+      "country": "AU",
+      "tel": "+12345678910",
+    };
+    let serverRecord = Object.assign({
+      "version": 1,
+    }, localRecord);
+
+    // We don't pass `sourceSync` so that the records are marked as NEW.
+    let aGuid = profileStorage.addresses.add(localRecord);
+    let bGuid = profileStorage.addresses.add(localRecord);
+
+    // Insert B before A.
+    server.insertWBO("foo", "addresses", new ServerWBO(bGuid, encryptPayload({
+      id: bGuid,
+      entry: serverRecord,
+    }), Date.now() / 1000));
+    server.insertWBO("foo", "addresses", new ServerWBO(aGuid, encryptPayload({
+      id: aGuid,
+      entry: serverRecord,
+    }), Date.now() / 1000));
+
+    engine.lastSync = 0;
+    await engine.sync();
+
+    expectLocalProfiles(profileStorage, [
+      {
+        "guid": aGuid,
+        "given-name": "Mark",
+        "family-name": "Hammond",
+        "organization": "Mozilla",
+        "country": "AU",
+        "tel": "+12345678910",
+      },
+      {
+        "guid": bGuid,
+        "given-name": "Mark",
+        "family-name": "Hammond",
+        "organization": "Mozilla",
+        "country": "AU",
+        "tel": "+12345678910",
+      },
+    ]);
+    // Make sure these are both syncing.
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, aGuid), 0,
+      "A should be marked as syncing");
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, bGuid), 0,
+      "B should be marked as syncing");
+  } finally {
+    await cleanup(server);
+  }
+});
+
+// Unlike most sync engines, we want "both modified" to inspect the records,
+// and if materially different, create a duplicate.
+add_task(async function test_reconcile_both_modified_conflict() {
+  let {profileStorage, server, engine} = await setup();
+  try {
+    // create a record locally.
+    let guid = profileStorage.addresses.add(TEST_PROFILE_1);
+
+    // Upload the record to the server.
+    engine.lastSync = 0;
+    await engine.sync();
+
+    strictEqual(getSyncChangeCounter(profileStorage.addresses, guid), 0,
+      "Original record should be marked as syncing");
+
+    // Change the same field locally and on the server.
+    let localCopy = Object.assign({}, TEST_PROFILE_1);
+    localCopy["street-address"] = "I moved!";
+    profileStorage.addresses.update(guid, localCopy);
+
+    let collection = server.user("foo").collection("addresses");
+    let serverPayload = JSON.parse(JSON.parse(collection.payload(guid)).ciphertext);
+    serverPayload.entry["street-address"] = "I moved, too!";
+    collection.insert(guid, encryptPayload(serverPayload), engine.lastSync + 10);
+
+    // Sync again.
+    await engine.sync();
+
+    // Since we wait to pull changes until we're ready to upload, both records
+    // should now exist on the server; we don't need a follow-up sync.
+    let serverPayloads = collection.payloads();
+    equal(serverPayloads.length, 2, "Both records should exist on server");
+
+    let forkedPayload = serverPayloads.find(payload => payload.id != guid);
+    ok(forkedPayload, "Forked record should exist on server");
+
+    expectLocalProfiles(profileStorage, [
+      {
+        guid,
+        "given-name": "Timothy",
+        "family-name": "Berners-Lee",
+        "street-address": "I moved, too!",
+      },
+      {
+        guid: forkedPayload.id,
+        "given-name": "Timothy",
+        "family-name": "Berners-Lee",
+        "street-address": "I moved!",
+      },
+    ]);
+
+    let changeCounter = getSyncChangeCounter(profileStorage.addresses,
+      forkedPayload.id);
+    strictEqual(changeCounter, 0,
+      "Forked record should be marked as syncing");
+  } finally {
+    await cleanup(server);
+  }
+});
--- a/browser/extensions/formautofill/test/unit/test_transformFields.js
+++ b/browser/extensions/formautofill/test/unit/test_transformFields.js
@@ -536,17 +536,17 @@ add_task(async function test_normalizeAd
   await profileStorage.initialize();
 
   ADDRESS_NORMALIZE_TESTCASES.forEach(testcase => profileStorage.addresses.add(testcase.address));
   await profileStorage._saveImmediately();
 
   profileStorage = new ProfileStorage(path);
   await profileStorage.initialize();
 
-  let addresses = profileStorage.addresses.getAll({noComputedFields: true});
+  let addresses = profileStorage.addresses.getAll();
 
   for (let i in addresses) {
     do_print("Verify testcase: " + ADDRESS_NORMALIZE_TESTCASES[i].description);
     do_check_record_matches(ADDRESS_NORMALIZE_TESTCASES[i].expectedResult, addresses[i]);
   }
 });
 
 add_task(async function test_computeCreditCardFields() {
--- a/browser/extensions/formautofill/test/unit/xpcshell.ini
+++ b/browser/extensions/formautofill/test/unit/xpcshell.ini
@@ -31,13 +31,16 @@ support-files =
 [test_isCJKName.js]
 [test_isFieldEligibleForAutofill.js]
 [test_markAsAutofillField.js]
 [test_migrateRecords.js]
 [test_nameUtils.js]
 [test_onFormSubmitted.js]
 [test_profileAutocompleteResult.js]
 [test_phoneNumber.js]
+[test_reconcile.js]
 [test_savedFieldNames.js]
 [test_toOneLineAddress.js]
 [test_storage_tombstones.js]
 [test_storage_syncfields.js]
 [test_transformFields.js]
+[test_sync.js]
+head = head.js ../../../../../services/sync/tests/unit/head_appinfo.js ../../../../../services/common/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_helpers.js ../../../../../services/sync/tests/unit/head_http_server.js
--- a/browser/locales/en-US/chrome/browser/preferences-old/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences-old/sync.dtd
@@ -11,16 +11,20 @@
 <!ENTITY engine.history.label       "History">
 <!ENTITY engine.history.accesskey   "r">
 <!ENTITY engine.logins.label        "Logins">
 <!ENTITY engine.logins.accesskey    "L">
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
 <!ENTITY engine.addons.label        "Add-ons">
 <!ENTITY engine.addons.accesskey    "A">
+<!ENTITY engine.addresses.label     "Addresses">
+<!ENTITY engine.addresses.accesskey "e">
+<!ENTITY engine.creditcards.label   "Credit cards">
+<!ENTITY engine.creditcards.accesskey "C">
 
 <!-- Device Settings -->
 <!ENTITY fxaSyncDeviceName.label       "Device Name">
 <!ENTITY changeSyncDeviceName.label "Change Device Name…">
 <!ENTITY changeSyncDeviceName.accesskey "h">
 <!ENTITY cancelChangeSyncDeviceName.label "Cancel">
 <!ENTITY cancelChangeSyncDeviceName.accesskey "n">
 <!ENTITY saveChangeSyncDeviceName.label "Save">
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -11,16 +11,20 @@
 <!ENTITY engine.history.label       "History">
 <!ENTITY engine.history.accesskey   "r">
 <!ENTITY engine.logins.label        "Logins">
 <!ENTITY engine.logins.accesskey    "L">
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
 <!ENTITY engine.addons.label        "Add-ons">
 <!ENTITY engine.addons.accesskey    "A">
+<!ENTITY engine.addresses.label     "Addresses">
+<!ENTITY engine.addresses.accesskey "e">
+<!ENTITY engine.creditcards.label   "Credit cards">
+<!ENTITY engine.creditcards.accesskey "C">
 
 <!-- Device Settings -->
 <!ENTITY fxaSyncDeviceName.label       "Device Name">
 <!ENTITY changeSyncDeviceName2.label "Change Device Name…">
 <!ENTITY changeSyncDeviceName2.accesskey "h">
 <!ENTITY cancelChangeSyncDeviceName.label "Cancel">
 <!ENTITY cancelChangeSyncDeviceName.accesskey "n">
 <!ENTITY saveChangeSyncDeviceName.label "Save">
--- a/browser/locales/search/list.json
+++ b/browser/locales/search/list.json
@@ -18,19 +18,16 @@
       "google": "google-nocodes"
     },
     "RU": {
       "google": "google-nocodes"
     },
     "TR": {
       "google": "google-nocodes"
     },
-    "UA": {
-      "google": "google-nocodes"
-    },
     "CN": {
       "google": "google-nocodes"
     },
     "TW": {
       "google": "google-nocodes"
     },
     "HK": {
       "google": "google-nocodes"
--- a/devtools/client/sourceeditor/codemirror/README
+++ b/devtools/client/sourceeditor/codemirror/README
@@ -1,21 +1,21 @@
 This is the CodeMirror editor packaged for the Mozilla Project. CodeMirror
 is a JavaScript component that provides a code editor in the browser. When
 a mode is available for the language you are coding in, it will color your
 code, and optionally help with indentation.
 
 # Upgrade
 
-Currently used version is 5.16.0. To upgrade: download a new version of
+Currently used version is 5.27.4. To upgrade: download a new version of
 CodeMirror from the project's page [1] and replace all JavaScript and
 CSS files inside the codemirror directory [2].
 
 Then to recreate codemirror.bundle.js:
- > cd devtools/client
+ > cd devtools/client/sourceeditor
  > npm install
  > webpack
 
 To confirm the functionality run mochitests for the following components:
 
  * sourceeditor
  * scratchpad
  * debugger
--- a/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
+++ b/devtools/client/sourceeditor/codemirror/addon/comment/comment.js
@@ -41,22 +41,27 @@
       } else {
         cm.lineComment(from, to, options);
       }
     }
   });
 
   // Rough heuristic to try and detect lines that are part of multi-line string
   function probablyInsideString(cm, pos, line) {
-    return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
+    return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
+  }
+
+  function getMode(cm, pos) {
+    var mode = cm.getMode()
+    return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
   }
 
   CodeMirror.defineExtension("lineComment", function(from, to, options) {
     if (!options) options = noOptions;
-    var self = this, mode = self.getModeAt(from);
+    var self = this, mode = getMode(self, from);
     var firstLine = self.getLine(from.line);
     if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
 
     var commentString = options.lineComment || mode.lineComment;
     if (!commentString) {
       if (options.blockCommentStart || mode.blockCommentStart) {
         options.fullLines = true;
         self.blockComment(from, to, options);
@@ -90,24 +95,25 @@
             self.replaceRange(commentString + pad, Pos(i, 0));
         }
       }
     });
   });
 
   CodeMirror.defineExtension("blockComment", function(from, to, options) {
     if (!options) options = noOptions;
-    var self = this, mode = self.getModeAt(from);
+    var self = this, mode = getMode(self, from);
     var startString = options.blockCommentStart || mode.blockCommentStart;
     var endString = options.blockCommentEnd || mode.blockCommentEnd;
     if (!startString || !endString) {
       if ((options.lineComment || mode.lineComment) && options.fullLines != false)
         self.lineComment(from, to, options);
       return;
     }
+    if (/\bcomment\b/.test(self.getTokenTypeAt(Pos(from.line, 0)))) return
 
     var end = Math.min(to.line, self.lastLine());
     if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
 
     var pad = options.padding == null ? " " : options.padding;
     if (from.line > end) return;
 
     self.operation(function() {
@@ -123,29 +129,29 @@
         self.replaceRange(endString, to);
         self.replaceRange(startString, from);
       }
     });
   });
 
   CodeMirror.defineExtension("uncomment", function(from, to, options) {
     if (!options) options = noOptions;
-    var self = this, mode = self.getModeAt(from);
+    var self = this, mode = getMode(self, from);
     var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
 
     // 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 (found == -1 && nonWS.test(line)) 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;
@@ -157,25 +163,29 @@
       if (didSomething) return true;
     }
 
     // Try block comments
     var startString = options.blockCommentStart || mode.blockCommentStart;
     var endString = options.blockCommentEnd || mode.blockCommentEnd;
     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);
+    var startLine = self.getLine(start), open = startLine.indexOf(startString)
+    if (open == -1) return false
+    var endLine = end == start ? startLine : self.getLine(end)
+    var close = endLine.indexOf(endString, end == start ? open + startString.length : 0);
     if (close == -1 && start != end) {
       endLine = self.getLine(--end);
-      close = endLine.lastIndexOf(endString);
+      close = endLine.indexOf(endString);
     }
-    if (open == -1 || close == -1 ||
-        !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
-        !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+    var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
+    if (close == -1 ||
+        !/comment/.test(self.getTokenTypeAt(insideStart)) ||
+        !/comment/.test(self.getTokenTypeAt(insideEnd)) ||
+        self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
       return false;
 
     // Avoid killing block comments completely outside the selection.
     // Positions of the last startString before the start of the selection, and the first endString after it.
     var lastStart = startLine.lastIndexOf(startString, from.ch);
     var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
     if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
     // Positions of the first endString after the end of the selection, and the last startString before it.
--- a/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/closebrackets.js
@@ -40,17 +40,17 @@
     keyMap["'" + bind.charAt(i) + "'"] = handler(bind.charAt(i));
 
   function handler(ch) {
     return function(cm) { return handleChar(cm, ch); };
   }
 
   function getConfig(cm) {
     var deflt = cm.state.closeBrackets;
-    if (!deflt) return null;
+    if (!deflt || deflt.override) return deflt;
     var mode = cm.getModeAt(cm.getCursor());
     return mode.closeBrackets || deflt;
   }
 
   function handleBackspace(cm) {
     var conf = getConfig(cm);
     if (!conf || cm.getOption("disableInput")) return CodeMirror.Pass;
 
@@ -111,17 +111,19 @@
 
     var type;
     for (var i = 0; i < ranges.length; i++) {
       var range = ranges[i], cur = range.head, curType;
       var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
       if (opening && !range.empty()) {
         curType = "surround";
       } else if ((identical || !opening) && next == ch) {
-        if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
+        if (identical && stringStartsAfter(cm, cur))
+          curType = "both";
+        else if (triples.indexOf(ch) >= 0 && cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == ch + ch + ch)
           curType = "skipThree";
         else
           curType = "skip";
       } else if (identical && cur.ch > 1 && triples.indexOf(ch) >= 0 &&
                  cm.getRange(Pos(cur.line, cur.ch - 2), cur) == ch + ch &&
                  (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != ch)) {
         curType = "addFour";
       } else if (identical) {
@@ -178,18 +180,23 @@
   }
 
   // Project the token type that will exists after the given char is
   // typed, and use it to determine whether it would cause the start
   // of a string token.
   function enteringString(cm, pos, ch) {
     var line = cm.getLine(pos.line);
     var token = cm.getTokenAt(pos);
-    if (/\bstring2?\b/.test(token.type)) return false;
+    if (/\bstring2?\b/.test(token.type) || stringStartsAfter(cm, pos)) return false;
     var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
     stream.pos = stream.start = token.start;
     for (;;) {
       var type1 = cm.getMode().token(stream, token.state);
       if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
       stream.start = stream.pos;
     }
   }
+
+  function stringStartsAfter(cm, pos) {
+    var token = cm.getTokenAt(Pos(pos.line, pos.ch + 1))
+    return /\bstring/.test(token.type) && token.start == pos.ch
+  }
 });
--- a/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/continuelist.js
@@ -6,18 +6,18 @@
     mod(require("../../lib/codemirror"));
   else if (typeof define == "function" && define.amd) // AMD
     define(["../../lib/codemirror"], mod);
   else // Plain browser env
     mod(CodeMirror);
 })(function(CodeMirror) {
   "use strict";
 
-  var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))(\s*)/,
-      emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)[.)])(\s*)$/,
+  var listRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,
+      emptyListRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,
       unorderedListRE = /[*+-]\s/;
 
   CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
     if (cm.getOption("disableInput")) return CodeMirror.Pass;
     var ranges = cm.listSelections(), replacements = [];
     for (var i = 0; i < ranges.length; i++) {
       var pos = ranges[i].head;
       var eolState = cm.getStateAfter(pos.line);
@@ -25,26 +25,26 @@
       var inQuote = eolState.quote !== 0;
 
       var line = cm.getLine(pos.line), match = listRE.exec(line);
       if (!ranges[i].empty() || (!inList && !inQuote) || !match) {
         cm.execCommand("newlineAndIndent");
         return;
       }
       if (emptyListRE.test(line)) {
-        cm.replaceRange("", {
+        if (!/>\s*$/.test(line)) cm.replaceRange("", {
           line: pos.line, ch: 0
         }, {
           line: pos.line, ch: pos.ch + 1
         });
         replacements[i] = "\n";
       } else {
         var indent = match[1], after = match[5];
         var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
-          ? match[2]
+          ? match[2].replace("x", " ")
           : (parseInt(match[3], 10) + 1) + match[4];
 
         replacements[i] = "\n" + indent + bullet + after;
       }
     }
 
     cm.replaceSelections(replacements);
   };
--- a/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
+++ b/devtools/client/sourceeditor/codemirror/addon/edit/matchbrackets.js
@@ -11,22 +11,31 @@
 })(function(CodeMirror) {
   var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
     (document.documentMode == null || document.documentMode < 8);
 
   var Pos = CodeMirror.Pos;
 
   var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
 
-  function findMatchingBracket(cm, where, strict, config) {
+  function findMatchingBracket(cm, where, config) {
     var line = cm.getLineHandle(where.line), pos = where.ch - 1;
-    var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
+    var afterCursor = config && config.afterCursor
+    if (afterCursor == null)
+      afterCursor = /(^| )cm-fat-cursor($| )/.test(cm.getWrapperElement().className)
+
+    // A cursor is defined as between two characters, but in in vim command mode
+    // (i.e. not insert mode), the cursor is visually represented as a
+    // highlighted box on top of the 2nd character. Otherwise, we allow matches
+    // from before or after the cursor.
+    var match = (!afterCursor && pos >= 0 && matching[line.text.charAt(pos)]) ||
+        matching[line.text.charAt(++pos)];
     if (!match) return null;
     var dir = match.charAt(1) == ">" ? 1 : -1;
-    if (strict && (dir > 0) != (pos == where.ch)) return null;
+    if (config && config.strict && (dir > 0) != (pos == where.ch)) return null;
     var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
 
     var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
     if (found == null) return null;
     return {from: Pos(where.line, pos), to: found && found.pos,
             match: found && found.ch == match.charAt(0), forward: dir > 0};
   }
 
@@ -64,17 +73,17 @@
     return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
   }
 
   function matchBrackets(cm, autoclear, config) {
     // Disable brace matching in long lines, since it'll cause hugely slow updates
     var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
     var marks = [], ranges = cm.listSelections();
     for (var i = 0; i < ranges.length; i++) {
-      var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+      var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, config);
       if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
         var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
         marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
         if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
           marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
       }
     }
 
@@ -97,24 +106,35 @@
   function doMatchBrackets(cm) {
     cm.operation(function() {
       if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
       currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
     });
   }
 
   CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
-    if (old && old != CodeMirror.Init)
+    if (old && old != CodeMirror.Init) {
       cm.off("cursorActivity", doMatchBrackets);
+      if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+    }
     if (val) {
       cm.state.matchBrackets = typeof val == "object" ? val : {};
       cm.on("cursorActivity", doMatchBrackets);
     }
   });
 
   CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
-  CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
-    return findMatchingBracket(this, pos, strict, config);
+  CodeMirror.defineExtension("findMatchingBracket", function(pos, config, oldConfig){
+    // Backwards-compatibility kludge
+    if (oldConfig || typeof config == "boolean") {
+      if (!oldConfig) {
+        config = config ? {strict: true} : null
+      } else {
+        oldConfig.strict = config
+        config = oldConfig
+      }
+    }
+    return findMatchingBracket(this, pos, config)
   });
   CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
     return scanForBracket(this, pos, dir, style, config);
   });
 });
--- a/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/indent-fold.js
@@ -6,39 +6,43 @@
     mod(require("../../lib/codemirror"));
   else if (typeof define == "function" && define.amd) // AMD
     define(["../../lib/codemirror"], mod);
   else // Plain browser env
     mod(CodeMirror);
 })(function(CodeMirror) {
 "use strict";
 
+function lineIndent(cm, lineNo) {
+  var text = cm.getLine(lineNo)
+  var spaceTo = text.search(/\S/)
+  if (spaceTo == -1 || /\bcomment\b/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, spaceTo + 1))))
+    return -1
+  return CodeMirror.countColumn(text, null, cm.getOption("tabSize"))
+}
+
 CodeMirror.registerHelper("fold", "indent", function(cm, start) {
-  var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line);
-  if (!/\S/.test(firstLine)) return;
-  var getIndent = function(line) {
-    return CodeMirror.countColumn(line, null, tabSize);
-  };
-  var myIndent = getIndent(firstLine);
-  var lastLineInFold = null;
+  var myIndent = lineIndent(cm, start.line)
+  if (myIndent < 0) return
+  var lastLineInFold = null
+
   // Go through lines until we find a line that definitely doesn't belong in
   // the block we're folding, or to the end.
   for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) {
-    var curLine = cm.getLine(i);
-    var curIndent = getIndent(curLine);
-    if (curIndent > myIndent) {
+    var indent = lineIndent(cm, i)
+    if (indent == -1) {
+    } else if (indent > myIndent) {
       // Lines with a greater indent are considered part of the block.
       lastLineInFold = i;
-    } else if (!/\S/.test(curLine)) {
-      // Empty lines might be breaks within the block we're trying to fold.
     } else {
-      // A non-empty line at an indent equal to or less than ours marks the
-      // start of another block.
+      // If this line has non-space, non-comment content, and is
+      // indented less or equal to the start line, it is the start of
+      // another block.
       break;
     }
   }
   if (lastLineInFold) return {
-    from: CodeMirror.Pos(start.line, firstLine.length),
+    from: CodeMirror.Pos(start.line, cm.getLine(start.line).length),
     to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length)
   };
 });
 
 });
--- a/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
+++ b/devtools/client/sourceeditor/codemirror/addon/fold/xml-fold.js
@@ -16,18 +16,18 @@
 
   var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
   var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
   var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
 
   function Iter(cm, line, ch, range) {
     this.line = line; this.ch = ch;
     this.cm = cm; this.text = cm.getLine(line);
-    this.min = range ? range.from : cm.firstLine();
-    this.max = range ? range.to - 1 : cm.lastLine();
+    this.min = range ? Math.max(range.from, cm.firstLine()) : cm.firstLine();
+    this.max = range ? Math.min(range.to - 1, cm.lastLine()) : cm.lastLine();
   }
 
   function tagAt(iter, ch) {
     var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
     return type && /\btag\b/.test(type);
   }
 
   function nextLine(iter) {
@@ -158,20 +158,20 @@
     if (start[1]) { // closing tag
       return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
     } else { // opening tag
       iter = new Iter(cm, to.line, to.ch, range);
       return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
     }
   };
 
-  CodeMirror.findEnclosingTag = function(cm, pos, range) {
+  CodeMirror.findEnclosingTag = function(cm, pos, range, tag) {
     var iter = new Iter(cm, pos.line, pos.ch, range);
     for (;;) {
-      var open = findMatchingOpen(iter);
+      var open = findMatchingOpen(iter, tag);
       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
--- a/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
+++ b/devtools/client/sourceeditor/codemirror/addon/hint/show-hint.js
@@ -225,16 +225,18 @@
     hints.style.left = left + "px";
     hints.style.top = top + "px";
     // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
     var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
     var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
     (completion.options.container || document.body).appendChild(hints);
     var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
     var scrolls = hints.scrollHeight > hints.clientHeight + 1
+    var startScroll = cm.getScrollInfo();
+
     if (overlapY > 0) {
       var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
       if (curTop - height > 0) { // Fits above cursor
         hints.style.top = (top = pos.top - height) + "px";
         below = false;
       } else if (height > winH) {
         hints.style.height = (winH - 5) + "px";
         hints.style.top = (top = pos.bottom - box.top) + "px";
@@ -268,17 +270,16 @@
     }));
 
     if (completion.options.closeOnUnfocus) {
       var closingOnBlur;
       cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
       cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
     }
 
-    var startScroll = cm.getScrollInfo();
     cm.on("scroll", this.onScroll = function() {
       var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
       var newTop = top + startScroll.top - curScroll.top;
       var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
       if (!below) point += hints.offsetHeight;
       if (point <= editor.top || point >= editor.bottom) return completion.close();
       hints.style.top = newTop + "px";
       hints.style.left = (left + startScroll.left - curScroll.left) + "px";
--- a/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
+++ b/devtools/client/sourceeditor/codemirror/addon/search/match-highlighter.js
@@ -40,44 +40,63 @@
   }
 
   function State(options) {
     this.options = {}
     for (var name in defaults)
       this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
     this.overlay = this.timeout = null;
     this.matchesonscroll = null;
+    this.active = false;
   }
 
   CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
     if (old && old != CodeMirror.Init) {
       removeOverlay(cm);
       clearTimeout(cm.state.matchHighlighter.timeout);
       cm.state.matchHighlighter = null;
       cm.off("cursorActivity", cursorActivity);
+      cm.off("focus", onFocus)
     }
     if (val) {
-      cm.state.matchHighlighter = new State(val);
-      highlightMatches(cm);
+      var state = cm.state.matchHighlighter = new State(val);
+      if (cm.hasFocus()) {
+        state.active = true
+        highlightMatches(cm)
+      } else {
+        cm.on("focus", onFocus)
+      }
       cm.on("cursorActivity", cursorActivity);
     }
   });
 
   function cursorActivity(cm) {
     var state = cm.state.matchHighlighter;
+    if (state.active || cm.hasFocus()) scheduleHighlight(cm, state)
+  }
+
+  function onFocus(cm) {
+    var state = cm.state.matchHighlighter
+    if (!state.active) {
+      state.active = true
+      scheduleHighlight(cm, state)
+    }
+  }
+
+  function scheduleHighlight(cm, state) {
     clearTimeout(state.timeout);
     state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
   }
 
   function addOverlay(cm, query, hasBoundary, style) {
     var state = cm.state.matchHighlighter;
     cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
     if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
       var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
-      state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, true,
+      state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
         {className: "CodeMirror-selection-highlight-scrollbar"});
     }
   }
 
   function removeOverlay(cm) {
     var state = cm.state.matchHighlighter;
     if (state.overlay) {
       cm.removeOverlay(state.overlay);
--- a/devtools/client/sourceeditor/codemirror/addon/search/search.js
+++ b/devtools/client/sourceeditor/codemirror/addon/search/search.js
@@ -49,25 +49,26 @@
   }
 
   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, queryCaseInsensitive(query));
+    return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});
   }
 
-  function persistentDialog(cm, text, deflt, f) {
-    cm.openDialog(text, f, {
+  function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
+    cm.openDialog(text, onEnter, {
       value: deflt,
       selectValueOnOpen: true,
       closeOnEnter: false,
-      onClose: function() { clearSearch(cm); }
+      onClose: function() { clearSearch(cm); },
+      onKeyDown: onKeyDown
     });
   }
 
   function dialog(cm, text, shortText, deflt, f) {
     if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
     else f(prompt(shortText, deflt));
   }
 
@@ -106,17 +107,17 @@
     state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
     cm.addOverlay(state.overlay);
     if (cm.showMatchesOnScrollbar) {
       if (state.annotate) { state.annotate.clear(); state.annotate = null; }
       state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
     }
   }
 
-  function doSearch(cm, rev, persistent) {
+  function doSearch(cm, rev, persistent, immediate) {
     if (!queryDialog) {
       let doc = cm.getWrapperElement().ownerDocument;
       let inp = doc.createElement("input");
 
       inp.type = "search";
       inp.placeholder = cm.l10n("findCmd.promptMessage");
       inp.style.marginInlineStart = "1em";
       inp.style.marginInlineEnd = "1em";
@@ -128,32 +129,50 @@
       queryDialog.style.display = "flex";
     }
 
     var state = getSearchState(cm);
     if (state.query) return findNext(cm, rev);
     var q = cm.getSelection() || state.lastQuery;
     if (persistent && cm.openDialog) {
       var hiding = null
-      persistentDialog(cm, queryDialog, q, function(query, event) {
+      var searchNext = function(query, event) {
         CodeMirror.e_stop(event);
         if (!query) return;
         if (query != state.queryText) {
           startSearch(cm, state, query);
           state.posFrom = state.posTo = cm.getCursor();
         }
         if (hiding) hiding.style.opacity = 1
         findNext(cm, event.shiftKey, function(_, to) {
           var dialog
           if (to.line < 3 && document.querySelector &&
               (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
               dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
             (hiding = dialog).style.opacity = .4
         })
+      };
+      persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
+        var keyName = CodeMirror.keyName(event)
+        var cmd = CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
+        if (!cmd) cmd = cm.getOption('extraKeys')[keyName]
+        if (cmd == "findNext" || cmd == "findPrev" ||
+          cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
+          CodeMirror.e_stop(event);
+          startSearch(cm, getSearchState(cm), query);
+          cm.execCommand(cmd);
+        } else if (cmd == "find" || cmd == "findPersistent") {
+          CodeMirror.e_stop(event);
+          searchNext(query, event);
+        }
       });
+      if (immediate && q) {
+        startSearch(cm, state, q);
+        findNext(cm, rev);
+      }
     } else {
       dialog(cm, queryDialog, "Search for:", q, function(query) {
         if (query && !state.query) cm.operation(function() {
           startSearch(cm, state, query);
           state.posFrom = state.posTo = cm.getCursor();
           findNext(cm, rev);
         });
       });
@@ -179,34 +198,34 @@
     if (!state.query) return;
     state.query = state.queryText = null;
     cm.removeOverlay(state.overlay);
     if (state.annotate) { state.annotate.clear(); state.annotate = null; }
   });}
 
   var replaceQueryDialog =
     ' <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
-  var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
-  var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>";
+  var replacementQueryDialog = '<span class="CodeMirror-search-label">With:</span> <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+  var doReplaceConfirm = '<span class="CodeMirror-search-label">Replace?</span> <button>Yes</button> <button>No</button> <button>All</button> <button>Stop</button>';
 
   function replaceAll(cm, query, text) {
     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)/g, function(_, i) {return match[i];}));
         } else cursor.replace(text);
       }
     });
   }
 
   function replace(cm, all) {
     if (cm.getOption("readOnly")) return;
     var query = cm.getSelection() || getSearchState(cm).lastQuery;
-    var dialogText = all ? "Replace all:" : "Replace:"
+    var dialogText = '<span class="CodeMirror-search-label">' + (all ? 'Replace all:' : 'Replace:') + '</span>';
     dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
       if (!query) return;
       query = parseQuery(query);
       dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
         text = parseString(text)
         if (all) {
           replaceAll(cm, query, text)
         } else {
@@ -233,14 +252,16 @@
           advance();
         }
       });
     });
   }
 
   CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
   CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
+  CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
+  CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
   CodeMirror.commands.findNext = doSearch;
   CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
   CodeMirror.commands.clearSearch = clearSearch;
   CodeMirror.commands.replace = replace;
   CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
 });
--- a/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
+++ b/devtools/client/sourceeditor/codemirror/addon/search/searchcursor.js
@@ -1,189 +1,287 @@
 // CodeMirror, copyright (c) by Marijn Haverbeke and others
 // Distributed under an MIT license: http://codemirror.net/LICENSE
 
 (function(mod) {
   if (typeof exports == "object" && typeof module == "object") // CommonJS
-    mod(require("../../lib/codemirror"));
+    mod(require("../../lib/codemirror"))
   else if (typeof define == "function" && define.amd) // AMD
-    define(["../../lib/codemirror"], mod);
+    define(["../../lib/codemirror"], mod)
   else // Plain browser env
-    mod(CodeMirror);
+    mod(CodeMirror)
 })(function(CodeMirror) {
-  "use strict";
-  var Pos = CodeMirror.Pos;
+  "use strict"
+  var Pos = CodeMirror.Pos
+
+  function regexpFlags(regexp) {
+    var flags = regexp.flags
+    return flags != null ? flags : (regexp.ignoreCase ? "i" : "")
+      + (regexp.global ? "g" : "")
+      + (regexp.multiline ? "m" : "")
+  }
+
+  function ensureGlobal(regexp) {
+    return regexp.global ? regexp : new RegExp(regexp.source, regexpFlags(regexp) + "g")
+  }
 
-  function SearchCursor(doc, query, pos, caseFold) {
-    this.atOccurrence = false; this.doc = doc;
-    if (caseFold == null && typeof query == "string") caseFold = false;
+  function maybeMultiline(regexp) {
+    return /\\s|\\n|\n|\\W|\\D|\[\^/.test(regexp.source)
+  }
 
-    pos = pos ? doc.clipPos(pos) : Pos(0, 0);
-    this.pos = {from: pos, to: pos};
+  function searchRegexpForward(doc, regexp, start) {
+    regexp = ensureGlobal(regexp)
+    for (var line = start.line, ch = start.ch, last = doc.lastLine(); line <= last; line++, ch = 0) {
+      regexp.lastIndex = ch
+      var string = doc.getLine(line), match = regexp.exec(string)
+      if (match)
+        return {from: Pos(line, match.index),
+                to: Pos(line, match.index + match[0].length),
+                match: match}
+    }
+  }
+
+  function searchRegexpForwardMultiline(doc, regexp, start) {
+    if (!maybeMultiline(regexp)) return searchRegexpForward(doc, regexp, start)
 
-    // The matches method is filled in based on the type of query.
-    // It takes a position and a direction, and returns an object
-    // describing the next occurrence of the query, or null if no
-    // more matches were found.
-    if (typeof query != "string") { // Regexp match
-      if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
-      this.matches = function(reverse, pos) {
-        if (reverse) {
-          query.lastIndex = 0;
-          var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
-          for (;;) {
-            query.lastIndex = cutOff;
-            var newMatch = query.exec(line);
-            if (!newMatch) break;
-            match = newMatch;
-            start = match.index;
-            cutOff = match.index + (match[0].length || 1);
-            if (cutOff == line.length) break;
-          }
-          var matchLen = (match && match[0].length) || 0;
-          if (!matchLen) {
-            if (start == 0 && line.length == 0) {match = undefined;}
-            else if (start != doc.getLine(pos.line).length) {
-              matchLen++;
-            }
-          }
-        } else {
-          query.lastIndex = pos.ch;
-          var line = doc.getLine(pos.line), match = query.exec(line);
-          var matchLen = (match && match[0].length) || 0;
-          var start = match && match.index;
-          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) {
-            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 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 (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
-            return {from: from, to: Pos(ln, origTarget[last].length)};
-          }
-        };
+    regexp = ensureGlobal(regexp)
+    var string, chunk = 1
+    for (var line = start.line, last = doc.lastLine(); line <= last;) {
+      // This grows the search buffer in exponentially-sized chunks
+      // between matches, so that nearby matches are fast and don't
+      // require concatenating the whole document (in case we're
+      // searching for something that has tons of matches), but at the
+      // same time, the amount of retries is limited.
+      for (var i = 0; i < chunk; i++) {
+        var curLine = doc.getLine(line++)
+        string = string == null ? curLine : string + "\n" + curLine
+      }
+      chunk = chunk * 2
+      regexp.lastIndex = start.ch
+      var match = regexp.exec(string)
+      if (match) {
+        var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
+        var startLine = start.line + before.length - 1, startCh = before[before.length - 1].length
+        return {from: Pos(startLine, startCh),
+                to: Pos(startLine + inside.length - 1,
+                        inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
+                match: match}
+      }
+    }
+  }
+
+  function lastMatchIn(string, regexp) {
+    var cutOff = 0, match
+    for (;;) {
+      regexp.lastIndex = cutOff
+      var newMatch = regexp.exec(string)
+      if (!newMatch) return match
+      match = newMatch
+      cutOff = match.index + (match[0].length || 1)
+      if (cutOff == string.length) return match
+    }
+  }
+
+  function searchRegexpBackward(doc, regexp, start) {
+    regexp = ensureGlobal(regexp)
+    for (var line = start.line, ch = start.ch, first = doc.firstLine(); line >= first; line--, ch = -1) {
+      var string = doc.getLine(line)
+      if (ch > -1) string = string.slice(0, ch)
+      var match = lastMatchIn(string, regexp)
+      if (match)
+        return {from: Pos(line, match.index),
+                to: Pos(line, match.index + match[0].length),
+                match: match}
+    }
+  }
+
+  function searchRegexpBackwardMultiline(doc, regexp, start) {
+    regexp = ensureGlobal(regexp)
+    var string, chunk = 1
+    for (var line = start.line, first = doc.firstLine(); line >= first;) {
+      for (var i = 0; i < chunk; i++) {
+        var curLine = doc.getLine(line--)
+        string = string == null ? curLine.slice(0, start.ch) : curLine + "\n" + string
+      }
+      chunk *= 2
+
+      var match = lastMatchIn(string, regexp)
+      if (match) {
+        var before = string.slice(0, match.index).split("\n"), inside = match[0].split("\n")
+        var startLine = line + before.length, startCh = before[before.length - 1].length
+        return {from: Pos(startLine, startCh),
+                to: Pos(startLine + inside.length - 1,
+                        inside.length == 1 ? startCh + inside[0].length : inside[inside.length - 1].length),
+                match: match}
       }
     }
   }
 
+  var doFold, noFold
+  if (String.prototype.normalize) {
+    doFold = function(str) { return str.normalize("NFD").toLowerCase() }
+    noFold = function(str) { return str.normalize("NFD") }
+  } else {
+    doFold = function(str) { return str.toLowerCase() }
+    noFold = function(str) { return str }
+  }
+
+  // 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, foldFunc) {
+    if (orig.length == folded.length) return pos
+    for (var pos1 = Math.min(pos, orig.length);;) {
+      var len1 = foldFunc(orig.slice(0, pos1)).length
+      if (len1 < pos) ++pos1
+      else if (len1 > pos) --pos1
+      else return pos1
+    }
+  }
+
+  function searchStringForward(doc, query, start, caseFold) {
+    // Empty string would match anything and never progress, so we
+    // define it to match nothing instead.
+    if (!query.length) return null
+    var fold = caseFold ? doFold : noFold
+    var lines = fold(query).split(/\r|\n\r?/)
+
+    search: for (var line = start.line, ch = start.ch, last = doc.lastLine() + 1 - lines.length; line <= last; line++, ch = 0) {
+      var orig = doc.getLine(line).slice(ch), string = fold(orig)
+      if (lines.length == 1) {
+        var found = string.indexOf(lines[0])
+        if (found == -1) continue search
+        var start = adjustPos(orig, string, found, fold) + ch
+        return {from: Pos(line, adjustPos(orig, string, found, fold) + ch),
+                to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold) + ch)}
+      } else {
+        var cutFrom = string.length - lines[0].length
+        if (string.slice(cutFrom) != lines[0]) continue search
+        for (var i = 1; i < lines.length - 1; i++)
+          if (fold(doc.getLine(line + i)) != lines[i]) continue search
+        var end = doc.getLine(line + lines.length - 1), endString = fold(end), lastLine = lines[lines.length - 1]
+        if (end.slice(0, lastLine.length) != lastLine) continue search
+        return {from: Pos(line, adjustPos(orig, string, cutFrom, fold) + ch),
+                to: Pos(line + lines.length - 1, adjustPos(end, endString, lastLine.length, fold))}
+      }
+    }
+  }
+
+  function searchStringBackward(doc, query, start, caseFold) {
+    if (!query.length) return null
+    var fold = caseFold ? doFold : noFold
+    var lines = fold(query).split(/\r|\n\r?/)
+
+    search: for (var line = start.line, ch = start.ch, first = doc.firstLine() - 1 + lines.length; line >= first; line--, ch = -1) {
+      var orig = doc.getLine(line)
+      if (ch > -1) orig = orig.slice(0, ch)
+      var string = fold(orig)
+      if (lines.length == 1) {
+        var found = string.lastIndexOf(lines[0])
+        if (found == -1) continue search
+        return {from: Pos(line, adjustPos(orig, string, found, fold)),
+                to: Pos(line, adjustPos(orig, string, found + lines[0].length, fold))}
+      } else {
+        var lastLine = lines[lines.length - 1]
+        if (string.slice(0, lastLine.length) != lastLine) continue search
+        for (var i = 1, start = line - lines.length + 1; i < lines.length - 1; i++)
+          if (fold(doc.getLine(start + i)) != lines[i]) continue search
+        var top = doc.getLine(line + 1 - lines.length), topString = fold(top)
+        if (topString.slice(topString.length - lines[0].length) != lines[0]) continue search
+        return {from: Pos(line + 1 - lines.length, adjustPos(top, topString, top.length - lines[0].length, fold)),
+                to: Pos(line, adjustPos(orig, string, lastLine.length, fold))}
+      }
+    }
+  }
+
+  function SearchCursor(doc, query, pos, options) {
+    this.atOccurrence = false
+    this.doc = doc
+    pos = pos ? doc.clipPos(pos) : Pos(0, 0)
+    this.pos = {from: pos, to: pos}
+
+    var caseFold
+    if (typeof options == "object") {
+      caseFold = options.caseFold
+    } else { // Backwards compat for when caseFold was the 4th argument
+      caseFold = options
+      options = null
+    }
+
+    if (typeof query == "string") {
+      if (caseFold == null) caseFold = false
+      this.matches = function(reverse, pos) {
+        return (reverse ? searchStringBackward : searchStringForward)(doc, query, pos, caseFold)
+      }
+    } else {
+      query = ensureGlobal(query)
+      if (!options || options.multiline !== false)
+        this.matches = function(reverse, pos) {
+          return (reverse ? searchRegexpBackwardMultiline : searchRegexpForwardMultiline)(doc, query, pos)
+        }
+      else
+        this.matches = function(reverse, pos) {
+          return (reverse ? searchRegexpBackward : searchRegexpForward)(doc, query, pos)
+        }
+    }
+  }
+
   SearchCursor.prototype = {
-    findNext: function() {return this.find(false);},
-    findPrevious: function() {return this.find(true);},
+    findNext: function() {return this.find(false)},
+    findPrevious: function() {return this.find(true)},
 
     find: function(reverse) {
-      var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
-      function savePosAndFail(line) {
-        var pos = Pos(line, 0);
-        self.pos = {from: pos, to: pos};
-        self.atOccurrence = false;
-        return false;
+      var result = this.matches(reverse, this.doc.clipPos(reverse ? this.pos.from : this.pos.to))
+
+      // Implements weird auto-growing behavior on null-matches for
+      // backwards-compatiblity with the vim code (unfortunately)
+      while (result && CodeMirror.cmpPos(result.from, result.to) == 0) {
+        if (reverse) {
+          if (result.from.ch) result.from = Pos(result.from.line, result.from.ch - 1)
+          else if (result.from.line == this.doc.firstLine()) result = null
+          else result = this.matches(reverse, this.doc.clipPos(Pos(result.from.line - 1)))
+        } else {
+          if (result.to.ch < this.doc.getLine(result.to.line).length) result.to = Pos(result.to.line, result.to.ch + 1)
+          else if (result.to.line == this.doc.lastLine()) result = null
+          else result = this.matches(reverse, Pos(result.to.line + 1, 0))
+        }
       }
 
-      for (;;) {
-        if (this.pos = this.matches(reverse, 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 {
-          var maxLine = this.doc.lineCount();
-          if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
-          pos = Pos(pos.line + 1, 0);
-        }
+      if (result) {
+        this.pos = result
+        this.atOccurrence = true
+        return this.pos.match || true
+      } else {
+        var end = Pos(reverse ? this.doc.firstLine() : this.doc.lastLine() + 1, 0)
+        this.pos = {from: end, to: end}
+        return this.atOccurrence = false
       }
     },
 
-    from: function() {if (this.atOccurrence) return this.pos.from;},
-    to: function() {if (this.atOccurrence) return this.pos.to;},
+    from: function() {if (this.atOccurrence) return this.pos.from},
+    to: function() {if (this.atOccurrence) return this.pos.to},
 
     replace: function(newText, origin) {
-      if (!this.atOccurrence) return;
-      var lines = CodeMirror.splitLines(newText);
-      this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin);
+      if (!this.atOccurrence) return
+      var lines = CodeMirror.splitLines(newText)
+      this.doc.replaceRange(lines, this.pos.from, this.pos.to, origin)
       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;
+                        lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0))
     }
   }
 
   CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
-    return new SearchCursor(this.doc, 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);
-  });
+    return new SearchCursor(this, query, pos, caseFold)
+  })
 
   CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
-    var ranges = [];
-    var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+    var ranges = []
+    var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold)
     while (cur.findNext()) {
-      if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
-      ranges.push({anchor: cur.from(), head: cur.to()});
+      if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break
+      ranges.push({anchor: cur.from(), head: cur.to()})
     }
     if (ranges.length)
-      this.setSelections(ranges, 0);
-  });
+      this.setSelections(ranges, 0)
+  })
 });
--- a/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/active-line.js
@@ -1,40 +1,36 @@
 // CodeMirror, copyright (c) by Marijn Haverbeke and others
 // Distributed under an MIT license: http://codemirror.net/LICENSE
 
-// 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(mod) {
   if (typeof exports == "object" && typeof module == "object") // CommonJS
     mod(require("../../lib/codemirror"));
   else if (typeof define == "function" && define.amd) // AMD
     define(["../../lib/codemirror"], mod);
   else // Plain browser env
     mod(CodeMirror);
 })(function(CodeMirror) {
   "use strict";
   var WRAP_CLASS = "CodeMirror-activeline";
   var BACK_CLASS = "CodeMirror-activeline-background";
   var GUTT_CLASS = "CodeMirror-activeline-gutter";
 
   CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
-    var prev = old && old != CodeMirror.Init;
-    if (val && !prev) {
+    var prev = old == CodeMirror.Init ? false : old;
+    if (val == prev) return
+    if (prev) {
+      cm.off("beforeSelectionChange", selectionChange);
+      clearActiveLines(cm);
+      delete cm.state.activeLines;
+    }
+    if (val) {
       cm.state.activeLines = [];
       updateActiveLines(cm, cm.listSelections());
       cm.on("beforeSelectionChange", selectionChange);
-    } else if (!val && prev) {
-      cm.off("beforeSelectionChange", selectionChange);
-      clearActiveLines(cm);
-      delete cm.state.activeLines;
     }
   });
 
   function clearActiveLines(cm) {
     for (var i = 0; i < cm.state.activeLines.length; i++) {
       cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
       cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
       cm.removeLineClass(cm.state.activeLines[i], "gutter", GUTT_CLASS);
@@ -47,17 +43,19 @@
       if (a[i] != b[i]) return false;
     return true;
   }
 
   function updateActiveLines(cm, ranges) {
     var active = [];
     for (var i = 0; i < ranges.length; i++) {
       var range = ranges[i];
-      if (!range.empty()) continue;
+      var option = cm.getOption("styleActiveLine");
+      if (typeof option == "object" && option.nonEmpty ? range.anchor.line != range.head.line : !range.empty())
+        continue
       var line = cm.getLineHandleVisualStart(range.head.line);
       if (active[active.length - 1] != line) active.push(line);
     }
     if (sameArray(cm.state.activeLines, active)) return;
     cm.operation(function() {
       clearActiveLines(cm);
       for (var i = 0; i < active.length; i++) {
         cm.addLineClass(active[i], "wrap", WRAP_CLASS);
--- a/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
+++ b/devtools/client/sourceeditor/codemirror/addon/selection/mark-selection.js
@@ -29,21 +29,22 @@
       cm.off("cursorActivity", onCursorActivity);
       cm.off("change", onChange);
       clear(cm);
       cm.state.markedSelection = cm.state.markedSelectionStyle = null;
     }
   });
 
   function onCursorActivity(cm) {
-    cm.operation(function() { update(cm); });
+    if (cm.state.markedSelection)
+      cm.operation(function() { update(cm); });
   }
 
   function onChange(cm) {
-    if (cm.state.markedSelection.length)
+    if (cm.state.markedSelection && cm.state.markedSelection.length)
       cm.operation(function() { clear(cm); });
   }
 
   var CHUNK_SIZE = 8;
   var Pos = CodeMirror.Pos;
   var cmp = CodeMirror.cmpPos;
 
   function coverRange(cm, from, to, addAt) {
--- a/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
+++ b/devtools/client/sourceeditor/codemirror/codemirror.bundle.js
@@ -38,17 +38,17 @@ var CodeMirror =
 /******/ 	__webpack_require__.p = "";
 
 /******/ 	// Load entry module and return exports
 /******/ 	return __webpack_require__(0);
 /******/ })
 /************************************************************************/
 /******/ ([
 /* 0 */
-/***/ function(module, exports, __webpack_require__) {
+/***/ (function(module, exports, __webpack_require__) {
 
 	__webpack_require__(1);
 	__webpack_require__(3);
 	__webpack_require__(4);
 	__webpack_require__(5);
 	__webpack_require__(6);
 	__webpack_require__(7);
 	__webpack_require__(8);
@@ -68,19 +68,19 @@ var CodeMirror =
 	__webpack_require__(22);
 	__webpack_require__(23);
 	__webpack_require__(24);
 	__webpack_require__(25);
 	__webpack_require__(26);
 	module.exports = __webpack_require__(2);
 
 
-/***/ },
+/***/ }),
 /* 1 */
-/***/ function(module, exports, __webpack_require__) {
+/***/ (function(module, exports, __webpack_require__) {
 
 	// CodeMirror, copyright (c) by Marijn Haverbeke and others
 	// Distributed under an MIT license: http://codemirror.net/LICENSE
 
 	// Open simple dialogs on top of an editor. Relies on dialog.css.
 
 	(function(mod) {
 	  if (true) // CommonJS
@@ -231,9142 +231,9807 @@ var CodeMirror =
 	    if (duration)
 	      doneTimer = setTimeout(close, duration);
 
 	    return close;
 	  });
 	});
 
 
-/***/ },
+/***/ }),
 /* 2 */
-/***/ function(module, exports, __webpack_require__) {
+/***/ (function(module, exports, __webpack_require__) {
 
 	// CodeMirror, copyright (c) by Marijn Haverbeke and others
 	// Distributed under an MIT license: http://codemirror.net/LICENSE
 
 	// This is CodeMirror (http://codemirror.net), a code editor
 	// implemented in JavaScript on top of the browser's DOM.
 	//
 	// You can find some technical background for some of the code below
 	// at http://marijnhaverbeke.nl/blog/#cm-internals .
 
-	(function(mod) {
-	  if (true) // CommonJS
-	    module.exports = mod();
-	  else if (typeof define == "function" && define.amd) // AMD
-	    return define([], mod);
-	  else // Plain browser env
-	    (this || window).CodeMirror = mod();
-	})(function() {
-	  "use strict";
-
-	  // BROWSER SNIFFING
-
-	  // Kludges for bugs and behavior differences that can't be feature
-	  // detected are enabled based on userAgent etc sniffing.
-	  var userAgent = navigator.userAgent;
-	  var platform = navigator.platform;
-
-	  var gecko = /gecko\/\d/i.test(userAgent);
-	  var ie_upto10 = /MSIE \d/.test(userAgent);
-	  var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent);
-	  var ie = ie_upto10 || ie_11up;
-	  var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]);
-	  var webkit = /WebKit\//.test(userAgent);
-	  var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent);
-	  var chrome = /Chrome\//.test(userAgent);
-	  var presto = /Opera\//.test(userAgent);
-	  var safari = /Apple Computer/.test(navigator.vendor);
-	  var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent);
-	  var phantom = /PhantomJS/.test(userAgent);
-
-	  var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent);
-	  // This is woefully incomplete. Suggestions for alternative methods welcome.
-	  var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent);
-	  var mac = ios || /Mac/.test(platform);
-	  var chromeOS = /\bCrOS\b/.test(userAgent);
-	  var windows = /win/i.test(platform);
-
-	  var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/);
-	  if (presto_version) presto_version = Number(presto_version[1]);
-	  if (presto_version && presto_version >= 15) { presto = false; webkit = true; }
-	  // Some browsers use the wrong event properties to signal cmd/ctrl on OS X
-	  var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11));
-	  var captureRightClick = gecko || (ie && ie_version >= 9);
-
-	  // Optimize some code when these features are not used.
-	  var sawReadOnlySpans = false, sawCollapsedSpans = false;
-
-	  // EDITOR CONSTRUCTOR
-
-	  // A CodeMirror instance represents an editor. This is the object
-	  // that user code is usually dealing with.
-
-	  function CodeMirror(place, options) {
-	    if (!(this instanceof CodeMirror)) return new CodeMirror(place, options);
-
-	    this.options = options = options ? copyObj(options) : {};
-	    // Determine effective options based on given values and defaults.
-	    copyObj(defaults, options, false);
-	    setGuttersForLineNumbers(options);
-
-	    var doc = options.value;
-	    if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator);
-	    this.doc = doc;
-
-	    var input = new CodeMirror.inputStyles[options.inputStyle](this);
-	    var display = this.display = new Display(place, doc, input);
-	    display.wrapper.CodeMirror = this;
-	    updateGutters(this);
-	    themeChanged(this);
-	    if (options.lineWrapping)
-	      this.display.wrapper.className += " CodeMirror-wrap";
-	    if (options.autofocus && !mobile) display.input.focus();
-	    initScrollbars(this);
-
-	    this.state = {
-	      keyMaps: [],  // stores maps added by addKeyMap
-	      overlays: [], // highlighting overlays, as added by addOverlay
-	      modeGen: 0,   // bumped when mode/overlay changes, used to invalidate highlighting info
-	      overwrite: false,
-	      delayingBlurEvent: false,
-	      focused: false,
-	      suppressEdits: false, // used to disable editing during key handlers when in readOnly mode
-	      pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll
-	      selectingText: false,
-	      draggingText: false,
-	      highlight: new Delayed(), // stores highlight worker timeout
-	      keySeq: null,  // Unfinished key sequence
-	      specialChars: null
-	    };
-
-	    var cm = this;
-
-	    // Override magic textarea content restore that IE sometimes does
-	    // on our hidden textarea on reload
-	    if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20);
-
-	    registerEventHandlers(this);
-	    ensureGlobalHandlers();
-
-	    startOperation(this);
-	    this.curOp.forceUpdate = true;
-	    attachDoc(this, doc);
-
-	    if ((options.autofocus && !mobile) || cm.hasFocus())
-	      setTimeout(bind(onFocus, this), 20);
-	    else
-	      onBlur(this);
-
-	    for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt))
-	      optionHandlers[opt](this, options[opt], Init);
-	    maybeUpdateLineNumberWidth(this);
-	    if (options.finishInit) options.finishInit(this);
-	    for (var i = 0; i < initHooks.length; ++i) initHooks[i](this);
-	    endOperation(this);
-	    // Suppress optimizelegibility in Webkit, since it breaks text
-	    // measuring on line wrapping boundaries.
-	    if (webkit && options.lineWrapping &&
-	        getComputedStyle(display.lineDiv).textRendering == "optimizelegibility")
-	      display.lineDiv.style.textRendering = "auto";
-	  }
-
-	  // DISPLAY CONSTRUCTOR
-
-	  // The display handles the DOM integration, both for input reading
-	  // and content drawing. It holds references to DOM nodes and
-	  // display-related state.
-
-	  function Display(place, doc, input) {
-	    var d = this;
-	    this.input = input;
-
-	    // Covers bottom-right square when both scrollbars are present.
-	    d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler");
-	    d.scrollbarFiller.setAttribute("cm-not-content", "true");
-	    // Covers bottom of gutter when coverGutterNextToScrollbar is on
-	    // and h scrollbar is present.
-	    d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler");
-	    d.gutterFiller.setAttribute("cm-not-content", "true");
-	    // Will contain the actual code, positioned to cover the viewport.
-	    d.lineDiv = elt("div", null, "CodeMirror-code");
-	    // Elements are added to these to represent selection and cursors.
-	    d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1");
-	    d.cursorDiv = elt("div", null, "CodeMirror-cursors");
-	    // A visibility: hidden element used to find the size of things.
-	    d.measure = elt("div", null, "CodeMirror-measure");
-	    // When lines outside of the viewport are measured, they are drawn in this.
-	    d.lineMeasure = elt("div", null, "CodeMirror-measure");
-	    // Wraps everything that needs to exist inside the vertically-padded coordinate system
-	    d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
-	                      null, "position: relative; outline: none");
-	    // Moved around its parent to cover visible view.
-	    d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative");
-	    // Set to the height of the document, allowing scrolling.
-	    d.sizer = elt("div", [d.mover], "CodeMirror-sizer");
-	    d.sizerWidth = null;
-	    // Behavior of elts with overflow: auto and padding is
-	    // inconsistent across browsers. This is used to ensure the
-	    // scrollable area is big enough.
-	    d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;");
-	    // Will contain the gutters, if any.
-	    d.gutters = elt("div", null, "CodeMirror-gutters");
-	    d.lineGutter = null;
-	    // Actual scrollable element.
-	    d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll");
-	    d.scroller.setAttribute("tabIndex", "-1");
-	    // The element in which the editor lives.
-	    d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
-
-	    // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
-	    if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; }
-	    if (!webkit && !(gecko && mobile)) d.scroller.draggable = true;
-
-	    if (place) {
-	      if (place.appendChild) place.appendChild(d.wrapper);
-	      else place(d.wrapper);
-	    }
-
-	    // Current rendered range (may be bigger than the view window).
-	    d.viewFrom = d.viewTo = doc.first;
-	    d.reportedViewFrom = d.reportedViewTo = doc.first;
-	    // Information about the rendered lines.
-	    d.view = [];
-	    d.renderedView = null;
-	    // Holds info about a single rendered line when it was rendered
-	    // for measurement, while not in view.
-	    d.externalMeasured = null;
-	    // Empty space (in pixels) above the view
-	    d.viewOffset = 0;
-	    d.lastWrapHeight = d.lastWrapWidth = 0;
-	    d.updateLineNumbers = null;
-
-	    d.nativeBarWidth = d.barHeight = d.barWidth = 0;
-	    d.scrollbarsClipped = false;
-
-	    // Used to only resize the line number gutter when necessary (when
-	    // the amount of lines crosses a boundary that makes its width change)
-	    d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null;
-	    // Set to true when a non-horizontal-scrolling line widget is
-	    // added. As an optimization, line widget aligning is skipped when
-	    // this is false.
-	    d.alignWidgets = false;
-
-	    d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null;
-
-	    // Tracks the maximum line length so that the horizontal scrollbar
-	    // can be kept static when scrolling.
-	    d.maxLine = null;
-	    d.maxLineLength = 0;
-	    d.maxLineChanged = false;
-
-	    // Used for measuring wheel scrolling granularity
-	    d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null;
-
-	    // True when shift is held down.
-	    d.shift = false;
-
-	    // Used to track whether anything happened since the context menu
-	    // was opened.
-	    d.selForContextMenu = null;
-
-	    d.activeTouch = null;
-
-	    input.init(d);
-	  }
-
-	  // 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);
-	  }
-
-	  function wrappingChanged(cm) {
-	    if (cm.options.lineWrapping) {
-	      addClass(cm.display.wrapper, "CodeMirror-wrap");
-	      cm.display.sizer.style.minWidth = "";
-	      cm.display.sizerWidth = null;
+	(function (global, factory) {
+	   true ? module.exports = factory() :
+	  typeof define === 'function' && define.amd ? define(factory) :
+	  (global.CodeMirror = factory());
+	}(this, (function () { 'use strict';
+
+	// Kludges for bugs and behavior differences that can't be feature
+	// detected are enabled based on userAgent etc sniffing.
+	var userAgent = navigator.userAgent
+	var platform = navigator.platform
+
+	var gecko = /gecko\/\d/i.test(userAgent)
+	var ie_upto10 = /MSIE \d/.test(userAgent)
+	var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent)
+	var edge = /Edge\/(\d+)/.exec(userAgent)
+	var ie = ie_upto10 || ie_11up || edge
+	var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1])
+	var webkit = !edge && /WebKit\//.test(userAgent)
+	var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent)
+	var chrome = !edge && /Chrome\//.test(userAgent)
+	var presto = /Opera\//.test(userAgent)
+	var safari = /Apple Computer/.test(navigator.vendor)
+	var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent)
+	var phantom = /PhantomJS/.test(userAgent)
+
+	var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent)
+	var android = /Android/.test(userAgent)
+	// This is woefully incomplete. Suggestions for alternative methods welcome.
+	var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent)
+	var mac = ios || /Mac/.test(platform)
+	var chromeOS = /\bCrOS\b/.test(userAgent)
+	var windows = /win/i.test(platform)
+
+	var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/)
+	if (presto_version) { presto_version = Number(presto_version[1]) }
+	if (presto_version && presto_version >= 15) { presto = false; webkit = true }
+	// Some browsers use the wrong event properties to signal cmd/ctrl on OS X
+	var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11))
+	var captureRightClick = gecko || (ie && ie_version >= 9)
+
+	function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
+
+	var rmClass = function(node, cls) {
+	  var current = node.className
+	  var match = classTest(cls).exec(current)
+	  if (match) {
+	    var after = current.slice(match.index + match[0].length)
+	    node.className = current.slice(0, match.index) + (after ? match[1] + after : "")
+	  }
+	}
+
+	function removeChildren(e) {
+	  for (var count = e.childNodes.length; count > 0; --count)
+	    { e.removeChild(e.firstChild) }
+	  return e
+	}
+
+	function removeChildrenAndAdd(parent, e) {
+	  return removeChildren(parent).appendChild(e)
+	}
+
+	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") { e.appendChild(document.createTextNode(content)) }
+	  else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]) } }
+	  return e
+	}
+	// wrapper for elt, which removes the elt from the accessibility tree
+	function eltP(tag, content, className, style) {
+	  var e = elt(tag, content, className, style)
+	  e.setAttribute("role", "presentation")
+	  return e
+	}
+
+	var range
+	if (document.createRange) { range = function(node, start, end, endNode) {
+	  var r = document.createRange()
+	  r.setEnd(endNode || node, end)
+	  r.setStart(node, start)
+	  return r
+	} }
+	else { range = function(node, start, end) {
+	  var r = document.body.createTextRange()
+	  try { r.moveToElementText(node.parentNode) }
+	  catch(e) { return r }
+	  r.collapse(true)
+	  r.moveEnd("character", end)
+	  r.moveStart("character", start)
+	  return r
+	} }
+
+	function contains(parent, child) {
+	  if (child.nodeType == 3) // Android browser always returns false when child is a textnode
+	    { child = child.parentNode }
+	  if (parent.contains)
+	    { return parent.contains(child) }
+	  do {
+	    if (child.nodeType == 11) { child = child.host }
+	    if (child == parent) { return true }
+	  } while (child = child.parentNode)
+	}
+
+	function activeElt() {
+	  // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement.
+	  // IE < 10 will throw when accessed while the page is loading or in an iframe.
+	  // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable.
+	  var activeElement
+	  try {
+	    activeElement = document.activeElement
+	  } catch(e) {
+	    activeElement = document.body || null
+	  }
+	  while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
+	    { activeElement = activeElement.shadowRoot.activeElement }
+	  return activeElement
+	}
+
+	function addClass(node, cls) {
+	  var current = node.className
+	  if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls }
+	}
+	function joinClasses(a, b) {
+	  var as = a.split(" ")
+	  for (var i = 0; i < as.length; i++)
+	    { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i] } }
+	  return b
+	}
+
+	var selectInput = function(node) { node.select() }
+	if (ios) // Mobile Safari apparently has a bug where select() is broken.
+	  { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length } }
+	else if (ie) // Suppress mysterious IE10 errors
+	  { selectInput = function(node) { try { node.select() } catch(_e) {} } }
+
+	function bind(f) {
+	  var args = Array.prototype.slice.call(arguments, 1)
+	  return function(){return f.apply(null, args)}
+	}
+
+	function copyObj(obj, target, overwrite) {
+	  if (!target) { target = {} }
+	  for (var prop in obj)
+	    { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
+	      { target[prop] = obj[prop] } }
+	  return target
+	}
+
+	// Counts the column offset in a string, taking tabs into account.
+	// Used mostly to find indentation.
+	function countColumn(string, end, tabSize, startIndex, startValue) {
+	  if (end == null) {
+	    end = string.search(/[^\s\u00a0]/)
+	    if (end == -1) { end = string.length }
+	  }
+	  for (var i = startIndex || 0, n = startValue || 0;;) {
+	    var nextTab = string.indexOf("\t", i)
+	    if (nextTab < 0 || nextTab >= end)
+	      { return n + (end - i) }
+	    n += nextTab - i
+	    n += tabSize - (n % tabSize)
+	    i = nextTab + 1
+	  }
+	}
+
+	var Delayed = function() {this.id = null};
+	Delayed.prototype.set = function (ms, f) {
+	  clearTimeout(this.id)
+	  this.id = setTimeout(f, ms)
+	};
+
+	function indexOf(array, elt) {
+	  for (var i = 0; i < array.length; ++i)
+	    { if (array[i] == elt) { return i } }
+	  return -1
+	}
+
+	// Number of pixels added to scroller and sizer to hide scrollbar
+	var scrollerGap = 30
+
+	// Returned or thrown by various protocols to signal 'I'm not
+	// handling this'.
+	var Pass = {toString: function(){return "CodeMirror.Pass"}}
+
+	// Reused option objects for setSelection & friends
+	var sel_dontScroll = {scroll: false};
+	var sel_mouse = {origin: "*mouse"};
+	var sel_move = {origin: "+move"};
+	// The inverse of countColumn -- find the offset that corresponds to
+	// a particular column.
+	function findColumn(string, goal, tabSize) {
+	  for (var pos = 0, col = 0;;) {
+	    var nextTab = string.indexOf("\t", pos)
+	    if (nextTab == -1) { nextTab = string.length }
+	    var skipped = nextTab - pos
+	    if (nextTab == string.length || col + skipped >= goal)
+	      { return pos + Math.min(skipped, goal - col) }
+	    col += nextTab - pos
+	    col += tabSize - (col % tabSize)
+	    pos = nextTab + 1
+	    if (col >= goal) { return pos }
+	  }
+	}
+
+	var spaceStrs = [""]
+	function spaceStr(n) {
+	  while (spaceStrs.length <= n)
+	    { spaceStrs.push(lst(spaceStrs) + " ") }
+	  return spaceStrs[n]
+	}
+
+	function lst(arr) { return arr[arr.length-1] }
+
+	function map(array, f) {
+	  var out = []
+	  for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i) }
+	  return out
+	}
+
+	function insertSorted(array, value, score) {
+	  var pos = 0, priority = score(value)
+	  while (pos < array.length && score(array[pos]) <= priority) { pos++ }
+	  array.splice(pos, 0, value)
+	}
+
+	function nothing() {}
+
+	function createObj(base, props) {
+	  var inst
+	  if (Object.create) {
+	    inst = Object.create(base)
+	  } else {
+	    nothing.prototype = base
+	    inst = new nothing()
+	  }
+	  if (props) { copyObj(props, inst) }
+	  return inst
+	}
+
+	var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/
+	function isWordCharBasic(ch) {
+	  return /\w/.test(ch) || ch > "\x80" &&
+	    (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch))
+	}
+	function isWordChar(ch, helper) {
+	  if (!helper) { return isWordCharBasic(ch) }
+	  if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true }
+	  return helper.test(ch)
+	}
+
+	function isEmpty(obj) {
+	  for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } }
+	  return true
+	}
+
+	// Extending unicode characters. A series of a non-extending char +
+	// any number of extending chars is treated as a single unit as far
+	// as editing and measuring is concerned. This is not fully correct,
+	// since some scripts/fonts/browsers also treat other configurations
+	// of code points as a group.
+	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) }
+
+	// Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range.
+	function skipExtendingChars(str, pos, dir) {
+	  while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir }
+	  return pos
+	}
+
+	// Returns the value from the range [`from`; `to`] that satisfies
+	// `pred` and is closest to `from`. Assumes that at least `to` satisfies `pred`.
+	function findFirst(pred, from, to) {
+	  for (;;) {
+	    if (Math.abs(from - to) <= 1) { return pred(from) ? from : to }
+	    var mid = Math.floor((from + to) / 2)
+	    if (pred(mid)) { to = mid }
+	    else { from = mid }
+	  }
+	}
+
+	// The display handles the DOM integration, both for input reading
+	// and content drawing. It holds references to DOM nodes and
+	// display-related state.
+
+	function Display(place, doc, input) {
+	  var d = this
+	  this.input = input
+
+	  // Covers bottom-right square when both scrollbars are present.
+	  d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler")
+	  d.scrollbarFiller.setAttribute("cm-not-content", "true")
+	  // Covers bottom of gutter when coverGutterNextToScrollbar is on
+	  // and h scrollbar is present.
+	  d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler")
+	  d.gutterFiller.setAttribute("cm-not-content", "true")
+	  // Will contain the actual code, positioned to cover the viewport.
+	  d.lineDiv = eltP("div", null, "CodeMirror-code")
+	  // Elements are added to these to represent selection and cursors.
+	  d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1")
+	  d.cursorDiv = elt("div", null, "CodeMirror-cursors")
+	  // A visibility: hidden element used to find the size of things.
+	  d.measure = elt("div", null, "CodeMirror-measure")
+	  // When lines outside of the viewport are measured, they are drawn in this.
+	  d.lineMeasure = elt("div", null, "CodeMirror-measure")
+	  // Wraps everything that needs to exist inside the vertically-padded coordinate system
+	  d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv],
+	                    null, "position: relative; outline: none")
+	  var lines = eltP("div", [d.lineSpace], "CodeMirror-lines")
+	  // Moved around its parent to cover visible view.
+	  d.mover = elt("div", [lines], null, "position: relative")
+	  // Set to the height of the document, allowing scrolling.
+	  d.sizer = elt("div", [d.mover], "CodeMirror-sizer")
+	  d.sizerWidth = null
+	  // Behavior of elts with overflow: auto and padding is
+	  // inconsistent across browsers. This is used to ensure the
+	  // scrollable area is big enough.
+	  d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;")
+	  // Will contain the gutters, if any.
+	  d.gutters = elt("div", null, "CodeMirror-gutters")
+	  d.lineGutter = null
+	  // Actual scrollable element.
+	  d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll")
+	  d.scroller.setAttribute("tabIndex", "-1")
+	  // The element in which the editor lives.
+	  d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror")
+
+	  // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported)
+	  if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0 }
+	  if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true }
+
+	  if (place) {
+	    if (place.appendChild) { place.appendChild(d.wrapper) }
+	    else { place(d.wrapper) }
+	  }
+
+	  // Current rendered range (may be bigger than the view window).
+	  d.viewFrom = d.viewTo = doc.first
+	  d.reportedViewFrom = d.reportedViewTo = doc.first
+	  // Information about the rendered lines.
+	  d.view = []
+	  d.renderedView = null
+	  // Holds info about a single rendered line when it was rendered
+	  // for measurement, while not in view.
+	  d.externalMeasured = null
+	  // Empty space (in pixels) above the view
+	  d.viewOffset = 0
+	  d.lastWrapHeight = d.lastWrapWidth = 0
+	  d.updateLineNumbers = null
+
+	  d.nativeBarWidth = d.barHeight = d.barWidth = 0
+	  d.scrollbarsClipped = false
+
+	  // Used to only resize the line number gutter when necessary (when
+	  // the amount of lines crosses a boundary that makes its width change)
+	  d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null
+	  // Set to true when a non-horizontal-scrolling line widget is
+	  // added. As an optimization, line widget aligning is skipped when
+	  // this is false.
+	  d.alignWidgets = false
+
+	  d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null
+
+	  // Tracks the maximum line length so that the horizontal scrollbar
+	  // can be kept static when scrolling.
+	  d.maxLine = null
+	  d.maxLineLength = 0
+	  d.maxLineChanged = false
+
+	  // Used for measuring wheel scrolling granularity
+	  d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null
+
+	  // True when shift is held down.
+	  d.shift = false
+
+	  // Used to track whether anything happened since the context menu
+	  // was opened.
+	  d.selForContextMenu = null
+
+	  d.activeTouch = null
+
+	  input.init(d)
+	}
+
+	// Find the line object corresponding to the given line number.
+	function getLine(doc, n) {
+	  n -= doc.first
+	  if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") }
+	  var chunk = doc
+	  while (!chunk.lines) {
+	    for (var i = 0;; ++i) {
+	      var child = chunk.children[i], sz = child.chunkSize()
+	      if (n < sz) { chunk = child; break }
+	      n -= sz
+	    }
+	  }
+	  return chunk.lines[n]
+	}
+
+	// Get the part of a document between two positions, as an array of
+	// strings.
+	function getBetween(doc, start, end) {
+	  var out = [], n = start.line
+	  doc.iter(start.line, end.line + 1, function (line) {
+	    var text = line.text
+	    if (n == end.line) { text = text.slice(0, end.ch) }
+	    if (n == start.line) { text = text.slice(start.ch) }
+	    out.push(text)
+	    ++n
+	  })
+	  return out
+	}
+	// Get the lines between from and to, as array of strings.
+	function getLines(doc, from, to) {
+	  var out = []
+	  doc.iter(from, to, function (line) { out.push(line.text) }) // iter aborts when callback returns truthy value
+	  return out
+	}
+
+	// Update the height of a line, propagating the height change
+	// upwards to parent nodes.
+	function updateLineHeight(line, height) {
+	  var diff = height - line.height
+	  if (diff) { for (var n = line; n; n = n.parent) { n.height += diff } }
+	}
+
+	// Given a line object, find its line number by walking up through
+	// its parent links.
+	function lineNo(line) {
+	  if (line.parent == null) { return null }
+	  var cur = line.parent, no = indexOf(cur.lines, line)
+	  for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) {
+	    for (var i = 0;; ++i) {
+	      if (chunk.children[i] == cur) { break }
+	      no += chunk.children[i].chunkSize()
+	    }
+	  }
+	  return no + cur.first
+	}
+
+	// Find the line at the given vertical position, using the height
+	// information in the document tree.
+	function lineAtHeight(chunk, h) {
+	  var n = chunk.first
+	  outer: do {
+	    for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) {
+	      var child = chunk.children[i$1], ch = child.height
+	      if (h < ch) { chunk = child; continue outer }
+	      h -= ch
+	      n += child.chunkSize()
+	    }
+	    return n
+	  } while (!chunk.lines)
+	  var i = 0
+	  for (; i < chunk.lines.length; ++i) {
+	    var line = chunk.lines[i], lh = line.height
+	    if (h < lh) { break }
+	    h -= lh
+	  }
+	  return n + i
+	}
+
+	function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size}
+
+	function lineNumberFor(options, i) {
+	  return String(options.lineNumberFormatter(i + options.firstLineNumber))
+	}
+
+	// A Pos instance represents a position within the text.
+	function Pos(line, ch, sticky) {
+	  if ( sticky === void 0 ) sticky = null;
+
+	  if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) }
+	  this.line = line
+	  this.ch = ch
+	  this.sticky = sticky
+	}
+
+	// Compare two positions, return 0 if they are the same, a negative
+	// number when a is less, and a positive number otherwise.
+	function cmp(a, b) { return a.line - b.line || a.ch - b.ch }
+
+	function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 }
+
+	function copyPos(x) {return Pos(x.line, x.ch)}
+	function maxPos(a, b) { return cmp(a, b) < 0 ? b : a }
+	function minPos(a, b) { return cmp(a, b) < 0 ? a : b }
+
+	// Most of the external API clips given positions to make sure they
+	// actually exist within the document.
+	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
+	  if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) }
+	  return clipToLen(pos, getLine(doc, pos.line).text.length)
+	}
+	function clipToLen(pos, linelen) {
+	  var ch = pos.ch
+	  if (ch == null || ch > linelen) { return Pos(pos.line, linelen) }
+	  else if (ch < 0) { return Pos(pos.line, 0) }
+	  else { return pos }
+	}
+	function clipPosArray(doc, array) {
+	  var out = []
+	  for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]) }
+	  return out
+	}
+
+	// Optimize some code when these features are not used.
+	var sawReadOnlySpans = false;
+	var sawCollapsedSpans = false;
+	function seeReadOnlySpans() {
+	  sawReadOnlySpans = true
+	}
+
+	function seeCollapsedSpans() {
+	  sawCollapsedSpans = true
+	}
+
+	// TEXTMARKER SPANS
+
+	function MarkedSpan(marker, from, to) {
+	  this.marker = marker
+	  this.from = from; this.to = to
+	}
+
+	// Search an array of spans for a span matching the given marker.
+	function getMarkedSpanFor(spans, marker) {
+	  if (spans) { for (var i = 0; i < spans.length; ++i) {
+	    var span = spans[i]
+	    if (span.marker == marker) { return span }
+	  } }
+	}
+	// Remove a span from an array, returning undefined if no spans are
+	// left (we don't store arrays for lines without spans).
+	function removeMarkedSpan(spans, span) {
+	  var r
+	  for (var i = 0; i < spans.length; ++i)
+	    { if (spans[i] != span) { (r || (r = [])).push(spans[i]) } }
+	  return r
+	}
+	// Add a span to a line.
+	function addMarkedSpan(line, span) {
+	  line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]
+	  span.marker.attachLine(line)
+	}
+
+	// Used for the algorithm that adjusts markers for a change in the
+	// document. These functions cut an array of spans at a given
+	// character position, returning an array of remaining chunks (or
+	// undefined if nothing remains).
+	function markedSpansBefore(old, startCh, isInsert) {
+	  var nw
+	  if (old) { for (var i = 0; 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 || 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(new MarkedSpan(marker, span.from, endsAfter ? null : span.to))
+	    }
+	  } }
+	  return nw
+	}
+	function markedSpansAfter(old, endCh, isInsert) {
+	  var nw
+	  if (old) { for (var i = 0; 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 || 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(new MarkedSpan(marker, startsBefore ? null : span.from - endCh,
+	                                            span.to == null ? null : span.to - endCh))
+	    }
+	  } }
+	  return nw
+	}
+
+	// Given a change object, compute the new set of marker spans that
+	// cover the line in which the change took place. Removes spans
+	// entirely within the change, reconnects spans belonging to the
+	// same marker that appear on both sides of the change, and cuts off
+	// spans partially within the change. Returns an array of span
+	// arrays with one element for each line in (after) the change.
+	function stretchSpansOverChange(doc, change) {
+	  if (change.full) { return null }
+	  var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans
+	  var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans
+	  if (!oldFirst && !oldLast) { return null }
+
+	  var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0
+	  // Get the spans that 'stick out' on both sides
+	  var first = markedSpansBefore(oldFirst, startCh, isInsert)
+	  var last = markedSpansAfter(oldLast, endCh, isInsert)
+
+	  // Next, merge those two ends
+	  var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0)
+	  if (first) {
+	    // Fix up .to properties of first
+	    for (var i = 0; i < first.length; ++i) {
+	      var span = first[i]
+	      if (span.to == null) {
+	        var found = getMarkedSpanFor(last, span.marker)
+	        if (!found) { span.to = startCh }
+	        else if (sameLine) { span.to = found.to == null ? null : found.to + offset }
+	      }
+	    }
+	  }
+	  if (last) {
+	    // Fix up .from in last (or move them into first in case of sameLine)
+	    for (var i$1 = 0; i$1 < last.length; ++i$1) {
+	      var span$1 = last[i$1]
+	      if (span$1.to != null) { span$1.to += offset }
+	      if (span$1.from == null) {
+	        var found$1 = getMarkedSpanFor(first, span$1.marker)
+	        if (!found$1) {
+	          span$1.from = offset
+	          if (sameLine) { (first || (first = [])).push(span$1) }
+	        }
+	      } else {
+	        span$1.from += offset
+	        if (sameLine) { (first || (first = [])).push(span$1) }
+	      }
+	    }
+	  }
+	  // 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$2 = 0; i$2 < first.length; ++i$2)
+	        { if (first[i$2].to == null)
+	          { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)) } } }
+	    for (var i$3 = 0; i$3 < gap; ++i$3)
+	      { newMarkers.push(gapMarkers) }
+	    newMarkers.push(last)
+	  }
+	  return newMarkers
+	}
+
+	// Remove spans that are empty and don't have a clearWhenEmpty
+	// option of false.
+	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
+	}
+
+	// Used to 'clip' out readOnly ranges when making a change.
+	function removeReadOnlyRanges(doc, from, to) {
+	  var markers = null
+	  doc.iter(from.line, to.line + 1, function (line) {
+	    if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) {
+	      var mark = line.markedSpans[i].marker
+	      if (mark.readOnly && (!markers || indexOf(markers, mark) == -1))
+	        { (markers || (markers = [])).push(mark) }
+	    } }
+	  })
+	  if (!markers) { return null }
+	  var parts = [{from: from, to: to}]
+	  for (var i = 0; i < markers.length; ++i) {
+	    var mk = markers[i], m = mk.find(0)
+	    for (var j = 0; j < parts.length; ++j) {
+	      var p = parts[j]
+	      if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue }
+	      var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to)
+	      if (dfrom < 0 || !mk.inclusiveLeft && !dfrom)
+	        { newParts.push({from: p.from, to: m.from}) }
+	      if (dto > 0 || !mk.inclusiveRight && !dto)
+	        { newParts.push({from: m.to, to: p.to}) }
+	      parts.splice.apply(parts, newParts)
+	      j += newParts.length - 3
+	    }
+	  }
+	  return parts
+	}
+
+	// Connect or disconnect spans from a line.
+	function detachMarkedSpans(line) {
+	  var spans = line.markedSpans
+	  if (!spans) { return }
+	  for (var i = 0; i < spans.length; ++i)
+	    { spans[i].marker.detachLine(line) }
+	  line.markedSpans = null
+	}
+	function attachMarkedSpans(line, spans) {
+	  if (!spans) { return }
+	  for (var i = 0; i < spans.length; ++i)
+	    { spans[i].marker.attachLine(line) }
+	  line.markedSpans = spans
+	}
+
+	// Helpers used when computing which overlapping collapsed span
+	// counts as the larger one.
+	function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 }
+	function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 }
+
+	// Returns a number indicating which of two overlapping collapsed
+	// spans is larger (and thus includes the other). Falls back to
+	// comparing ids when the spans cover exactly the same range.
+	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
+	}
+
+	// Find out whether a line ends or starts in a collapsed span. If
+	// so, return the marker for that span.
+	function collapsedSpanAtSide(line, start) {
+	  var sps = sawCollapsedSpans && line.markedSpans, found
+	  if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) {
+	    sp = sps[i]
+	    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 collapsedSpanAtSide(line, true) }
+	function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) }
+
+	// Test whether there exists a collapsed span that partially
+	// overlaps (covers the start or end, but not both) of a new span.
+	// Such overlap is not allowed.
+	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(0)
+	    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 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) ||
+	        fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0))
+	      { return true }
+	  } }
+	}
+
+	// A visual line is a line as drawn on the screen. Folding, for
+	// example, can cause multiple logical lines to appear on the same
+	// visual line. This finds the start of the visual line that the
+	// given line is part of (usually that is the line itself).
+	function visualLine(line) {
+	  var merged
+	  while (merged = collapsedSpanAtStart(line))
+	    { line = merged.find(-1, true).line }
+	  return line
+	}
+
+	function visualLineEnd(line) {
+	  var merged
+	  while (merged = collapsedSpanAtEnd(line))
+	    { line = merged.find(1, true).line }
+	  return line
+	}
+
+	// Returns an array of logical lines that continue the visual line
+	// started by the argument, or undefined if there are no such lines.
+	function visualLineContinued(line) {
+	  var merged, lines
+	  while (merged = collapsedSpanAtEnd(line)) {
+	    line = merged.find(1, true).line
+	    ;(lines || (lines = [])).push(line)
+	  }
+	  return lines
+	}
+
+	// Get the line number of the start of the visual line that the
+	// given line number is part of.
+	function visualLineNo(doc, lineN) {
+	  var line = getLine(doc, lineN), vis = visualLine(line)
+	  if (line == vis) { return lineN }
+	  return lineNo(vis)
+	}
+
+	// Get the line number of the start of the next visual line after
+	// the given line.
+	function visualLineEndNo(doc, lineN) {
+	  if (lineN > doc.lastLine()) { return lineN }
+	  var line = getLine(doc, lineN), merged
+	  if (!lineIsHidden(doc, line)) { return lineN }
+	  while (merged = collapsedSpanAtEnd(line))
+	    { line = merged.find(1, true).line }
+	  return lineNo(line) + 1
+	}
+
+	// Compute whether a line is hidden. Lines count as hidden when they
+	// are part of a visual line that starts with another line, or when
+	// they are entirely covered by collapsed, non-widget span.
+	function lineIsHidden(doc, line) {
+	  var sps = sawCollapsedSpans && line.markedSpans
+	  if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) {
+	    sp = sps[i]
+	    if (!sp.marker.collapsed) { continue }
+	    if (sp.from == null) { return true }
+	    if (sp.marker.widgetNode) { continue }
+	    if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp))
+	      { return true }
+	  } }
+	}
+	function lineIsHiddenInner(doc, line, span) {
+	  if (span.to == null) {
+	    var end = span.marker.find(1, true)
+	    return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker))
+	  }
+	  if (span.marker.inclusiveRight && span.to == line.text.length)
+	    { return true }
+	  for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) {
+	    sp = line.markedSpans[i]
+	    if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to &&
+	        (sp.to == null || sp.to != span.from) &&
+	        (sp.marker.inclusiveLeft || span.marker.inclusiveRight) &&
+	        lineIsHiddenInner(doc, line, sp)) { return true }
+	  }
+	}
+
+	// Find the height above the given line.
+	function heightAtLine(lineObj) {
+	  lineObj = visualLine(lineObj)
+
+	  var h = 0, chunk = lineObj.parent
+	  for (var i = 0; i < chunk.lines.length; ++i) {
+	    var line = chunk.lines[i]
+	    if (line == lineObj) { break }
+	    else { h += line.height }
+	  }
+	  for (var p = chunk.parent; p; chunk = p, p = chunk.parent) {
+	    for (var i$1 = 0; i$1 < p.children.length; ++i$1) {
+	      var cur = p.children[i$1]
+	      if (cur == chunk) { break }
+	      else { h += cur.height }
+	    }
+	  }
+	  return h
+	}
+
+	// Compute the character length of a line, taking into account
+	// collapsed ranges (see markText) that might hide parts, and join
+	// other lines onto it.
+	function lineLength(line) {
+	  if (line.height == 0) { return 0 }
+	  var len = line.text.length, merged, cur = line
+	  while (merged = collapsedSpanAtStart(cur)) {
+	    var found = merged.find(0, true)
+	    cur = found.from.line
+	    len += found.from.ch - found.to.ch
+	  }
+	  cur = line
+	  while (merged = collapsedSpanAtEnd(cur)) {
+	    var found$1 = merged.find(0, true)
+	    len -= cur.text.length - found$1.from.ch
+	    cur = found$1.to.line
+	    len += cur.text.length - found$1.to.ch
+	  }
+	  return len
+	}
+
+	// Find the longest line in the document.
+	function findMaxLine(cm) {
+	  var d = cm.display, doc = cm.doc
+	  d.maxLine = getLine(doc, doc.first)
+	  d.maxLineLength = lineLength(d.maxLine)
+	  d.maxLineChanged = true
+	  doc.iter(function (line) {
+	    var len = lineLength(line)
+	    if (len > d.maxLineLength) {
+	      d.maxLineLength = len
+	      d.maxLine = line
+	    }
+	  })
+	}
+
+	// BIDI HELPERS
+
+	function iterateBidiSections(order, from, to, f) {
+	  if (!order) { return f(from, to, "ltr") }
+	  var found = false
+	  for (var i = 0; i < order.length; ++i) {
+	    var part = order[i]
+	    if (part.from < to && part.to > from || from == to && part.to == from) {
+	      f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr")
+	      found = true
+	    }
+	  }
+	  if (!found) { f(from, to, "ltr") }
+	}
+
+	var bidiOther = null
+	function getBidiPartAt(order, ch, sticky) {
+	  var found
+	  bidiOther = null
+	  for (var i = 0; i < order.length; ++i) {
+	    var cur = order[i]
+	    if (cur.from < ch && cur.to > ch) { return i }
+	    if (cur.to == ch) {
+	      if (cur.from != cur.to && sticky == "before") { found = i }
+	      else { bidiOther = i }
+	    }
+	    if (cur.from == ch) {
+	      if (cur.from != cur.to && sticky != "before") { found = i }
+	      else { bidiOther = i }
+	    }
+	  }
+	  return found != null ? found : bidiOther
+	}
+
+	// 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:
+	// L (L):   Left-to-Right
+	// R (R):   Right-to-Left
+	// r (AL):  Right-to-Left Arabic
+	// 1 (EN):  European Number
+	// + (ES):  European Number Separator
+	// % (ET):  European Number Terminator
+	// n (AN):  Arabic Number
+	// , (CS):  Common Number Separator
+	// m (NSM): Non-Spacing Mark
+	// b (BN):  Boundary Neutral
+	// s (B):   Paragraph Separator
+	// t (S):   Segment Separator
+	// w (WS):  Whitespace
+	// N (ON):  Other Neutrals
+
+	// Returns null if characters are ordered as they appear
+	// (left-to-right), or an array of sections ({from, to, level}
+	// objects) in the order in which they occur visually.
+	var bidiOrdering = (function() {
+	  // Character types for codepoints 0 to 0xff
+	  var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"
+	  // Character types for codepoints 0x600 to 0x6f9
+	  var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"
+	  function charType(code) {
+	    if (code <= 0xf7) { return lowTypes.charAt(code) }
+	    else if (0x590 <= code && code <= 0x5f4) { return "R" }
+	    else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) }
+	    else if (0x6ee <= code && code <= 0x8ac) { return "r" }
+	    else if (0x2000 <= code && code <= 0x200b) { return "w" }
+	    else if (code == 0x200c) { return "b" }
+	    else { return "L" }
+	  }
+
+	  var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/
+	  var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/
+
+	  function BidiSpan(level, from, to) {
+	    this.level = level
+	    this.from = from; this.to = to
+	  }
+
+	  return function(str, direction) {
+	    var outerType = direction == "ltr" ? "L" : "R"
+
+	    if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false }
+	    var len = str.length, types = []
+	    for (var i = 0; i < len; ++i)
+	      { types.push(charType(str.charCodeAt(i))) }
+
+	    // W1. Examine each non-spacing mark (NSM) in the level run, and
+	    // change the type of the NSM to the type of the previous
+	    // character. If the NSM is at the start of the level run, it will
+	    // get the type of sor.
+	    for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) {
+	      var type = types[i$1]
+	      if (type == "m") { types[i$1] = prev }
+	      else { prev = type }
+	    }
+
+	    // W2. Search backwards from each instance of a European number
+	    // until the first strong type (R, L, AL, or sor) is found. If an
+	    // AL is found, change the type of the European number to Arabic
+	    // number.
+	    // W3. Change all ALs to R.
+	    for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) {
+	      var type$1 = types[i$2]
+	      if (type$1 == "1" && cur == "r") { types[i$2] = "n" }
+	      else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R" } }
+	    }
+
+	    // W4. A single European separator between two European numbers
+	    // changes to a European number. A single common separator between
+	    // two numbers of the same type changes to that type.
+	    for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) {
+	      var type$2 = types[i$3]
+	      if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1" }
+	      else if (type$2 == "," && prev$1 == types[i$3+1] &&
+	               (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1 }
+	      prev$1 = type$2
+	    }
+
+	    // W5. A sequence of European terminators adjacent to European
+	    // numbers changes to all European numbers.
+	    // W6. Otherwise, separators and terminators change to Other
+	    // Neutral.
+	    for (var i$4 = 0; i$4 < len; ++i$4) {
+	      var type$3 = types[i$4]
+	      if (type$3 == ",") { types[i$4] = "N" }
+	      else if (type$3 == "%") {
+	        var end = (void 0)
+	        for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {}
+	        var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"
+	        for (var j = i$4; j < end; ++j) { types[j] = replace }
+	        i$4 = 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.
+	    for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) {
+	      var type$4 = types[i$5]
+	      if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L" }
+	      else if (isStrong.test(type$4)) { cur$1 = type$4 }
+	    }
+
+	    // N1. A sequence of neutrals takes the direction of the
+	    // surrounding strong text if the text on both sides has the same
+	    // 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$6 = 0; i$6 < len; ++i$6) {
+	      if (isNeutral.test(types[i$6])) {
+	        var end$1 = (void 0)
+	        for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {}
+	        var before = (i$6 ? types[i$6-1] : outerType) == "L"
+	        var after = (end$1 < len ? types[end$1] : outerType) == "L"
+	        var replace$1 = before == after ? (before ? "L" : "R") : outerType
+	        for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1 }
+	        i$6 = end$1 - 1
+	      }
+	    }
+
+	    // Here we depart from the documented algorithm, in order to avoid
+	    // building up an actual levels array. Since there are only three
+	    // levels (0, 1, 2) in an implementation that doesn't take
+	    // explicit embedding into account, we can build up the order on
+	    // the fly, without following the level-based algorithm.
+	    var order = [], m
+	    for (var i$7 = 0; i$7 < len;) {
+	      if (countsAsLeft.test(types[i$7])) {
+	        var start = i$7
+	        for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {}
+	        order.push(new BidiSpan(0, start, i$7))
+	      } else {
+	        var pos = i$7, at = order.length
+	        for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {}
+	        for (var j$2 = pos; j$2 < i$7;) {
+	          if (countsAsNum.test(types[j$2])) {
+	            if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)) }
+	            var nstart = j$2
+	            for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {}
+	            order.splice(at, 0, new BidiSpan(2, nstart, j$2))
+	            pos = j$2
+	          } else { ++j$2 }
+	        }
+	        if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)) }
+	      }
+	    }
+	    if (order[0].level == 1 && (m = str.match(/^\s+/))) {
+	      order[0].from = m[0].length
+	      order.unshift(new BidiSpan(0, 0, m[0].length))
+	    }
+	    if (lst(order).level == 1 && (m = str.match(/\s+$/))) {
+	      lst(order).to -= m[0].length
+	      order.push(new BidiSpan(0, len - m[0].length, len))
+	    }
+
+	    return direction == "rtl" ? order.reverse() : order
+	  }
+	})()
+
+	// Get the bidi ordering for the given line (and cache it). Returns
+	// false for lines that are fully left-to-right, and an array of
+	// BidiSpan objects otherwise.
+	function getOrder(line, direction) {
+	  var order = line.order
+	  if (order == null) { order = line.order = bidiOrdering(line.text, direction) }
+	  return order
+	}
+
+	function moveCharLogically(line, ch, dir) {
+	  var target = skipExtendingChars(line.text, ch + dir, dir)
+	  return target < 0 || target > line.text.length ? null : target
+	}
+
+	function moveLogically(line, start, dir) {
+	  var ch = moveCharLogically(line, start.ch, dir)
+	  return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before")
+	}
+
+	function endOfLine(visually, cm, lineObj, lineNo, dir) {
+	  if (visually) {
+	    var order = getOrder(lineObj, cm.doc.direction)
+	    if (order) {
+	      var part = dir < 0 ? lst(order) : order[0]
+	      var moveInStorageOrder = (dir < 0) == (part.level == 1)
+	      var sticky = moveInStorageOrder ? "after" : "before"
+	      var ch
+	      // With a wrapped rtl chunk (possibly spanning multiple bidi parts),
+	      // it could be that the last bidi part is not on the last visual line,
+	      // since visual lines contain content order-consecutive chunks.
+	      // Thus, in rtl, we are looking for the first (content-order) character
+	      // in the rtl chunk that is on the last line (that is, the same line
+	      // as the last (content-order) character).
+	      if (part.level > 0) {
+	        var prep = prepareMeasureForLine(cm, lineObj)
+	        ch = dir < 0 ? lineObj.text.length - 1 : 0
+	        var targetTop = measureCharPrepared(cm, prep, ch).top
+	        ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch)
+	        if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1) }
+	      } else { ch = dir < 0 ? part.to : part.from }
+	      return new Pos(lineNo, ch, sticky)
+	    }
+	  }
+	  return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after")
+	}
+
+	function moveVisually(cm, line, start, dir) {
+	  var bidi = getOrder(line, cm.doc.direction)
+	  if (!bidi) { return moveLogically(line, start, dir) }
+	  if (start.ch >= line.text.length) {
+	    start.ch = line.text.length
+	    start.sticky = "before"
+	  } else if (start.ch <= 0) {
+	    start.ch = 0
+	    start.sticky = "after"
+	  }
+	  var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]
+	  if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) {
+	    // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines,
+	    // nothing interesting happens.
+	    return moveLogically(line, start, dir)
+	  }
+
+	  var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }
+	  var prep
+	  var getWrappedLineExtent = function (ch) {
+	    if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} }
+	    prep = prep || prepareMeasureForLine(cm, line)
+	    return wrappedLineExtentChar(cm, line, prep, ch)
+	  }
+	  var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch)
+
+	  if (cm.doc.direction == "rtl" || part.level == 1) {
+	    var moveInStorageOrder = (part.level == 1) == (dir < 0)
+	    var ch = mv(start, moveInStorageOrder ? 1 : -1)
+	    if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) {
+	      // Case 2: We move within an rtl part or in an rtl editor on the same visual line
+	      var sticky = moveInStorageOrder ? "before" : "after"
+	      return new Pos(start.line, ch, sticky)
+	    }
+	  }
+
+	  // Case 3: Could not move within this bidi part in this visual line, so leave
+	  // the current bidi part
+
+	  var searchInVisualLine = function (partPos, dir, wrappedLineExtent) {
+	    var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder
+	      ? new Pos(start.line, mv(ch, 1), "before")
+	      : new Pos(start.line, ch, "after"); }
+
+	    for (; partPos >= 0 && partPos < bidi.length; partPos += dir) {
+	      var part = bidi[partPos]
+	      var moveInStorageOrder = (dir > 0) == (part.level != 1)
+	      var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1)
+	      if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) }
+	      ch = moveInStorageOrder ? part.from : mv(part.to, -1)
+	      if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) }
+	    }
+	  }
+
+	  // Case 3a: Look for other bidi parts on the same visual line
+	  var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent)
+	  if (res) { return res }
+
+	  // Case 3b: Look for other bidi parts on the next visual line
+	  var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1)
+	  if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) {
+	    res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh))
+	    if (res) { return res }
+	  }
+
+	  // Case 4: Nowhere to move
+	  return null
+	}
+
+	// EVENT HANDLING
+
+	// Lightweight event framework. on/off also work on DOM nodes,
+	// registering native DOM handlers.
+
+	var noHandlers = []
+
+	var on = function(emitter, type, f) {
+	  if (emitter.addEventListener) {
+	    emitter.addEventListener(type, f, false)
+	  } else if (emitter.attachEvent) {
+	    emitter.attachEvent("on" + type, f)
+	  } else {
+	    var map = emitter._handlers || (emitter._handlers = {})
+	    map[type] = (map[type] || noHandlers).concat(f)
+	  }
+	}
+
+	function getHandlers(emitter, type) {
+	  return emitter._handlers && emitter._handlers[type] || noHandlers
+	}
+
+	function off(emitter, type, f) {
+	  if (emitter.removeEventListener) {
+	    emitter.removeEventListener(type, f, false)
+	  } else if (emitter.detachEvent) {
+	    emitter.detachEvent("on" + type, f)
+	  } else {
+	    var map = emitter._handlers, arr = map && map[type]
+	    if (arr) {
+	      var index = indexOf(arr, f)
+	      if (index > -1)
+	        { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)) }
+	    }
+	  }
+	}
+
+	function signal(emitter, type /*, values...*/) {
+	  var handlers = getHandlers(emitter, type)
+	  if (!handlers.length) { return }
+	  var args = Array.prototype.slice.call(arguments, 2)
+	  for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args) }
+	}
+
+	// The DOM events that CodeMirror handles can be overridden by
+	// registering a (non-DOM) handler on the editor for the event name,
+	// and preventDefault-ing the event in that handler.
+	function signalDOMEvent(cm, e, override) {
+	  if (typeof e == "string")
+	    { e = {type: e, preventDefault: function() { this.defaultPrevented = true }} }
+	  signal(cm, override || e.type, cm, e)
+	  return e_defaultPrevented(e) || e.codemirrorIgnore
+	}
+
+	function signalCursorActivity(cm) {
+	  var arr = cm._handlers && cm._handlers.cursorActivity
+	  if (!arr) { return }
+	  var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = [])
+	  for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1)
+	    { set.push(arr[i]) } }
+	}
+
+	function hasHandler(emitter, type) {
+	  return getHandlers(emitter, type).length > 0
+	}
+
+	// Add on and off methods to a constructor's prototype, to make
+	// registering events on such objects more convenient.
+	function eventMixin(ctor) {
+	  ctor.prototype.on = function(type, f) {on(this, type, f)}
+	  ctor.prototype.off = function(type, f) {off(this, type, f)}
+	}
+
+	// Due to the fact that we still support jurassic IE versions, some
+	// compatibility wrappers are needed.
+
+	function e_preventDefault(e) {
+	  if (e.preventDefault) { e.preventDefault() }
+	  else { e.returnValue = false }
+	}
+	function e_stopPropagation(e) {
+	  if (e.stopPropagation) { e.stopPropagation() }
+	  else { e.cancelBubble = true }
+	}
+	function e_defaultPrevented(e) {
+	  return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false
+	}
+	function e_stop(e) {e_preventDefault(e); e_stopPropagation(e)}
+
+	function e_target(e) {return e.target || e.srcElement}
+	function e_button(e) {
+	  var b = e.which
+	  if (b == null) {
+	    if (e.button & 1) { b = 1 }
+	    else if (e.button & 2) { b = 3 }
+	    else if (e.button & 4) { b = 2 }
+	  }
+	  if (mac && e.ctrlKey && b == 1) { b = 3 }
+	  return b
+	}
+
+	// Detect drag-and-drop
+	var dragAndDrop = function() {
+	  // There is *some* kind of drag-and-drop support in IE6-8, but I
+	  // couldn't get it to work yet.
+	  if (ie && ie_version < 9) { return false }
+	  var div = elt('div')
+	  return "draggable" in div || "dragDrop" in div
+	}()
+
+	var zwspSupported
+	function zeroWidthElement(measure) {
+	  if (zwspSupported == null) {
+	    var test = elt("span", "\u200b")
+	    removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")]))
+	    if (measure.firstChild.offsetHeight != 0)
+	      { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8) }
+	  }
+	  var node = zwspSupported ? elt("span", "\u200b") :
+	    elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px")
+	  node.setAttribute("cm-text", "")
+	  return node
+	}
+
+	// Feature-detect IE's crummy client rect reporting for bidi text
+	var badBidiRects
+	function hasBadBidiRects(measure) {
+	  if (badBidiRects != null) { return badBidiRects }
+	  var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA"))
+	  var r0 = range(txt, 0, 1).getBoundingClientRect()
+	  var r1 = range(txt, 1, 2).getBoundingClientRect()
+	  removeChildren(measure)
+	  if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780)
+	  return badBidiRects = (r1.right - r0.right < 3)
+	}
+
+	// See if "".split is the broken IE version, if so, provide an
+	// alternative way to split lines.
+	var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) {
+	  var pos = 0, result = [], l = string.length
+	  while (pos <= l) {
+	    var nl = string.indexOf("\n", pos)
+	    if (nl == -1) { nl = string.length }
+	    var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl)
+	    var rt = line.indexOf("\r")
+	    if (rt != -1) {
+	      result.push(line.slice(0, rt))
+	      pos += rt + 1
 	    } else {
-	      rmClass(cm.display.wrapper, "CodeMirror-wrap");
-	      findMaxLine(cm);
-	    }
-	    estimateLineHeights(cm);
-	    regChange(cm);
-	    clearCaches(cm);
-	    setTimeout(function(){updateScrollbars(cm);}, 100);
-	  }
-
-	  // Returns a function that estimates the height of a line, to use as
-	  // first approximation until the line becomes visible (and is thus
-	  // properly measurable).
-	  function estimateHeight(cm) {
-	    var th = textHeight(cm.display), wrapping = cm.options.lineWrapping;
-	    var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3);
-	    return function(line) {
-	      if (lineIsHidden(cm.doc, line)) return 0;
-
-	      var widgetsHeight = 0;
-	      if (line.widgets) for (var i = 0; i < line.widgets.length; i++) {
-	        if (line.widgets[i].height) widgetsHeight += line.widgets[i].height;
-	      }
-
-	      if (wrapping)
-	        return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th;
-	      else
-	        return widgetsHeight + th;
-	    };
-	  }
-
-	  function estimateLineHeights(cm) {
-	    var doc = cm.doc, est = estimateHeight(cm);
-	    doc.iter(function(line) {
-	      var estHeight = est(line);
-	      if (estHeight != line.height) updateLineHeight(line, estHeight);
-	    });
-	  }
-
-	  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);
-	  }
-
-	  function guttersChanged(cm) {
-	    updateGutters(cm);
-	    regChange(cm);
-	    setTimeout(function(){alignHorizontally(cm);}, 20);
-	  }
-
-	  // Rebuild the gutter elements, ensure the margin to the left of the
-	  // code matches their width.
-	  function updateGutters(cm) {
-	    var gutters = cm.display.gutters, specs = cm.options.gutters;
-	    removeChildren(gutters);
-	    for (var i = 0; i < specs.length; ++i) {
-	      var gutterClass = specs[i];
-	      var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass));
-	      if (gutterClass == "CodeMirror-linenumbers") {
-	        cm.display.lineGutter = gElt;
-	        gElt.style.width = (cm.display.lineNumWidth || 1) + "px";
-	      }
-	    }
-	    gutters.style.display = i ? "" : "none";
-	    updateGutterSpace(cm);
-	  }
-
-	  function updateGutterSpace(cm) {
-	    var width = cm.display.gutters.offsetWidth;
-	    cm.display.sizer.style.marginLeft = width + "px";
-	  }
-
-	  // Compute the character length of a line, taking into account
-	  // collapsed ranges (see markText) that might hide parts, and join
-	  // other lines onto it.
-	  function lineLength(line) {
-	    if (line.height == 0) return 0;
-	    var len = line.text.length, merged, cur = line;
-	    while (merged = collapsedSpanAtStart(cur)) {
-	      var found = merged.find(0, true);
-	      cur = found.from.line;
-	      len += found.from.ch - found.to.ch;
-	    }
-	    cur = line;
-	    while (merged = collapsedSpanAtEnd(cur)) {
-	      var found = merged.find(0, true);
-	      len -= cur.text.length - found.from.ch;
-	      cur = found.to.line;
-	      len += cur.text.length - found.to.ch;
-	    }
-	    return len;
-	  }
-
-	  // Find the longest line in the document.
-	  function findMaxLine(cm) {
-	    var d = cm.display, doc = cm.doc;
-	    d.maxLine = getLine(doc, doc.first);
-	    d.maxLineLength = lineLength(d.maxLine);
-	    d.maxLineChanged = true;
-	    doc.iter(function(line) {
-	      var len = lineLength(line);
-	      if (len > d.maxLineLength) {
-	        d.maxLineLength = len;
-	        d.maxLine = line;
-	      }
-	    });
-	  }
-
-	  // Make sure the gutters options contains the element
-	  // "CodeMirror-linenumbers" when the lineNumbers option is true.
-	  function setGuttersForLineNumbers(options) {
-	    var found = indexOf(options.gutters, "CodeMirror-linenumbers");
-	    if (found == -1 && options.lineNumbers) {
-	      options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]);
-	    } else if (found > -1 && !options.lineNumbers) {
-	      options.gutters = options.gutters.slice(0);
-	      options.gutters.splice(found, 1);
-	    }
-	  }
-
-	  // SCROLLBARS
-
-	  // Prepare DOM reads needed to update the scrollbars. Done in one
-	  // shot to minimize update/measure roundtrips.
-	  function measureForScrollbars(cm) {
-	    var d = cm.display, gutterW = d.gutters.offsetWidth;
-	    var docH = Math.round(cm.doc.height + paddingVert(cm.display));
-	    return {
-	      clientHeight: d.scroller.clientHeight,
-	      viewHeight: d.wrapper.clientHeight,
-	      scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth,
-	      viewWidth: d.wrapper.clientWidth,
-	      barLeft: cm.options.fixedGutter ? gutterW : 0,
-	      docHeight: docH,
-	      scrollHeight: docH + scrollGap(cm) + d.barHeight,
-	      nativeBarWidth: d.nativeBarWidth,
-	      gutterWidth: gutterW
-	    };
-	  }
-
-	  function NativeScrollbars(place, scroll, cm) {
-	    this.cm = cm;
-	    var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar");
-	    var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar");
-	    place(vert); place(horiz);
-
-	    on(vert, "scroll", function() {
-	      if (vert.clientHeight) scroll(vert.scrollTop, "vertical");
-	    });
-	    on(horiz, "scroll", function() {
-	      if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal");
-	    });
-
-	    this.checkedZeroWidth = false;
-	    // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
-	    if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px";
-	  }
-
-	  NativeScrollbars.prototype = copyObj({
-	    update: function(measure) {
-	      var needsH = measure.scrollWidth > measure.clientWidth + 1;
-	      var needsV = measure.scrollHeight > measure.clientHeight + 1;
-	      var sWidth = measure.nativeBarWidth;
-
-	      if (needsV) {
-	        this.vert.style.display = "block";
-	        this.vert.style.bottom = needsH ? sWidth + "px" : "0";
-	        var totalHeight = measure.viewHeight - (needsH ? sWidth : 0);
-	        // A bug in IE8 can cause this value to be negative, so guard it.
-	        this.vert.firstChild.style.height =
-	          Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px";
-	      } else {
-	        this.vert.style.display = "";
-	        this.vert.firstChild.style.height = "0";
-	      }
-
-	      if (needsH) {
-	        this.horiz.style.display = "block";
-	        this.horiz.style.right = needsV ? sWidth + "px" : "0";
-	        this.horiz.style.left = measure.barLeft + "px";
-	        var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0);
-	        this.horiz.firstChild.style.width =
-	          (measure.scrollWidth - measure.clientWidth + totalWidth) + "px";
-	      } else {
-	        this.horiz.style.display = "";
-	        this.horiz.firstChild.style.width = "0";
-	      }
-
-	      if (!this.checkedZeroWidth && measure.clientHeight > 0) {
-	        if (sWidth == 0) this.zeroWidthHack();
-	        this.checkedZeroWidth = true;
-	      }
-
-	      return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0};
-	    },
-	    setScrollLeft: function(pos) {
-	      if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos;
-	      if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz);
-	    },
-	    setScrollTop: function(pos) {
-	      if (this.vert.scrollTop != pos) this.vert.scrollTop = pos;
-	      if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert);
-	    },
-	    zeroWidthHack: function() {
-	      var w = mac && !mac_geMountainLion ? "12px" : "18px";
-	      this.horiz.style.height = this.vert.style.width = w;
-	      this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none";
-	      this.disableHoriz = new Delayed;
-	      this.disableVert = new Delayed;
-	    },
-	    enableZeroWidthBar: function(bar, delay) {
-	      bar.style.pointerEvents = "auto";
-	      function maybeDisable() {
-	        // To find out whether the scrollbar is still visible, we
-	        // check whether the element under the pixel in the bottom
-	        // left corner of the scrollbar box is the scrollbar box
-	        // itself (when the bar is still visible) or its filler child
-	        // (when the bar is hidden). If it is still visible, we keep
-	        // it enabled, if it's hidden, we disable pointer events.
-	        var box = bar.getBoundingClientRect();
-	        var elt = document.elementFromPoint(box.left + 1, box.bottom - 1);
-	        if (elt != bar) bar.style.pointerEvents = "none";
-	        else delay.set(1000, maybeDisable);
-	      }
-	      delay.set(1000, maybeDisable);
-	    },
-	    clear: function() {
-	      var parent = this.horiz.parentNode;
-	      parent.removeChild(this.horiz);
-	      parent.removeChild(this.vert);
-	    }
-	  }, NativeScrollbars.prototype);
-
-	  function NullScrollbars() {}
-
-	  NullScrollbars.prototype = copyObj({
-	    update: function() { return {bottom: 0, right: 0}; },
-	    setScrollLeft: function() {},
-	    setScrollTop: function() {},
-	    clear: function() {}
-	  }, NullScrollbars.prototype);
-
-	  CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars};
-
-	  function initScrollbars(cm) {
-	    if (cm.display.scrollbars) {
-	      cm.display.scrollbars.clear();
-	      if (cm.display.scrollbars.addClass)
-	        rmClass(cm.display.wrapper, cm.display.scrollbars.addClass);
-	    }
-
-	    cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) {
-	      cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller);
-	      // Prevent clicks in the scrollbars from killing focus
-	      on(node, "mousedown", function() {
-	        if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0);
-	      });
-	      node.setAttribute("cm-not-content", "true");
-	    }, function(pos, axis) {
-	      if (axis == "horizontal") setScrollLeft(cm, pos);
-	      else setScrollTop(cm, pos);
-	    }, cm);
-	    if (cm.display.scrollbars.addClass)
-	      addClass(cm.display.wrapper, cm.display.scrollbars.addClass);
-	  }
-
-	  function updateScrollbars(cm, measure) {
-	    if (!measure) measure = measureForScrollbars(cm);
-	    var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight;
-	    updateScrollbarsInner(cm, measure);
-	    for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) {
-	      if (startWidth != cm.display.barWidth && cm.options.lineWrapping)
-	        updateHeightsInViewport(cm);
-	      updateScrollbarsInner(cm, measureForScrollbars(cm));
-	      startWidth = cm.display.barWidth; startHeight = cm.display.barHeight;
-	    }
-	  }
-
-	  // Re-synchronize the fake scrollbars with the actual size of the
-	  // content.
-	  function updateScrollbarsInner(cm, measure) {
-	    var d = cm.display;
-	    var sizes = d.scrollbars.update(measure);
-
-	    d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px";
-	    d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px";
-	    d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"
-
-	    if (sizes.right && sizes.bottom) {
-	      d.scrollbarFiller.style.display = "block";
-	      d.scrollbarFiller.style.height = sizes.bottom + "px";
-	      d.scrollbarFiller.style.width = sizes.right + "px";
-	    } else d.scrollbarFiller.style.display = "";
-	    if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) {
-	      d.gutterFiller.style.display = "block";
-	      d.gutterFiller.style.height = sizes.bottom + "px";
-	      d.gutterFiller.style.width = measure.gutterWidth + "px";
-	    } else d.gutterFiller.style.display = "";
-	  }
-
-	  // Compute the lines that are visible in a given viewport (defaults
-	  // the the current scroll position). viewport may contain top,
-	  // height, and ensure (see op.scrollToPos) properties.
-	  function visibleLines(display, doc, viewport) {
-	    var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop;
-	    top = Math.floor(top - paddingTop(display));
-	    var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight;
-
-	    var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom);
-	    // Ensure is a {from: {line, ch}, to: {line, ch}} object, and
-	    // forces those lines into the viewport (if possible).
-	    if (viewport && viewport.ensure) {
-	      var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line;
-	      if (ensureFrom < from) {
-	        from = ensureFrom;
-	        to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight);
-	      } else if (Math.min(ensureTo, doc.lastLine()) >= to) {
-	        from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight);
-	        to = ensureTo;
-	      }
-	    }
-	    return {from: from, to: Math.max(to, from + 1)};
-	  }
-
-	  // LINE NUMBERS
-
-	  // Re-align line numbers and gutter marks to compensate for
-	  // horizontal scrolling.
-	  function alignHorizontally(cm) {
-	    var display = cm.display, view = display.view;
-	    if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return;
-	    var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft;
-	    var gutterW = display.gutters.offsetWidth, left = comp + "px";
-	    for (var i = 0; i < view.length; i++) if (!view[i].hidden) {
-	      if (cm.options.fixedGutter && view[i].gutter)
-	        view[i].gutter.style.left = left;
-	      var align = view[i].alignable;
-	      if (align) for (var j = 0; j < align.length; j++)
-	        align[j].style.left = left;
-	    }
-	    if (cm.options.fixedGutter)
-	      display.gutters.style.left = (comp + gutterW) + "px";
-	  }
-
-	  // Used to ensure that the line number gutter is still the right
-	  // size for the current document size. Returns true when an update
-	  // is needed.
-	  function maybeUpdateLineNumberWidth(cm) {
-	    if (!cm.options.lineNumbers) return false;
-	    var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display;
-	    if (last.length != display.lineNumChars) {
-	      var test = display.measure.appendChild(elt("div", [elt("div", last)],
-	                                                 "CodeMirror-linenumber CodeMirror-gutter-elt"));
-	      var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW;
-	      display.lineGutter.style.width = "";
-	      display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1;
-	      display.lineNumWidth = display.lineNumInnerWidth + padding;
-	      display.lineNumChars = display.lineNumInnerWidth ? last.length : -1;
-	      display.lineGutter.style.width = display.lineNumWidth + "px";
-	      updateGutterSpace(cm);
-	      return true;
-	    }
-	    return false;
-	  }
-
-	  function lineNumberFor(options, i) {
-	    return String(options.lineNumberFormatter(i + options.firstLineNumber));
-	  }
-
-	  // Computes display.scroller.scrollLeft + display.gutters.offsetWidth,
-	  // but using getBoundingClientRect to get a sub-pixel-accurate
-	  // result.
-	  function compensateForHScroll(display) {
-	    return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left;
-	  }
-
-	  // DISPLAY DRAWING
-
-	  function DisplayUpdate(cm, viewport, force) {
-	    var display = cm.display;
-
-	    this.viewport = viewport;
-	    // Store some values that we'll need later (but don't want to force a relayout for)
-	    this.visible = visibleLines(display, cm.doc, viewport);
-	    this.editorIsHidden = !display.wrapper.offsetWidth;
-	    this.wrapperHeight = display.wrapper.clientHeight;
-	    this.wrapperWidth = display.wrapper.clientWidth;
-	    this.oldDisplayWidth = displayWidth(cm);
-	    this.force = force;
-	    this.dims = getDimensions(cm);
-	    this.events = [];
-	  }
-
-	  DisplayUpdate.prototype.signal = function(emitter, type) {
-	    if (hasHandler(emitter, type))
-	      this.events.push(arguments);
-	  };
-	  DisplayUpdate.prototype.finish = function() {
-	    for (var i = 0; i < this.events.length; i++)
-	      signal.apply(null, this.events[i]);
-	  };
-
-	  function maybeClipScrollbars(cm) {
-	    var display = cm.display;
-	    if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
-	      display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth;
-	      display.heightForcer.style.height = scrollGap(cm) + "px";
-	      display.sizer.style.marginBottom = -display.nativeBarWidth + "px";
-	      display.sizer.style.borderRightWidth = scrollGap(cm) + "px";
-	      display.scrollbarsClipped = true;
-	    }
-	  }
-
-	  // Does the actual updating of the line display. Bails out
-	  // (returning false) when there is nothing to be done and forced is
-	  // false.
-	  function updateDisplayIfNeeded(cm, update) {
-	    var display = cm.display, doc = cm.doc;
-
-	    if (update.editorIsHidden) {
-	      resetView(cm);
-	      return false;
-	    }
-
-	    // Bail out if the visible area is already rendered and nothing changed.
-	    if (!update.force &&
-	        update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
-	        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
-	        display.renderedView == display.view && countDirtyView(cm) == 0)
-	      return false;
-
-	    if (maybeUpdateLineNumberWidth(cm)) {
-	      resetView(cm);
-	      update.dims = getDimensions(cm);
-	    }
-
-	    // Compute a suitable new viewport (from & to)
-	    var end = doc.first + doc.size;
-	    var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first);
-	    var to = Math.min(end, update.visible.to + cm.options.viewportMargin);
-	    if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom);
-	    if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo);
-	    if (sawCollapsedSpans) {
-	      from = visualLineNo(cm.doc, from);
-	      to = visualLineEndNo(cm.doc, to);
-	    }
-
-	    var different = from != display.viewFrom || to != display.viewTo ||
-	      display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth;
-	    adjustView(cm, from, to);
-
-	    display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom));
-	    // Position the mover div to align with the current scroll position
-	    cm.display.mover.style.top = display.viewOffset + "px";
-
-	    var toUpdate = countDirtyView(cm);
-	    if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
-	        (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
-	      return false;
-
-	    // For big changes, we hide the enclosing element during the
-	    // update, since that speeds up the operations on most browsers.
-	    var focused = activeElt();
-	    if (toUpdate > 4) display.lineDiv.style.display = "none";
-	    patchDisplay(cm, display.updateLineNumbers, update.dims);
-	    if (toUpdate > 4) display.lineDiv.style.display = "";
-	    display.renderedView = display.view;
-	    // There might have been a widget with a focused element that got
-	    // hidden or updated, if so re-focus it.
-	    if (focused && activeElt() != focused && focused.offsetHeight) focused.focus();
-
-	    // Prevent selection and cursors from interfering with the scroll
-	    // width and height.
-	    removeChildren(display.cursorDiv);
-	    removeChildren(display.selectionDiv);
-	    display.gutters.style.height = display.sizer.style.minHeight = 0;
-
-	    if (different) {
-	      display.lastWrapHeight = update.wrapperHeight;
-	      display.lastWrapWidth = update.wrapperWidth;
-	      startWorker(cm, 400);
-	    }
-
-	    display.updateLineNumbers = null;
-
-	    return true;
-	  }
-
-	  function postUpdateDisplay(cm, update) {
-	    var viewport = update.viewport;
-
-	    for (var first = true;; first = false) {
-	      if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
-	        // Clip forced viewport to actual scrollable area.
-	        if (viewport && viewport.top != null)
-	          viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)};
-	        // Updated line heights might result in the drawn area not
-	        // actually covering the viewport. Keep looping until it does.
-	        update.visible = visibleLines(cm.display, cm.doc, viewport);
-	        if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
-	          break;
-	      }
-	      if (!updateDisplayIfNeeded(cm, update)) break;
-	      updateHeightsInViewport(cm);
-	      var barMeasure = measureForScrollbars(cm);
-	      updateSelection(cm);
-	      updateScrollbars(cm, barMeasure);
-	      setDocumentHeight(cm, barMeasure);
-	    }
-
-	    update.signal(cm, "update", cm);
-	    if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
-	      update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo);
-	      cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo;
-	    }
-	  }
-
-	  function updateDisplaySimple(cm, viewport) {
-	    var update = new DisplayUpdate(cm, viewport);
-	    if (updateDisplayIfNeeded(cm, update)) {
-	      updateHeightsInViewport(cm);
-	      postUpdateDisplay(cm, update);
-	      var barMeasure = measureForScrollbars(cm);
-	      updateSelection(cm);
-	      updateScrollbars(cm, barMeasure);
-	      setDocumentHeight(cm, barMeasure);
-	      update.finish();
-	    }
-	  }
-
-	  function setDocumentHeight(cm, measure) {
-	    cm.display.sizer.style.minHeight = measure.docHeight + "px";
-	    cm.display.heightForcer.style.top = measure.docHeight + "px";
-	    cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px";
-	  }
-
-	  // Read the actual heights of the rendered lines, and update their
-	  // stored heights to match.
-	  function updateHeightsInViewport(cm) {
-	    var display = cm.display;
-	    var prevBottom = display.lineDiv.offsetTop;
-	    for (var i = 0; i < display.view.length; i++) {
-	      var cur = display.view[i], height;
-	      if (cur.hidden) continue;
-	      if (ie && ie_version < 8) {
-	        var bot = cur.node.offsetTop + cur.node.offsetHeight;
-	        height = bot - prevBottom;
-	        prevBottom = bot;
-	      } else {
-	        var box = cur.node.getBoundingClientRect();
-	        height = box.bottom - box.top;
-	      }
-	      var diff = cur.line.height - height;
-	      if (height < 2) height = textHeight(display);
-	      if (diff > .001 || diff < -.001) {
-	        updateLineHeight(cur.line, height);
-	        updateWidgetHeight(cur.line);
-	        if (cur.rest) for (var j = 0; j < cur.rest.length; j++)
-	          updateWidgetHeight(cur.rest[j]);
-	      }
-	    }
-	  }
-
-	  // Read and store the height of line widgets associated with the
-	  // given line.
-	  function updateWidgetHeight(line) {
-	    if (line.widgets) for (var i = 0; i < line.widgets.length; ++i)
-	      line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight;
-	  }
-
-	  // Do a bulk-read of the DOM positions and sizes needed to draw the
-	  // view, so that we don't interleave reading and writing to the DOM.
-	  function getDimensions(cm) {
-	    var d = cm.display, left = {}, width = {};
-	    var gutterLeft = d.gutters.clientLeft;
-	    for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) {
-	      left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft;
-	      width[cm.options.gutters[i]] = n.clientWidth;
-	    }
-	    return {fixedPos: compensateForHScroll(d),
-	            gutterTotalWidth: d.gutters.offsetWidth,
-	            gutterLeft: left,
-	            gutterWidth: width,
-	            wrapperWidth: d.wrapper.clientWidth};
-	  }
-
-	  // Sync the actual display DOM structure with display.view, removing
-	  // nodes for lines that are no longer in view, and creating the ones
-	  // that are not there yet, and updating the ones that are out of
-	  // date.
-	  function patchDisplay(cm, updateNumbersFrom, dims) {
-	    var display = cm.display, lineNumbers = cm.options.lineNumbers;
-	    var container = display.lineDiv, cur = container.firstChild;
-
-	    function rm(node) {
-	      var next = node.nextSibling;
-	      // Works around a throw-scroll bug in OS X Webkit
-	      if (webkit && mac && cm.display.currentWheelTarget == node)
-	        node.style.display = "none";
-	      else
-	        node.parentNode.removeChild(node);
-	      return next;
-	    }
-
-	    var view = display.view, lineN = display.viewFrom;
-	    // Loop over the elements in the view, syncing cur (the DOM nodes
-	    // in display.lineDiv) with the view as we go.
-	    for (var i = 0; i < view.length; i++) {
-	      var lineView = view[i];
-	      if (lineView.hidden) {
-	      } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
-	        var node = buildLineElement(cm, lineView, lineN, dims);
-	        container.insertBefore(node, cur);
-	      } else { // Already drawn
-	        while (cur != lineView.node) cur = rm(cur);
-	        var updateNumber = lineNumbers && updateNumbersFrom != null &&
-	          updateNumbersFrom <= lineN && lineView.lineNumber;
-	        if (lineView.changes) {
-	          if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false;
-	          updateLineForChanges(cm, lineView, lineN, dims);
-	        }
-	        if (updateNumber) {
-	          removeChildren(lineView.lineNumber);
-	          lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)));
-	        }
-	        cur = lineView.node.nextSibling;
-	      }
-	      lineN += lineView.size;
-	    }
-	    while (cur) cur = rm(cur);
-	  }
-
-	  // When an aspect of a line changes, a string is added to
-	  // lineView.changes. This updates the relevant part of the line's
-	  // DOM structure.
-	  function updateLineForChanges(cm, lineView, lineN, dims) {
-	    for (var j = 0; j < lineView.changes.length; j++) {
-	      var type = lineView.changes[j];
-	      if (type == "text") updateLineText(cm, lineView);
-	      else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims);
-	      else if (type == "class") updateLineClasses(lineView);
-	      else if (type == "widget") updateLineWidgets(cm, lineView, dims);
-	    }
-	    lineView.changes = null;
-	  }
-
-	  // Lines with gutter elements, widgets or a background class need to
-	  // be wrapped, and have the extra elements added to the wrapper div
-	  function ensureLineWrapped(lineView) {
-	    if (lineView.node == lineView.text) {
-	      lineView.node = elt("div", null, null, "position: relative");
-	      if (lineView.text.parentNode)
-	        lineView.text.parentNode.replaceChild(lineView.node, lineView.text);
-	      lineView.node.appendChild(lineView.text);
-	      if (ie && ie_version < 8) lineView.node.style.zIndex = 2;
-	    }
-	    return lineView.node;
-	  }
-
-	  function updateLineBackground(lineView) {
-	    var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass;
-	    if (cls) cls += " CodeMirror-linebackground";
-	    if (lineView.background) {
-	      if (cls) lineView.background.className = cls;
-	      else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; }
-	    } else if (cls) {
-	      var wrap = ensureLineWrapped(lineView);
-	      lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild);
-	    }
-	  }
-
-	  // Wrapper around buildLineContent which will reuse the structure
-	  // in display.externalMeasured when possible.
-	  function getLineContent(cm, lineView) {
-	    var ext = cm.display.externalMeasured;
-	    if (ext && ext.line == lineView.line) {
-	      cm.display.externalMeasured = null;
-	      lineView.measure = ext.measure;
-	      return ext.built;
-	    }
-	    return buildLineContent(cm, lineView);
-	  }
-
-	  // Redraw the line's text. Interacts with the background and text
-	  // classes because the mode may output tokens that influence these
-	  // classes.
-	  function updateLineText(cm, lineView) {
-	    var cls = lineView.text.className;
-	    var built = getLineContent(cm, lineView);
-	    if (lineView.text == lineView.node) lineView.node = built.pre;
-	    lineView.text.parentNode.replaceChild(built.pre, lineView.text);
-	    lineView.text = built.pre;
-	    if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) {
-	      lineView.bgClass = built.bgClass;
-	      lineView.textClass = built.textClass;
-	      updateLineClasses(lineView);
-	    } else if (cls) {
-	      lineView.text.className = cls;
-	    }
-	  }
-
-	  function updateLineClasses(lineView) {
-	    updateLineBackground(lineView);
-	    if (lineView.line.wrapClass)
-	      ensureLineWrapped(lineView).className = lineView.line.wrapClass;
-	    else if (lineView.node != lineView.text)
-	      lineView.node.className = "";
-	    var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass;
-	    lineView.text.className = textClass || "";
-	  }
-
-	  function updateLineGutter(cm, lineView, lineN, dims) {
-	    if (lineView.gutter) {
-	      lineView.node.removeChild(lineView.gutter);
-	      lineView.gutter = null;
-	    }
-	    if (lineView.gutterBackground) {
-	      lineView.node.removeChild(lineView.gutterBackground);
-	      lineView.gutterBackground = null;
-	    }
-	    if (lineView.line.gutterClass) {
-	      var wrap = ensureLineWrapped(lineView);
-	      lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass,
-	                                      "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) +
-	                                      "px; width: " + dims.gutterTotalWidth + "px");
-	      wrap.insertBefore(lineView.gutterBackground, lineView.text);
-	    }
-	    var markers = lineView.line.gutterMarkers;
-	    if (cm.options.lineNumbers || markers) {
-	      var wrap = ensureLineWrapped(lineView);
-	      var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " +
-	                                             (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px");
-	      cm.display.input.setUneditable(gutterWrap);
-	      wrap.insertBefore(gutterWrap, lineView.text);
-	      if (lineView.line.gutterClass)
-	        gutterWrap.className += " " + lineView.line.gutterClass;
-	      if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"]))
-	        lineView.lineNumber = gutterWrap.appendChild(
-	          elt("div", lineNumberFor(cm.options, lineN),
-	              "CodeMirror-linenumber CodeMirror-gutter-elt",
-	              "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: "
-	              + cm.display.lineNumInnerWidth + "px"));
-	      if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) {
-	        var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id];
-	        if (found)
-	          gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " +
-	                                     dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px"));
-	      }
-	    }
-	  }
-
-	  function updateLineWidgets(cm, lineView, dims) {
-	    if (lineView.alignable) lineView.alignable = null;
-	    for (var node = lineView.node.firstChild, next; node; node = next) {
-	      var next = node.nextSibling;
-	      if (node.className == "CodeMirror-linewidget")
-	        lineView.node.removeChild(node);
-	    }
-	    insertLineWidgets(cm, lineView, dims);
-	  }
-
-	  // Build a line's DOM representation from scratch
-	  function buildLineElement(cm, lineView, lineN, dims) {
-	    var built = getLineContent(cm, lineView);
-	    lineView.text = lineView.node = built.pre;
-	    if (built.bgClass) lineView.bgClass = built.bgClass;
-	    if (built.textClass) lineView.textClass = built.textClass;
-
-	    updateLineClasses(lineView);
-	    updateLineGutter(cm, lineView, lineN, dims);
-	    insertLineWidgets(cm, lineView, dims);
-	    return lineView.node;
-	  }
-
-	  // A lineView may contain multiple logical lines (when merged by
-	  // collapsed spans). The widgets for all of them need to be drawn.
-	  function insertLineWidgets(cm, lineView, dims) {
-	    insertLineWidgetsFor(cm, lineView.line, lineView, dims, true);
-	    if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++)
-	      insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false);
-	  }
-
-	  function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) {
-	    if (!line.widgets) return;
-	    var wrap = ensureLineWrapped(lineView);
-	    for (var i = 0, ws = line.widgets; i < ws.length; ++i) {
-	      var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget");
-	      if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true");
-	      positionLineWidget(widget, node, lineView, dims);
-	      cm.display.input.setUneditable(node);
-	      if (allowAbove && widget.above)
-	        wrap.insertBefore(node, lineView.gutter || lineView.text);
+	      result.push(line)
+	      pos = nl + 1
+	    }
+	  }
+	  return result
+	} : function (string) { return string.split(/\r\n?|\n/); }
+
+	var hasSelection = window.getSelection ? function (te) {
+	  try { return te.selectionStart != te.selectionEnd }
+	  catch(e) { return false }
+	} : function (te) {
+	  var range
+	  try {range = te.ownerDocument.selection.createRange()}
+	  catch(e) {}
+	  if (!range || range.parentElement() != te) { return false }
+	  return range.compareEndPoints("StartToEnd", range) != 0
+	}
+
+	var hasCopyEvent = (function () {
+	  var e = elt("div")
+	  if ("oncopy" in e) { return true }
+	  e.setAttribute("oncopy", "return;")
+	  return typeof e.oncopy == "function"
+	})()
+
+	var badZoomedRects = null
+	function hasBadZoomedRects(measure) {
+	  if (badZoomedRects != null) { return badZoomedRects }
+	  var node = removeChildrenAndAdd(measure, elt("span", "x"))
+	  var normal = node.getBoundingClientRect()
+	  var fromRange = range(node, 0, 1).getBoundingClientRect()
+	  return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1
+	}
+
+	var modes = {};
+	var mimeModes = {};
+	// Extra arguments are stored as the mode's dependencies, which is
+	// used by (legacy) mechanisms like loadmode.js to automatically
+	// load a mode. (Preferred mechanism is the require/define calls.)
+	function defineMode(name, mode) {
+	  if (arguments.length > 2)
+	    { mode.dependencies = Array.prototype.slice.call(arguments, 2) }
+	  modes[name] = mode
+	}
+
+	function defineMIME(mime, spec) {
+	  mimeModes[mime] = spec
+	}
+
+	// Given a MIME type, a {name, ...options} config object, or a name
+	// string, return a mode config object.
+	function resolveMode(spec) {
+	  if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
+	    spec = mimeModes[spec]
+	  } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
+	    var found = mimeModes[spec.name]
+	    if (typeof found == "string") { found = {name: found} }
+	    spec = createObj(found, spec)
+	    spec.name = found.name
+	  } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) {
+	    return resolveMode("application/xml")
+	  } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) {
+	    return resolveMode("application/json")
+	  }
+	  if (typeof spec == "string") { return {name: spec} }
+	  else { return spec || {name: "null"} }
+	}
+
+	// Given a mode spec (anything that resolveMode accepts), find and
+	// initialize an actual mode object.
+	function getMode(options, spec) {
+	  spec = resolveMode(spec)
+	  var mfactory = modes[spec.name]
+	  if (!mfactory) { return getMode(options, "text/plain") }
+	  var modeObj = mfactory(options, spec)
+	  if (modeExtensions.hasOwnProperty(spec.name)) {
+	    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$1 in spec.modeProps)
+	    { modeObj[prop$1] = spec.modeProps[prop$1] } }
+
+	  return modeObj
+	}
+
+	// This can be used to attach properties to mode objects from
+	// outside the actual mode definition.
+	var modeExtensions = {}
+	function extendMode(mode, properties) {
+	  var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {})
+	  copyObj(properties, exts)
+	}
+
+	function copyState(mode, state) {
+	  if (state === true) { return state }
+	  if (mode.copyState) { return mode.copyState(state) }
+	  var nstate = {}
+	  for (var n in state) {
+	    var val = state[n]
+	    if (val instanceof Array) { val = val.concat([]) }
+	    nstate[n] = val
+	  }
+	  return nstate
+	}
+
+	// Given a mode and a state (for that mode), find the inner mode and
+	// state at the position that the state refers to.
+	function innerMode(mode, state) {
+	  var info
+	  while (mode.innerMode) {
+	    info = mode.innerMode(state)
+	    if (!info || info.mode == mode) { break }
+	    state = info.state
+	    mode = info.mode
+	  }
+	  return info || {mode: mode, state: state}
+	}
+
+	function startState(mode, a1, a2) {
+	  return mode.startState ? mode.startState(a1, a2) : true
+	}
+
+	// STRING STREAM
+
+	// Fed to the mode parsers, provides helper functions to make
+	// parsers more succinct.
+
+	var StringStream = function(string, tabSize, lineOracle) {
+	  this.pos = this.start = 0
+	  this.string = string
+	  this.tabSize = tabSize || 8
+	  this.lastColumnPos = this.lastColumnValue = 0
+	  this.lineStart = 0
+	  this.lineOracle = lineOracle
+	};
+
+	StringStream.prototype.eol = function () {return this.pos >= this.string.length};
+	StringStream.prototype.sol = function () {return this.pos == this.lineStart};
+	StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined};
+	StringStream.prototype.next = function () {
+	  if (this.pos < this.string.length)
+	    { return this.string.charAt(this.pos++) }
+	};
+	StringStream.prototype.eat = function (match) {
+	  var ch = this.string.charAt(this.pos)
+	  var ok
+	  if (typeof match == "string") { ok = ch == match }
+	  else { ok = ch && (match.test ? match.test(ch) : match(ch)) }
+	  if (ok) {++this.pos; return ch}
+	};
+	StringStream.prototype.eatWhile = function (match) {
+	  var start = this.pos
+	  while (this.eat(match)){}
+	  return this.pos > start
+	};
+	StringStream.prototype.eatSpace = function () {
+	    var this$1 = this;
+
+	  var start = this.pos
+	  while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this$1.pos }
+	  return this.pos > start
+	};
+	StringStream.prototype.skipToEnd = function () {this.pos = this.string.length};
+	StringStream.prototype.skipTo = function (ch) {
+	  var found = this.string.indexOf(ch, this.pos)
+	  if (found > -1) {this.pos = found; return true}
+	};
+	StringStream.prototype.backUp = function (n) {this.pos -= n};
+	StringStream.prototype.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 - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)
+	};
+	StringStream.prototype.indentation = function () {
+	  return countColumn(this.string, null, this.tabSize) -
+	    (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0)
+	};
+	StringStream.prototype.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
+	  }
+	};
+	StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)};
+	StringStream.prototype.hideFirstChars = function (n, inner) {
+	  this.lineStart += n
+	  try { return inner() }
+	  finally { this.lineStart -= n }
+	};
+	StringStream.prototype.lookAhead = function (n) {
+	  var oracle = this.lineOracle
+	  return oracle && oracle.lookAhead(n)
+	};
+
+	var SavedContext = function(state, lookAhead) {
+	  this.state = state
+	  this.lookAhead = lookAhead
+	};
+
+	var Context = function(doc, state, line, lookAhead) {
+	  this.state = state
+	  this.doc = doc
+	  this.line = line
+	  this.maxLookAhead = lookAhead || 0
+	};
+
+	Context.prototype.lookAhead = function (n) {
+	  var line = this.doc.getLine(this.line + n)
+	  if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n }
+	  return line
+	};
+
+	Context.prototype.nextLine = function () {
+	  this.line++
+	  if (this.maxLookAhead > 0) { this.maxLookAhead-- }
+	};
+
+	Context.fromSaved = function (doc, saved, line) {
+	  if (saved instanceof SavedContext)
+	    { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) }
+	  else
+	    { return new Context(doc, copyState(doc.mode, saved), line) }
+	};
+
+	Context.prototype.save = function (copy) {
+	  var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
+	  return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
+	};
+
+
+	// Compute a style array (an array starting with a mode generation
+	// -- for invalidation -- followed by pairs of end positions and
+	// style strings), which is used to highlight the tokens on the
+	// line.
+	function highlightLine(cm, line, context, forceToEnd) {
+	  // A styles array always starts with a number identifying the
+	  // mode/overlays that it is based on (for easy invalidation).
+	  var st = [cm.state.modeGen], lineClasses = {}
+	  // Compute the base array of styles
+	  runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); },
+	          lineClasses, forceToEnd)
+	  var state = context.state
+
+	  // Run overlays, adjust style array.
+	  var loop = function ( o ) {
+	    var overlay = cm.state.overlays[o], i = 1, at = 0
+	    context.state = true
+	    runMode(cm, line.text, overlay.mode, context, function (end, style) {
+	      var start = i
+	      // Ensure there's a token end at the current position, and that i points at it
+	      while (at < end) {
+	        var i_end = st[i]
+	        if (i_end > end)
+	          { st.splice(i, 1, end, st[i+1], i_end) }
+	        i += 2
+	        at = Math.min(end, i_end)
+	      }
+	      if (!style) { return }
+	      if (overlay.opaque) {
+	        st.splice(start, i - start, end, "overlay " + style)
+	        i = start + 2
+	      } else {
+	        for (; start < i; start += 2) {
+	          var cur = st[start+1]
+	          st[start+1] = (cur ? cur + " " : "") + "overlay " + style
+	        }
+	      }
+	    }, lineClasses)
+	  };
+
+	  for (var o = 0; o < cm.state.overlays.length; ++o) loop( o );
+	  context.state = state
+
+	  return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}
+	}
+
+	function getLineStyles(cm, line, updateFrontier) {
+	  if (!line.styles || line.styles[0] != cm.state.modeGen) {
+	    var context = getContextBefore(cm, lineNo(line))
+	    var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
+	    var result = highlightLine(cm, line, context)
+	    if (resetState) { context.state = resetState }
+	    line.stateAfter = context.save(!resetState)
+	    line.styles = result.styles
+	    if (result.classes) { line.styleClasses = result.classes }
+	    else if (line.styleClasses) { line.styleClasses = null }
+	    if (updateFrontier === cm.doc.highlightFrontier)
+	      { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier) }
+	  }
+	  return line.styles
+	}
+
+	function getContextBefore(cm, n, precise) {
+	  var doc = cm.doc, display = cm.display
+	  if (!doc.mode.startState) { return new Context(doc, true, n) }
+	  var start = findStartLine(cm, n, precise)
+	  var saved = start > doc.first && getLine(doc, start - 1).stateAfter
+	  var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start)
+
+	  doc.iter(start, n, function (line) {
+	    processLine(cm, line.text, context)
+	    var pos = context.line
+	    line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
+	    context.nextLine()
+	  })
+	  if (precise) { doc.modeFrontier = context.line }
+	  return context
+	}
+
+	// Lightweight form of highlight -- proceed over this line and
+	// update state, but don't save a style array. Used for lines that
+	// aren't currently visible.
+	function processLine(cm, text, context, startAt) {
+	  var mode = cm.doc.mode
+	  var stream = new StringStream(text, cm.options.tabSize, context)
+	  stream.start = stream.pos = startAt || 0
+	  if (text == "") { callBlankLine(mode, context.state) }
+	  while (!stream.eol()) {
+	    readToken(mode, stream, context.state)
+	    stream.start = stream.pos
+	  }
+	}
+
+	function callBlankLine(mode, state) {
+	  if (mode.blankLine) { return mode.blankLine(state) }
+	  if (!mode.innerMode) { return }
+	  var inner = innerMode(mode, state)
+	  if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) }
+	}
+
+	function readToken(mode, stream, state, inner) {
+	  for (var i = 0; i < 10; i++) {
+	    if (inner) { inner[0] = innerMode(mode, state).mode }
+	    var style = mode.token(stream, state)
+	    if (stream.pos > stream.start) { return style }
+	  }
+	  throw new Error("Mode " + mode.name + " failed to advance stream.")
+	}
+
+	var Token = function(stream, type, state) {
+	  this.start = stream.start; this.end = stream.pos
+	  this.string = stream.current()
+	  this.type = type || null
+	  this.state = state
+	};
+
+	// Utility for getTokenAt and getLineTokens
+	function takeToken(cm, pos, precise, asArray) {
+	  var doc = cm.doc, mode = doc.mode, style
+	  pos = clipPos(doc, pos)
+	  var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise)
+	  var stream = new StringStream(line.text, cm.options.tabSize, context), tokens
+	  if (asArray) { tokens = [] }
+	  while ((asArray || stream.pos < pos.ch) && !stream.eol()) {
+	    stream.start = stream.pos
+	    style = readToken(mode, stream, context.state)
+	    if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))) }
+	  }
+	  return asArray ? tokens : new Token(stream, style, context.state)
+	}
+
+	function extractLineClasses(type, output) {
+	  if (type) { for (;;) {
+	    var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/)
+	    if (!lineClass) { break }
+	    type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length)
+	    var prop = lineClass[1] ? "bgClass" : "textClass"
+	    if (output[prop] == null)
+	      { output[prop] = lineClass[2] }
+	    else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop]))
+	      { output[prop] += " " + lineClass[2] }
+	  } }
+	  return type
+	}
+
+	// Run the given mode's parser over a line, calling f for each token.
+	function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) {
+	  var flattenSpans = mode.flattenSpans
+	  if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans }
+	  var curStart = 0, curStyle = null
+	  var stream = new StringStream(text, cm.options.tabSize, context), style
+	  var inner = cm.options.addModeClass && [null]
+	  if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses) }
+	  while (!stream.eol()) {
+	    if (stream.pos > cm.options.maxHighlightLength) {
+	      flattenSpans = false
+	      if (forceToEnd) { processLine(cm, text, context, stream.pos) }
+	      stream.pos = text.length
+	      style = null
+	    } else {
+	      style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses)
+	    }
+	    if (inner) {
+	      var mName = inner[0].name
+	      if (mName) { style = "m-" + (style ? mName + " " + style : mName) }
+	    }
+	    if (!flattenSpans || curStyle != style) {
+	      while (curStart < stream.start) {
+	        curStart = Math.min(stream.start, curStart + 5000)
+	        f(curStart, curStyle)
+	      }
+	      curStyle = style
+	    }
+	    stream.start = stream.pos
+	  }
+	  while (curStart < stream.pos) {
+	    // Webkit seems to refuse to render text nodes longer than 57444
+	    // characters, and returns inaccurate measurements in nodes
+	    // starting around 5000 chars.
+	    var pos = Math.min(stream.pos, curStart + 5000)
+	    f(pos, curStyle)
+	    curStart = pos
+	  }
+	}
+
+	// Finds the line to start with when starting a parse. Tries to
+	// find a line with a stateAfter, so that it can start with a
+	// valid state. If that fails, it returns the line with the
+	// smallest indentation, which tends to need the least context to
+	// parse correctly.
+	function findStartLine(cm, n, precise) {
+	  var minindent, minline, doc = cm.doc
+	  var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100)
+	  for (var search = n; search > lim; --search) {
+	    if (search <= doc.first) { return doc.first }
+	    var line = getLine(doc, search - 1), after = line.stateAfter
+	    if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
+	      { return search }
+	    var indented = countColumn(line.text, null, cm.options.tabSize)
+	    if (minline == null || minindent > indented) {
+	      minline = search - 1
+	      minindent = indented
+	    }
+	  }
+	  return minline
+	}
+
+	function retreatFrontier(doc, n) {
+	  doc.modeFrontier = Math.min(doc.modeFrontier, n)
+	  if (doc.highlightFrontier < n - 10) { return }
+	  var start = doc.first
+	  for (var line = n - 1; line > start; line--) {
+	    var saved = getLine(doc, line).stateAfter
+	    // change is on 3
+	    // state on line 1 looked ahead 2 -- so saw 3
+	    // test 1 + 2 < 3 should cover this
+	    if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) {
+	      start = line + 1
+	      break
+	    }
+	  }
+	  doc.highlightFrontier = Math.min(doc.highlightFrontier, start)
+	}
+
+	// LINE DATA STRUCTURE
+
+	// Line objects. These hold state related to a line, including
+	// highlighting info (the styles array).
+	var Line = function(text, markedSpans, estimateHeight) {
+	  this.text = text
+	  attachMarkedSpans(this, markedSpans)
+	  this.height = estimateHeight ? estimateHeight(this) : 1
+	};
+
+	Line.prototype.lineNo = function () { return lineNo(this) };
+	eventMixin(Line)
+
+	// Change the content (text, markers) of a line. Automatically
+	// invalidates cached information and tries to re-estimate the
+	// line's height.
+	function updateLine(line, text, markedSpans, estimateHeight) {
+	  line.text = text
+	  if (line.stateAfter) { line.stateAfter = null }
+	  if (line.styles) { line.styles = null }
+	  if (line.order != null) { line.order = null }
+	  detachMarkedSpans(line)
+	  attachMarkedSpans(line, markedSpans)
+	  var estHeight = estimateHeight ? estimateHeight(line) : 1
+	  if (estHeight != line.height) { updateLineHeight(line, estHeight) }
+	}
+
+	// Detach a line from the document tree and its markers.
+	function cleanUpLine(line) {
+	  line.parent = null
+	  detachMarkedSpans(line)
+	}
+
+	// Convert a style as returned by a mode (either null, or a string
+	// containing one or more styles) to a CSS style. This is cached,
+	// and also looks for line-wide styles.
+	var styleToClassCache = {};
+	var styleToClassCacheWithMode = {};
+	function interpretTokenStyle(style, options) {
+	  if (!style || /^\s*$/.test(style)) { return null }
+	  var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache
+	  return cache[style] ||
+	    (cache[style] = style.replace(/\S+/g, "cm-$&"))
+	}
+
+	// Render the DOM representation of the text of a line. Also builds
+	// up a 'line map', which points at the DOM nodes that represent
+	// specific stretches of text, and is used by the measuring code.
+	// The returned object contains the DOM node, this map, and
+	// information about line-wide styles that were set by the mode.
+	function buildLineContent(cm, lineView) {
+	  // The padding-right forces the element to have a 'border', which
+	  // is needed on Webkit to be able to get line-level bounding
+	  // rectangles for it (in measureChar).
+	  var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null)
+	  var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content,
+	                 col: 0, pos: 0, cm: cm,
+	                 trailingSpace: false,
+	                 splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}
+	  lineView.measure = {}
+
+	  // Iterate over the logical lines that make up this visual line.
+	  for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) {
+	    var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0)
+	    builder.pos = 0
+	    builder.addToken = buildToken
+	    // Optionally wire in some hacks into the token-rendering
+	    // algorithm, to deal with browser quirks.
+	    if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction)))
+	      { builder.addToken = buildTokenBadBidi(builder.addToken, order) }
+	    builder.map = []
+	    var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line)
+	    insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate))
+	    if (line.styleClasses) {
+	      if (line.styleClasses.bgClass)
+	        { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || "") }
+	      if (line.styleClasses.textClass)
+	        { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || "") }
+	    }
+
+	    // Ensure at least a single node is present, for measuring.
+	    if (builder.map.length == 0)
+	      { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))) }
+
+	    // Store the map and a cache object for the current logical line
+	    if (i == 0) {
+	      lineView.measure.map = builder.map
+	      lineView.measure.cache = {}
+	    } else {
+	      ;(lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map)
+	      ;(lineView.measure.caches || (lineView.measure.caches = [])).push({})
+	    }
+	  }
+
+	  // See issue #2901
+	  if (webkit) {
+	    var last = builder.content.lastChild
+	    if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab")))
+	      { builder.content.className = "cm-tab-wrap-hack" }
+	  }
+
+	  signal(cm, "renderLine", cm, lineView.line, builder.pre)
+	  if (builder.pre.className)
+	    { builder.textClass = joinClasses(builder.pre.className, builder.textClass || "") }
+
+	  return builder
+	}
+
+	function defaultSpecialCharPlaceholder(ch) {
+	  var token = elt("span", "\u2022", "cm-invalidchar")
+	  token.title = "\\u" + ch.charCodeAt(0).toString(16)
+	  token.setAttribute("aria-label", token.title)
+	  return token
+	}
+
+	// Build up the DOM representation for a single token, and add it to
+	// the line map. Takes care to render special characters separately.
+	function buildToken(builder, text, style, startStyle, endStyle, title, css) {
+	  if (!text) { return }
+	  var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text
+	  var special = builder.cm.state.specialChars, mustWrap = false
+	  var content
+	  if (!special.test(text)) {
+	    builder.col += text.length
+	    content = document.createTextNode(displayText)
+	    builder.map.push(builder.pos, builder.pos + text.length, content)
+	    if (ie && ie_version < 9) { mustWrap = true }
+	    builder.pos += text.length
+	  } else {
+	    content = document.createDocumentFragment()
+	    var pos = 0
+	    while (true) {
+	      special.lastIndex = pos
+	      var m = special.exec(text)
+	      var skipped = m ? m.index - pos : text.length - pos
+	      if (skipped) {
+	        var txt = document.createTextNode(displayText.slice(pos, pos + skipped))
+	        if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])) }
+	        else { content.appendChild(txt) }
+	        builder.map.push(builder.pos, builder.pos + skipped, txt)
+	        builder.col += skipped
+	        builder.pos += skipped
+	      }
+	      if (!m) { break }
+	      pos += skipped + 1
+	      var txt$1 = (void 0)
+	      if (m[0] == "\t") {
+	        var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize
+	        txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"))
+	        txt$1.setAttribute("role", "presentation")
+	        txt$1.setAttribute("cm-text", "\t")
+	        builder.col += tabWidth
+	      } else if (m[0] == "\r" || m[0] == "\n") {
+	        txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar"))
+	        txt$1.setAttribute("cm-text", m[0])
+	        builder.col += 1
+	      } else {
+	        txt$1 = builder.cm.options.specialCharPlaceholder(m[0])
+	        txt$1.setAttribute("cm-text", m[0])
+	        if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])) }
+	        else { content.appendChild(txt$1) }
+	        builder.col += 1
+	      }
+	      builder.map.push(builder.pos, builder.pos + 1, txt$1)
+	      builder.pos++
+	    }
+	  }
+	  builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32
+	  if (style || startStyle || endStyle || mustWrap || css) {
+	    var fullStyle = style || ""
+	    if (startStyle) { fullStyle += startStyle }
+	    if (endStyle) { fullStyle += endStyle }
+	    var token = elt("span", [content], fullStyle, css)
+	    if (title) { token.title = title }
+	    return builder.content.appendChild(token)
+	  }
+	  builder.content.appendChild(content)
+	}
+
+	function splitSpaces(text, trailingBefore) {
+	  if (text.length > 1 && !/  /.test(text)) { return text }
+	  var spaceBefore = trailingBefore, result = ""
+	  for (var i = 0; i < text.length; i++) {
+	    var ch = text.charAt(i)
+	    if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32))
+	      { ch = "\u00a0" }
+	    result += ch
+	    spaceBefore = ch == " "
+	  }
+	  return result
+	}
+
+	// Work around nonsense dimensions being reported for stretches of
+	// right-to-left text.
+	function buildTokenBadBidi(inner, order) {
+	  return function (builder, text, style, startStyle, endStyle, title, css) {
+	    style = style ? style + " cm-force-border" : "cm-force-border"
+	    var start = builder.pos, end = start + text.length
+	    for (;;) {
+	      // Find the part that overlaps with the start of this text
+	      var part = (void 0)
+	      for (var i = 0; i < order.length; i++) {
+	        part = order[i]
+	        if (part.to > start && part.from <= start) { break }
+	      }
+	      if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, title, css) }
+	      inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css)
+	      startStyle = null
+	      text = text.slice(part.to - start)
+	      start = part.to
+	    }
+	  }
+	}
+
+	function buildCollapsedSpan(builder, size, marker, ignoreWidget) {
+	  var widget = !ignoreWidget && marker.widgetNode
+	  if (widget) { builder.map.push(builder.pos, builder.pos + size, widget) }
+	  if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) {
+	    if (!widget)
+	      { widget = builder.content.appendChild(document.createElement("span")) }
+	    widget.setAttribute("cm-marker", marker.id)
+	  }
+	  if (widget) {
+	    builder.cm.display.input.setUneditable(widget)
+	    builder.content.appendChild(widget)
+	  }
+	  builder.pos += size
+	  builder.trailingSpace = false
+	}
+
+	// Outputs a number of spans to make up a line, taking highlighting
+	// and marked text into account.
+	function insertLineContent(line, builder, styles) {
+	  var spans = line.markedSpans, allText = line.text, at = 0
+	  if (!spans) {
+	    for (var i$1 = 1; i$1 < styles.length; i$1+=2)
+	      { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)) }
+	    return
+	  }
+
+	  var len = allText.length, pos = 0, i = 1, text = "", style, css
+	  var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed
+	  for (;;) {
+	    if (nextChange == pos) { // Update current marker set
+	      spanStyle = spanEndStyle = spanStartStyle = title = css = ""
+	      collapsed = null; nextChange = Infinity
+	      var foundBookmarks = [], endStyles = (void 0)
+	      for (var j = 0; j < spans.length; ++j) {
+	        var sp = spans[j], m = sp.marker
+	        if (m.type == "bookmark" && sp.from == pos && m.widgetNode) {
+	          foundBookmarks.push(m)
+	        } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) {
+	          if (sp.to != null && sp.to != pos && nextChange > sp.to) {
+	            nextChange = sp.to
+	            spanEndStyle = ""
+	          }
+	          if (m.className) { spanStyle += " " + m.className }
+	          if (m.css) { css = (css ? css + ";" : "") + m.css }
+	          if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle }
+	          if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to) }
+	          if (m.title && !title) { title = m.title }
+	          if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0))
+	            { collapsed = sp }
+	        } else if (sp.from > pos && nextChange > sp.from) {
+	          nextChange = sp.from
+	        }
+	      }
+	      if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2)
+	        { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1] } } }
+
+	      if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2)
+	        { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]) } }
+	      if (collapsed && (collapsed.from || 0) == pos) {
+	        buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos,
+	                           collapsed.marker, collapsed.from == null)
+	        if (collapsed.to == null) { return }
+	        if (collapsed.to == pos) { collapsed = false }
+	      }
+	    }
+	    if (pos >= len) { break }
+
+	    var upto = Math.min(len, nextChange)
+	    while (true) {
+	      if (text) {
+	        var end = pos + text.length
+	        if (!collapsed) {
+	          var tokenText = end > upto ? text.slice(0, upto - pos) : text
+	          builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle,
+	                           spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css)
+	        }
+	        if (end >= upto) {text = text.slice(upto - pos); pos = upto; break}
+	        pos = end
+	        spanStartStyle = ""
+	      }
+	      text = allText.slice(at, at = styles[i++])
+	      style = interpretTokenStyle(styles[i++], builder.cm.options)
+	    }
+	  }
+	}
+
+
+	// These objects are used to represent the visible (currently drawn)
+	// part of the document. A LineView may correspond to multiple
+	// logical lines, if those are connected by collapsed ranges.
+	function LineView(doc, line, lineN) {
+	  // The starting line
+	  this.line = line
+	  // Continuing lines, if any
+	  this.rest = visualLineContinued(line)
+	  // Number of logical lines in this visual line
+	  this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1
+	  this.node = this.text = null
+	  this.hidden = lineIsHidden(doc, line)
+	}
+
+	// Create a range of LineView objects for the given lines.
+	function buildViewArray(cm, from, to) {
+	  var array = [], nextPos
+	  for (var pos = from; pos < to; pos = nextPos) {
+	    var view = new LineView(cm.doc, getLine(cm.doc, pos), pos)
+	    nextPos = pos + view.size
+	    array.push(view)
+	  }
+	  return array
+	}
+
+	var operationGroup = null
+
+	function pushOperation(op) {
+	  if (operationGroup) {
+	    operationGroup.ops.push(op)
+	  } else {
+	    op.ownsGroup = operationGroup = {
+	      ops: [op],
+	      delayedCallbacks: []
+	    }
+	  }
+	}
+
+	function fireCallbacksForOps(group) {
+	  // Calls delayed callbacks and cursorActivity handlers until no
+	  // new ones appear
+	  var callbacks = group.delayedCallbacks, i = 0
+	  do {
+	    for (; i < callbacks.length; i++)
+	      { callbacks[i].call(null) }
+	    for (var j = 0; j < group.ops.length; j++) {
+	      var op = group.ops[j]
+	      if (op.cursorActivityHandlers)
+	        { while (op.cursorActivityCalled < op.cursorActivityHandlers.length)
+	          { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm) } }
+	    }
+	  } while (i < callbacks.length)
+	}
+
+	function finishOperation(op, endCb) {
+	  var group = op.ownsGroup
+	  if (!group) { return }
+
+	  try { fireCallbacksForOps(group) }
+	  finally {
+	    operationGroup = null
+	    endCb(group)
+	  }
+	}
+
+	var orphanDelayedCallbacks = null
+
+	// Often, we want to signal events at a point where we are in the
+	// middle of some work, but don't want the handler to start calling
+	// other methods on the editor, which might be in an inconsistent
+	// state or simply not expect any other events to happen.
+	// signalLater looks whether there are any handlers, and schedules
+	// them to be executed when the last operation ends, or, if no
+	// operation is active, when a timeout fires.