Merge "Asynchronous diff rendering"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 557e7b8..832665b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -21,7 +21,9 @@
     <div class="contentWrapper">
       <content></content>
     </div>
-    <gr-diff-processor id="processor"></gr-diff-processor>
+    <gr-diff-processor
+        id="processor"
+        groups="{{_groups}}"></gr-diff-processor>
   </template>
   <script src="../gr-diff/gr-diff-line.js"></script>
   <script src="../gr-diff/gr-diff-group.js"></script>
@@ -53,19 +55,34 @@
           baseImage: Object,
           revisionImage: Object,
           _builder: Object,
+          _groups: Array,
         },
 
         get diffElement() {
           return this.queryEffectiveChildren('#diffTable');
         },
 
+        observers: [
+          '_groupsChanged(_groups.splices)',
+        ],
+
         render: function(diff, comments, prefs) {
+          // Stop the processor (if it's running).
+          this.$.processor.cancel();
+
           this._builder = this._getDiffBuilder(diff, comments, prefs);
 
           this.$.processor.context = prefs.context;
           this.$.processor.keyLocations = this._getCommentLocations(comments);
-          this.$.processor.process(diff.content)
-              .then(this._renderDiff.bind(this));
+
+          this._clearDiffContent();
+
+          this.$.processor.process(diff.content).then(function() {
+            if (this.isImageDiff) {
+              this._builder.renderDiffImages();
+            }
+            this.fire('render');
+          }.bind(this));
         },
 
         getLineElByChild: function(node) {
@@ -183,10 +200,6 @@
           this._builder.emitGroup(group, sectionEl);
         },
 
-        emitDiff: function() {
-          this._builder.emitDiff();
-        },
-
         showContext: function(newGroups, sectionEl) {
           var groups = this._builder.groups;
           // TODO(viktard): Polyfill findIndex for IE10.
@@ -219,19 +232,6 @@
           throw Error('Unsupported diff view mode: ' + this.viewMode);
         },
 
-        _renderDiff: function(groups) {
-          this._builder.groups = groups;
-
-          this._clearDiffContent();
-          this.emitDiff();
-          if (this.isImageDiff) {
-            this._builder.renderDiffImages();
-          }
-          this.async(function() {
-            this.fire('render');
-          }, 1);
-        },
-
         _clearDiffContent: function() {
           this.diffElement.innerHTML = null;
         },
@@ -252,6 +252,18 @@
           }
           return result;
         },
+
+        _groupsChanged: function(changeRecord) {
+          if (!changeRecord) { return; }
+          changeRecord.indexSplices.forEach(function(splice) {
+            var group;
+            for (var i = 0; i < splice.addedCount; i++) {
+              group = splice.object[splice.index + i];
+              this._builder.groups.push(group);
+              this._builder.emitGroup(group);
+            }
+          }, this);
+        },
       });
     })();
   </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index b1256db..f68825c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -59,12 +59,6 @@
 
   var PARTIAL_CONTEXT_AMOUNT = 10;
 
-  GrDiffBuilder.prototype.emitDiff = function() {
-    for (var i = 0; i < this.groups.length; i++) {
-      this.emitGroup(this.groups[i]);
-    }
-  };
-
   GrDiffBuilder.prototype.buildSectionElement = function(group) {
     throw Error('Subclasses must implement buildGroupElement');
   };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 60654a8..4c5ee62 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -98,28 +98,39 @@
       assert.equal(cursorElement.diffRow, firstDeltaRow);
     });
 
-    test('diff cursor functionality (unified)', function() {
-      diffElement.viewMode = 'UNIFIED_DIFF';
-      cursorElement.reInitCursor();
+    suite('unified diff', function() {
 
-      // The cursor has been initialized to the first delta.
-      assert.isOk(cursorElement.diffRow);
+      setup(function(done) {
+        // We must allow the diff to re-render after setting the viewMode.
+        var renderHandler = function() {
+          diffElement.removeEventListener('render', renderHandler);
+          cursorElement.reInitCursor();
+          done();
+        };
+        diffElement.addEventListener('render', renderHandler);
+        diffElement.viewMode = 'UNIFIED_DIFF';
+      });
 
-      var firstDeltaRow = diffElement.$$('.section.delta .diff-row');
-      assert.equal(cursorElement.diffRow, firstDeltaRow);
+      test('diff cursor functionality (unified)', function() {
+        // The cursor has been initialized to the first delta.
+        assert.isOk(cursorElement.diffRow);
 
-      firstDeltaRow = diffElement.$$('.section.delta .diff-row');
-      assert.equal(cursorElement.diffRow, firstDeltaRow);
+        var firstDeltaRow = diffElement.$$('.section.delta .diff-row');
+        assert.equal(cursorElement.diffRow, firstDeltaRow);
 
-      cursorElement.moveDown();
+        firstDeltaRow = diffElement.$$('.section.delta .diff-row');
+        assert.equal(cursorElement.diffRow, firstDeltaRow);
 
-      assert.notEqual(cursorElement.diffRow, firstDeltaRow);
-      assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
+        cursorElement.moveDown();
 
-      cursorElement.moveUp();
+        assert.notEqual(cursorElement.diffRow, firstDeltaRow);
+        assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling);
 
-      assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
-      assert.equal(cursorElement.diffRow, firstDeltaRow);
+        cursorElement.moveUp();
+
+        assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling);
+        assert.equal(cursorElement.diffRow, firstDeltaRow);
+      });
     });
 
     test('cursor side functionality', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 0a54665..f0fc649 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -58,6 +58,8 @@
         type: Object,
         value: function() { return {left: {}, right: {}}; },
       },
+
+      _nextStepHandle: Number,
     },
 
     /**
@@ -82,6 +84,7 @@
           // If we are done, resolve the promise.
           if (state.sectionIndex >= content.length) {
             resolve(this.groups);
+            this._nextStepHandle = undefined;
             return;
           }
 
@@ -95,7 +98,7 @@
 
           // Increment the index and recurse.
           state.sectionIndex++;
-          this.async(nextStep, 1);
+          this._nextStepHandle = this.async(nextStep, 1);
         };
 
         nextStep.call(this);
@@ -103,6 +106,16 @@
     },
 
     /**
+     * Cancel any jobs that are running.
+     */
+    cancel: function() {
+      if (this._nextStepHandle !== undefined) {
+        this.cancelAsync(this._nextStepHandle);
+        this._nextStepHandle = undefined;
+      }
+    },
+
+    /**
      * Process the next section of the diff.
      */
     _processNext: function(state, content) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 32de7d5..d4cced0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -36,7 +36,10 @@
       changeNum: String,
       patchRange: Object,
       path: String,
-      prefs: Object,
+      prefs: {
+        type: Object,
+        observer: '_prefsObserver',
+      },
       projectConfig: {
         type: Object,
         observer: '_projectConfigChanged',
@@ -57,6 +60,7 @@
       viewMode: {
         type: String,
         value: DiffViewMode.SIDE_BY_SIDE,
+        observer: '_viewModeObserver',
       },
       _diff: Object,
       _comments: Object,
@@ -64,10 +68,6 @@
       _revisionImage: Object,
     },
 
-    observers: [
-      '_prefsChanged(prefs.*, viewMode)',
-    ],
-
     listeners: {
       'thread-discard': '_handleThreadDiscard',
       'comment-discard': '_handleCommentDiscard',
@@ -303,8 +303,28 @@
       });
     },
 
-    _prefsChanged: function(prefsChangeRecord) {
-      var prefs = prefsChangeRecord.base;
+    _prefsObserver: function(newPrefs, oldPrefs) {
+      // Scan the preference objects one level deep to see if they differ.
+      var differ = !oldPrefs;
+      if (newPrefs && oldPrefs) {
+        for (var key in newPrefs) {
+          if (newPrefs[key] !== oldPrefs[key]) {
+            differ = true;
+          }
+        }
+      }
+
+      if (differ) {
+        this._prefsChanged(newPrefs);
+      }
+    },
+
+    _viewModeObserver: function() {
+      this._prefsChanged(this.prefs);
+    },
+
+    _prefsChanged: function(prefs) {
+      if (!prefs) { return; }
       this.customStyle['--content-width'] = prefs.line_length + 'ch';
       this.updateStyles();