diff --git a/src/display/highlight_worker.js b/src/display/highlight_worker.js
index 1d964e640d6a16bcb06985b4b2c487f4bc928dc8..e868c42f3c58732af5b0f6387b62f0368151b7f4 100644
--- a/src/display/highlight_worker.js
+++ b/src/display/highlight_worker.js
@@ -8,20 +8,19 @@ import { regLineChange } from "./view_tracking"
 // HIGHLIGHT WORKER
 
 export function startWorker(cm, time) {
-  if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo)
+  if (cm.doc.highlightFrontier < cm.display.viewTo)
     cm.state.highlight.set(time, bind(highlightWorker, cm))
 }
 
 function highlightWorker(cm) {
   let doc = cm.doc
-  if (doc.frontier < doc.first) doc.frontier = doc.first
-  if (doc.frontier >= cm.display.viewTo) return
+  if (doc.highlightFrontier >= cm.display.viewTo) return
   let end = +new Date + cm.options.workTime
-  let context = getContextBefore(cm, doc.frontier)
+  let context = getContextBefore(cm, doc.highlightFrontier)
   let changedLines = []
 
-  doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), line => {
-    if (doc.frontier >= cm.display.viewFrom) { // Visible
+  doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), line => {
+    if (context.line >= cm.display.viewFrom) { // Visible
       let oldStyles = line.styles
       let resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null
       let highlighted = highlightLine(cm, line, context, true)
@@ -33,19 +32,22 @@ function highlightWorker(cm) {
       let ischange = !oldStyles || oldStyles.length != line.styles.length ||
         oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass)
       for (let i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]
-      if (ischange) changedLines.push(doc.frontier)
+      if (ischange) changedLines.push(context.line)
       line.stateAfter = context.save()
+      context.nextLine()
     } else {
       if (line.text.length <= cm.options.maxHighlightLength)
         processLine(cm, line.text, context)
-      line.stateAfter = doc.frontier % 5 == 0 ? context.save() : null
+      line.stateAfter = context.line % 5 == 0 ? context.save() : null
+      context.nextLine()
     }
-    ++doc.frontier
     if (+new Date > end) {
       startWorker(cm, cm.options.workDelay)
       return true
     }
   })
+  doc.highlightFrontier = context.line
+  doc.modeFrontier = Math.max(doc.modeFrontier, context.line)
   if (changedLines.length) runInOp(cm, () => {
     for (let i = 0; i < changedLines.length; i++)
       regLineChange(cm, changedLines[i], "text")
diff --git a/src/display/mode_state.js b/src/display/mode_state.js
index 8c4c60b0581a941611c1e1ea021c3d654262dcef..ca0a534ca44935c0f5a50681bc960fc78332fceb 100644
--- a/src/display/mode_state.js
+++ b/src/display/mode_state.js
@@ -15,7 +15,7 @@ export function resetModeState(cm) {
     if (line.stateAfter) line.stateAfter = null
     if (line.styles) line.styles = null
   })
-  cm.doc.frontier = cm.doc.first
+  cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first
   startWorker(cm, 100)
   cm.state.modeGen++
   if (cm.curOp) regChange(cm)
diff --git a/src/line/highlight.js b/src/line/highlight.js
index f1114a5d98d93e332ab86dec7b589e3040a2053f..e835aa0411ce907106a0f1d16f350bfb262f1fe7 100644
--- a/src/line/highlight.js
+++ b/src/line/highlight.js
@@ -39,7 +39,7 @@ class Context {
   }
 
   save(copy) {
-    let state = copy ? copyState(this.doc.mode, this.state) : this.state
+    let state = copy !== false ? copyState(this.doc.mode, this.state) : this.state
     return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state
   }
 }
@@ -95,11 +95,12 @@ export function getLineStyles(cm, line, updateFrontier) {
     let resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state)
     let result = highlightLine(cm, line, context)
     if (resetState) context.state = resetState
-    line.stateAfter = context.save()
+    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.frontier) cm.doc.frontier++
+    if (updateFrontier === cm.doc.highlightFrontier)
+      cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier)
   }
   return line.styles
 }
@@ -117,7 +118,7 @@ export function getContextBefore(cm, n, precise) {
     line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null
     context.nextLine()
   })
-  if (precise) doc.frontier = context.line
+  if (precise) doc.modeFrontier = context.line
   return context
 }
 
@@ -240,7 +241,7 @@ function findStartLine(cm, n, precise) {
   for (let search = n; search > lim; --search) {
     if (search <= doc.first) return doc.first
     let line = getLine(doc, search - 1), after = line.stateAfter
-    if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.frontier))
+    if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier))
       return search
     let indented = countColumn(line.text, null, cm.options.tabSize)
     if (minline == null || minindent > indented) {
@@ -250,3 +251,20 @@ function findStartLine(cm, n, precise) {
   }
   return minline
 }
+
+export function retreatFrontier(doc, n) {
+  doc.modeFrontier = Math.min(doc.modeFrontier, n)
+  if (doc.highlightFrontier < n - 10) return
+  let start = doc.first
+  for (let line = n - 1; line > start; line--) {
+    let 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)
+}
diff --git a/src/model/Doc.js b/src/model/Doc.js
index e76d3b152f7208f1c3bfc47b5c7c13abbf4b1dfc..c3da76d74e67466534be975708463fe7a8c2a624 100644
--- a/src/model/Doc.js
+++ b/src/model/Doc.js
@@ -29,7 +29,7 @@ let Doc = function(text, mode, firstLine, lineSep, direction) {
   this.scrollTop = this.scrollLeft = 0
   this.cantEdit = false
   this.cleanGeneration = 1
-  this.frontier = firstLine
+  this.modeFrontier = this.highlightFrontier = firstLine
   let start = Pos(firstLine, 0)
   this.sel = simpleSelection(start)
   this.history = new History(null)
diff --git a/src/model/changes.js b/src/model/changes.js
index c6692d4c5a96bd1f2dfd96bbad18e887b8716d04..214e0231abdbf53d65a5beaff2ec5a85f2a22491 100644
--- a/src/model/changes.js
+++ b/src/model/changes.js
@@ -1,3 +1,4 @@
+import { retreatFrontier } from "../line/highlight"
 import { startWorker } from "../display/highlight_worker"
 import { operation } from "../display/operations"
 import { regChange, regLineChange } from "../display/view_tracking"
@@ -231,8 +232,7 @@ function makeChangeSingleDocInEditor(cm, change, spans) {
     if (recomputeMaxLength) cm.curOp.updateMaxLine = true
   }
 
-  // Adjust frontier, schedule worker
-  doc.frontier = Math.min(doc.frontier, from.line)
+  retreatFrontier(doc, from.line)
   startWorker(cm, 400)
 
   let lendiff = change.text.length - (to.line - from.line) - 1
diff --git a/test/test.js b/test/test.js
index d1abc7a8122aa0c5f0cbf0caca283f9fb4ba3b7b..59b760d5ae0eaaa604f84167a7d7c66dfc183b10 100644
--- a/test/test.js
+++ b/test/test.js
@@ -2474,3 +2474,22 @@ testCM("delete_wrapped", function(cm) {
   cm.deleteH(-1, "char");
   eq(cm.getLine(0), "1245");
 }, {value: "12345", lineWrapping: true})
+
+CodeMirror.defineMode("lookahead_mode", function() {
+  // Colors text as atom if the line two lines down has an x in it
+  return {
+    token: function(stream) {
+      stream.skipToEnd()
+      return /x/.test(stream.lookAhead(2)) ? "atom" : null
+    }
+  }
+})
+
+testCM("mode_lookahead", function(cm) {
+  eq(cm.getTokenAt(Pos(0, 1)).type, "atom")
+  eq(cm.getTokenAt(Pos(1, 1)).type, "atom")
+  eq(cm.getTokenAt(Pos(2, 1)).type, null)
+  cm.replaceRange("\n", Pos(2, 0))
+  eq(cm.getTokenAt(Pos(0, 1)).type, null)
+  eq(cm.getTokenAt(Pos(1, 1)).type, "atom")
+}, {value: "foo\na\nx\nx\n", mode: "lookahead_mode"})