Merge "Trace time of ETag computations"
diff --git a/Documentation/dev-design-docs.txt b/Documentation/dev-design-docs.txt
index ac53bf8..5e3f7a9 100644
--- a/Documentation/dev-design-docs.txt
+++ b/Documentation/dev-design-docs.txt
@@ -85,6 +85,13 @@
 but in this case the implementation is only done if someone volunteers
 to do it (which is not guaranteed to happen).
 
+Only very few maintainers actively watch out for uploaded design docs.
+To raise awareness you may want to send a notification to the
+link:https://groups.google.com/d/forum/repo-discuss[repo-discuss]
+mailing list about your uploaded design doc. But the discussion should
+not take place on the mailing list, comments should be made by reviewing
+the change in Gerrit.
+
 [[review]]
 == Design doc review
 
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c05ef0c..e991318 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -194,8 +194,10 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.stream.Collectors;
@@ -358,7 +360,7 @@
 
   // Collections populated during processing.
   private final List<UpdateGroupsRequest> updateGroups;
-  private final List<ValidationMessage> messages;
+  private final Queue<ValidationMessage> messages;
   /** Multimap of error text to refnames that produced that error. */
   private final ListMultimap<String, String> errors;
 
@@ -485,7 +487,7 @@
 
     // Collections populated during processing.
     errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
-    messages = new ArrayList<>();
+    messages = new ConcurrentLinkedQueue<>();
     pushOptions = LinkedListMultimap.create();
     replaceByChange = new LinkedHashMap<>();
     updateGroups = new ArrayList<>();
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 780301b..f29a5da 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
@@ -20,6 +20,7 @@
 <link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html">
 <link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
 <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
+<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
 
 <dom-module id="gr-diff-builder">
   <template>
@@ -29,6 +30,9 @@
     <gr-ranged-comment-layer
         id="rangeLayer"
         comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
+    <gr-syntax-layer
+        id="syntaxLayer"
+        diff="[[diff]]"></gr-syntax-layer>
     <gr-coverage-layer
         id="coverageLayerLeft"
         coverage-ranges="[[_leftCoverageRanges]]"
@@ -60,6 +64,13 @@
         UNIFIED: 'UNIFIED_DIFF',
       };
 
+      // If any line of the diff is more than the character limit, then disable
+      // syntax highlighting for the entire file.
+      const SYNTAX_MAX_LINE_LENGTH = 500;
+
+      // Disable syntax highlighting if the overall diff is too large.
+      const SYNTAX_MAX_DIFF_LENGTH = 20000;
+
       const TRAILING_WHITESPACE_PATTERN = /\s+$/;
 
       Polymer({
@@ -73,11 +84,18 @@
          */
 
         /**
-         * Fired when the diff finishes rendering text content.
+         * Fired when the diff finishes rendering text content and starts
+         * syntax highlighting.
          *
          * @event render-content
          */
 
+        /**
+         * Fired when the diff finishes syntax highlighting.
+         *
+         * @event render-syntax
+         */
+
         properties: {
           diff: Object,
           diffPath: String,
@@ -120,7 +138,7 @@
            * @type {?Object}
            */
           _cancelableRenderPromise: Object,
-          layers: {
+          pluginLayers: {
             type: Array,
             value: [],
           },
@@ -153,10 +171,11 @@
           // attached before plugins are installed.
           this._setupAnnotationLayers();
 
+          this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
           this._showTabs = !!prefs.show_tabs;
           this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
 
-          // Stop the processor if it's running.
+          // Stop the processor and syntax layer (if they're running).
           this.cancel();
 
           this._builder = this._getDiffBuilder(this.diff, prefs);
@@ -179,6 +198,16 @@
                     }
                     this.dispatchEvent(new CustomEvent('render-content',
                         {bubbles: true, composed: true}));
+
+                    if (this._diffTooLargeForSyntax()) {
+                      this.$.syntaxLayer.enabled = false;
+                    }
+
+                    return this.$.syntaxLayer.process();
+                  })
+                  .then(() => {
+                    this.dispatchEvent(new CustomEvent(
+                        'render-syntax', {bubbles: true, composed: true}));
                   }));
           return this._cancelableRenderPromise
               .finally(() => { this._cancelableRenderPromise = null; })
@@ -191,6 +220,7 @@
         _setupAnnotationLayers() {
           const layers = [
             this._createTrailingWhitespaceLayer(),
+            this.$.syntaxLayer,
             this._createIntralineLayer(),
             this._createTabIndicatorLayer(),
             this.$.rangeLayer,
@@ -198,8 +228,8 @@
             this.$.coverageLayerRight,
           ];
 
-          if (this.layers) {
-            layers.push(...this.layers);
+          if (this.pluginLayers) {
+            layers.push(...this.pluginLayers);
           }
           this._layers = layers;
         },
@@ -277,6 +307,7 @@
 
         cancel() {
           this.$.processor.cancel();
+          this.$.syntaxLayer.cancel();
           if (this._cancelableRenderPromise) {
             this._cancelableRenderPromise.cancel();
             this._cancelableRenderPromise = null;
@@ -415,10 +446,44 @@
           };
         },
 
+        /**
+         * @return {boolean} whether any of the lines in _groups are longer
+         * than SYNTAX_MAX_LINE_LENGTH.
+         */
+        _anyLineTooLong() {
+          return this._groups.reduce((acc, group) => {
+            return acc || group.lines.reduce((acc, line) => {
+              return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH;
+            }, false);
+          }, false);
+        },
+
+        _diffTooLargeForSyntax() {
+          return this._anyLineTooLong() ||
+              this.getDiffLength() > SYNTAX_MAX_DIFF_LENGTH;
+        },
+
         setBlame(blame) {
           if (!this._builder || !blame) { return; }
           this._builder.setBlame(blame);
         },
+
+        /**
+         * Get the approximate length of the diff as the sum of the maximum
+         * length of the chunks.
+         * @return {number}
+         */
+        getDiffLength() {
+          return this.diff.content.reduce((sum, sec) => {
+            if (sec.hasOwnProperty('ab')) {
+              return sum + sec.ab.length;
+            } else {
+              return sum + Math.max(
+                  sec.hasOwnProperty('a') ? sec.a.length : 0,
+                  sec.hasOwnProperty('b') ? sec.b.length : 0);
+            }
+          }, 0);
+        },
       });
     })();
   </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index 45de810..6831a8d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -590,38 +590,38 @@
       });
     });
 
-    suite('layers', () => {
+    suite('layers from plugins', () => {
       let element;
       let initialLayersCount;
-      let withLayerCount;
+      let withPluginLayerCount;
       setup(() => {
-        const layers = [];
+        const pluginLayers = [];
         element = fixture('basic');
-        element.layers = layers;
+        element.pluginLayers = pluginLayers;
         element._showTrailingWhitespace = true;
         element._setupAnnotationLayers();
         initialLayersCount = element._layers.length;
       });
 
-      test('no layers', () => {
+      test('no plugin layers', () => {
         element._setupAnnotationLayers();
         assert.equal(element._layers.length, initialLayersCount);
       });
 
-      suite('with layers', () => {
-        const layers = [{}, {}];
+      suite('with plugin layers', () => {
+        const pluginLayers = [{}, {}];
         setup(() => {
           element = fixture('basic');
-          element.layers = layers;
+          element.pluginLayers = pluginLayers;
           element._showTrailingWhitespace = true;
           element._setupAnnotationLayers();
-          withLayerCount = element._layers.length;
+          withPluginLayerCount = element._layers.length;
         });
-        test('with layers', () => {
+        test('with plugin layers', () => {
           element._setupAnnotationLayers();
-          assert.equal(element._layers.length, withLayerCount);
-          assert.equal(initialLayersCount + layers.length,
-              withLayerCount);
+          assert.equal(element._layers.length, withPluginLayerCount);
+          assert.equal(initialLayersCount + pluginLayers.length,
+              withPluginLayerCount);
         });
       });
     });
@@ -733,6 +733,7 @@
         element.viewMode = 'SIDE_BY_SIDE';
         processStub = sandbox.stub(element.$.processor, 'process')
             .returns(Promise.resolve());
+        sandbox.stub(element, '_anyLineTooLong').returns(true);
         keyLocations = {left: {}, right: {}};
         prefs = {
           line_length: 10,
@@ -861,14 +862,37 @@
               .map(c => { return c.args[0].type; });
           assert.include(firedEventTypes, 'render-start');
           assert.include(firedEventTypes, 'render-content');
+          assert.include(firedEventTypes, 'render-syntax');
+          done();
+        });
+      });
+
+      test('rendering normal-sized diff does not disable syntax', () => {
+        assert.isTrue(element.$.syntaxLayer.enabled);
+      });
+
+      test('rendering large diff disables syntax', done => {
+        // Before it renders, set the first diff line to 500 '*' characters.
+        element.diff.content[0].a = [new Array(501).join('*')];
+        const prefs = {
+          line_length: 10,
+          show_tabs: true,
+          tab_size: 4,
+          context: -1,
+          syntax_highlighting: true,
+        };
+        element.render(keyLocations, prefs).then(() => {
+          assert.isFalse(element.$.syntaxLayer.enabled);
           done();
         });
       });
 
       test('cancel', () => {
         const processorCancelStub = sandbox.stub(element.$.processor, 'cancel');
+        const syntaxCancelStub = sandbox.stub(element.$.syntaxLayer, 'cancel');
         element.cancel();
         assert.isTrue(processorCancelStub.called);
+        assert.isTrue(syntaxCancelStub.called);
       });
     });
 
@@ -897,6 +921,10 @@
         });
       });
 
+      test('getDiffLength', () => {
+        assert.equal(element.getDiffLength(diff), 52);
+      });
+
       test('getContentByLine', () => {
         let actual;
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
index 85d8fb7..13b2269 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.html
@@ -23,7 +23,6 @@
 <link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 <link rel="import" href="../gr-diff/gr-diff.html">
-<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
 
 <dom-module id="gr-diff-host">
   <template>
@@ -50,13 +49,8 @@
         revision-image=[[_revisionImage]]
         coverage-ranges="[[_coverageRanges]]"
         blame="[[_blame]]"
-        layers="[[_layers]]"
-        diff="[[_diff]]">
-    </gr-diff>
-    <gr-syntax-layer
-        id="syntaxLayer"
-        enabled="[[_syntaxHighlightingEnabled]]"
-        diff="[[_diff]]"></gr-syntax-layer>
+        plugin-layers="[[pluginLayers]]"
+        diff="[[_diff]]"></gr-diff>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-reporting id="reporting" category="diff"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index 3ddf78c..79135a7 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -35,13 +35,6 @@
     SYNTAX: 'Diff Syntax Render',
   };
 
-  // Disable syntax highlighting if the overall diff is too large.
-  const SYNTAX_MAX_DIFF_LENGTH = 20000;
-
-  // If any line of the diff is more than the character limit, then disable
-  // syntax highlighting for the entire file.
-  const SYNTAX_MAX_LINE_LENGTH = 500;
-
   const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
 
   /**
@@ -212,13 +205,7 @@
         computed: '_computeParentIndex(patchRange.*)',
       },
 
-      _syntaxHighlightingEnabled: {
-        type: Boolean,
-        computed:
-          '_isSyntaxHighlightingEnabled(prefs.syntax_highlighting, _diff)',
-      },
-
-      _layers: {
+      pluginLayers: {
         type: Array,
         value: [],
       },
@@ -243,6 +230,7 @@
 
       'render-start': '_handleRenderStart',
       'render-content': '_handleRenderContent',
+      'render-syntax': '_handleRenderSyntax',
 
       'normalize-range': '_handleNormalizeRange',
     },
@@ -270,13 +258,13 @@
       this._errorMessage = null;
       const whitespaceLevel = this._getIgnoreWhitespace();
 
-      const layers = [this.$.syntaxLayer];
+      const pluginLayers = [];
       // Get layers from plugins (if any).
       for (const pluginLayer of this.$.jsAPI.getDiffLayers(
           this.diffPath, this.changeNum, this.patchNum)) {
-        layers.push(pluginLayer);
+        pluginLayers.push(pluginLayer);
       }
-      this._layers = layers;
+      this.push('pluginLayers', ...pluginLayers);
 
       this._coverageRanges = [];
       const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this;
@@ -863,25 +851,6 @@
           item => item.__draftID === comment.__draftID);
     },
 
-    _isSyntaxHighlightingEnabled(preference, diff) {
-      if (!preference) return false;
-      return !this._anyLineTooLong(diff) &&
-          this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH;
-    },
-
-    /**
-     * @return {boolean} whether any of the lines in diff are longer
-     * than SYNTAX_MAX_LINE_LENGTH.
-     */
-    _anyLineTooLong(diff) {
-      return diff.content.some(section => {
-        const lines = section.ab ?
-              section.ab :
-              (section.a || []).concat(section.b || []);
-        return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH);
-      });
-    },
-
     _handleRenderStart() {
       this.$.reporting.time(TimingLabel.TOTAL);
       this.$.reporting.time(TimingLabel.CONTENT);
@@ -890,10 +859,11 @@
     _handleRenderContent() {
       this.$.reporting.timeEnd(TimingLabel.CONTENT);
       this.$.reporting.time(TimingLabel.SYNTAX);
-      this.$.syntaxLayer.process().then(() => {
-        this.$.reporting.timeEnd(TimingLabel.SYNTAX);
-        this.$.reporting.timeEnd(TimingLabel.TOTAL);
-      });
+    },
+
+    _handleRenderSyntax() {
+      this.$.reporting.timeEnd(TimingLabel.SYNTAX);
+      this.$.reporting.timeEnd(TimingLabel.TOTAL);
     },
 
     _handleNormalizeRange(event) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index dcca01a..f87ef7a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -60,7 +60,7 @@
 
 
     suite('plugin layers', () => {
-      const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}];
+      const pluginLayers = [{}, {}];
       setup(() => {
         stub('gr-js-api-interface', {
           getDiffLayers() { return pluginLayers; },
@@ -303,7 +303,6 @@
       });
 
       test('ends content and starts syntax timer on render-content', done => {
-        element._diff = {content: []};
         element.dispatchEvent(
             new CustomEvent('render-content', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
@@ -313,18 +312,14 @@
         done();
       });
 
-      test('ends total and syntax timer after syntax layer processing', done => {
-        const processed = Promise.resolve();
-        sandbox.stub(element.$.syntaxLayer, 'process').returns(processed);
+      test('ends total and syntax timer on render-syntax', done => {
         element.dispatchEvent(
-            new CustomEvent('render-content', {bubbles: true, composed: true}));
-        processed.then(() => {
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-              'Diff Total Render'));
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-              'Diff Syntax Render'));
-          done();
-        });
+            new CustomEvent('render-syntax', {bubbles: true, composed: true}));
+        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+            'Diff Total Render'));
+        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+            'Diff Syntax Render'));
+        done();
       });
     });
 
@@ -1290,50 +1285,5 @@
       assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
           Gerrit.DiffSide.RIGHT), [r]);
     });
-
-    suite('syntax layer', () => {
-      setup(() => {
-        const prefs = {
-          line_length: 10,
-          show_tabs: true,
-          tab_size: 4,
-          context: -1,
-          syntax_highlighting: true,
-        };
-        element.prefs = prefs;
-      });
-
-      test('gr-diff-host provides syntax highlighting layer to gr-diff', () => {
-        element.patchRange = {};
-        element.reload();
-        assert.equal(element.$.diff.layers[0], element.$.syntaxLayer);
-      });
-
-      test('rendering normal-sized diff does not disable syntax', () => {
-        element._diff = {
-          content: [{
-            a: ['foo'],
-          }],
-        };
-        assert.isTrue(element.$.syntaxLayer.enabled);
-      });
-
-      test('rendering large diff disables syntax', () => {
-        // Before it renders, set the first diff line to 500 '*' characters.
-        element._diff = {
-          content: [{
-            a: [new Array(501).join('*')],
-          }],
-        };
-        assert.isFalse(element.$.syntaxLayer.enabled);
-      });
-
-      test('starts syntax layer processing on render-content event', () => {
-        sandbox.stub(element.$.syntaxLayer, 'process').returns(Promise.resolve());
-        element.dispatchEvent(
-            new CustomEvent('render-content', {bubbles: true, composed: true}));
-        assert.isTrue(element.$.syntaxLayer.process.called);
-      });
-    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index 374368c..32fa7e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -378,7 +378,7 @@
               line-wrapping="[[lineWrapping]]"
               is-image-diff="[[isImageDiff]]"
               base-image="[[baseImage]]"
-              layers="[[layers]]"
+              plugin-layers="[[pluginLayers]]"
               revision-image="[[revisionImage]]">
             <table
                 id="diffTable"
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 12162cb..16c92b1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -271,7 +271,7 @@
 
       /** Set by Polymer. */
       isAttached: Boolean,
-      layers: Array,
+      pluginLayers: Array,
     },
 
     behaviors: [
@@ -724,7 +724,7 @@
 
     _diffChanged(newValue) {
       if (newValue) {
-        this._diffLength = this.getDiffLength(newValue);
+        this._diffLength = this.$.diffBuilder.getDiffLength();
         this._debounceRenderDiffTable();
       }
     },
@@ -955,23 +955,5 @@
       if (loading || !warning) { return 'newlineWarning hidden'; }
       return 'newlineWarning';
     },
-
-    /**
-     * Get the approximate length of the diff as the sum of the maximum
-     * length of the chunks.
-     * @param {Object} diff object
-     * @return {number}
-     */
-    getDiffLength(diff) {
-      return diff.content.reduce((sum, sec) => {
-        if (sec.hasOwnProperty('ab')) {
-          return sum + sec.ab.length;
-        } else {
-          return sum + Math.max(
-              sec.hasOwnProperty('a') ? sec.a.length : 0,
-              sec.hasOwnProperty('b') ? sec.b.length : 0);
-        }
-      }, 0);
-    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 016dee0..15deaff 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -767,7 +767,7 @@
                   new CustomEvent('render', {bubbles: true, composed: true}));
             });
         const mock = document.createElement('mock-diff-response');
-        sandbox.stub(element, 'getDiffLength').returns(10000);
+        sandbox.stub(element.$.diffBuilder, 'getDiffLength').returns(10000);
         element.diff = mock.diffResponse;
         element.noRenderOnPrefsChange = true;
       });
@@ -1101,11 +1101,6 @@
           ));
       });
     });
-
-    test('getDiffLength', () => {
-      const diff = document.createElement('mock-diff-response').diffResponse;
-      assert.equal(element.getDiffLength(diff), 52);
-    });
   });
 
   a11ySuite('basic');
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index fcaa696..627a279 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -225,7 +225,7 @@
     process() {
       // Cancel any still running process() calls, because they append to the
       // same _baseRanges and _revisionRanges fields.
-      this._cancel();
+      this.cancel();
 
       // Discard existing ranges.
       this._baseRanges = [];
@@ -295,7 +295,7 @@
     /**
      * Cancel any asynchronous syntax processing jobs.
      */
-    _cancel() {
+    cancel() {
       if (this._processHandle != null) {
         this.cancelAsync(this._processHandle);
         this._processHandle = null;
@@ -306,7 +306,7 @@
     },
 
     _diffChanged() {
-      this._cancel();
+      this.cancel();
       this._baseRanges = [];
       this._revisionRanges = [];
     },
diff --git a/polygerrit-ui/app/styles/themes/app-theme.html b/polygerrit-ui/app/styles/themes/app-theme.html
index a0cb3d8..91bcd69 100644
--- a/polygerrit-ui/app/styles/themes/app-theme.html
+++ b/polygerrit-ui/app/styles/themes/app-theme.html
@@ -16,137 +16,137 @@
 -->
 <custom-style><style is="custom-style">
 html {
-  /* Following vars have LTS for plugin API. */
-  --primary-text-color: #000;
-  /* For backwords compatibility we keep this as --header-background-color. */
-  --header-background-color: #eee;
-  --header-title-content: 'Gerrit';
-  --header-icon: none;
-  --header-icon-size: 0em;
-  --header-text-color: #000;
-  --header-title-font-size: 1.75rem;
-  --footer-background-color: #eee;
-  --border-color: #ddd;
-  /* This allows to add a border in custom themes */
-  --header-border-bottom: 1px solid var(--border-color);
-  --header-border-image: '';
-  --footer-border-top: 1px solid var(--border-color);
+  /**
+   * When adding a new color variable make sure to also add it to the other
+   * theme files in the same directory.
+   *
+   * For colors prefer lower case hex colors.
+   *
+   * Note that plugins might be using these variables, so removing a variable
+   * can be a breaking change that should go into the release notes.
+   */
 
-  /* Following are not part of plugin API. */
-  --selection-background-color: rgba(161, 194, 250, 0.1);
-  --hover-background-color: rgba(161, 194, 250, 0.2);
-  --expanded-background-color: #eee;
-  --view-background-color: #fff;
-  --default-horizontal-margin: 1rem;
-
+  /* text colors */
+  --primary-text-color: black;
+  --link-color: #2a66d9;
+  --comment-text-color: black;
   --deemphasized-text-color: #757575;
-  /* Used on text color for change list that doesn't need user's attention. */
-  --reviewed-text-color: var(--primary-text-color);
-  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
-  /* Used on text for change list that needs user's attention. */
-  --font-weight-bold: 500;
-  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
-  --iron-overlay-backdrop: {
-    transition: none;
-  }
+  --default-button-text-color: #2a66d9;
+  --error-text-color: red;
+  --primary-button-text-color: white;
+    /* Used on text color for change list that doesn't need user's attention. */
+  --reviewed-text-color: black;
+  --secondary-button-text-color: #212121;
+  --tooltip-text-color: white;
+  --vote-text-color-recommended: #388e3c;
+  --vote-text-color-disliked: #d32f2f;
+
+  /* background colors */
+  --assignee-highlight-color: #fcfad6;
+  --chip-background-color: #eee;
+  --comment-background-color: #fcfad6;
+  --default-button-background-color: white;
+  --dialog-background-color: white;
+  --dropdown-background-color: white;
+  --edit-mode-background-color: #ebf5fb;
+  --emphasis-color: #fff9c4;
+  --expanded-background-color: #eee;
+  --hover-background-color: rgba(161, 194, 250, 0.2);
+  --primary-button-background-color: #2a66d9;
+  --secondary-button-background-color: white;
+  --select-background-color: #f8f8f8;
+  --selection-background-color: rgba(161, 194, 250, 0.1);
+  --shell-command-background-color: #f5f5f5;
+  --shell-command-decoration-background-color: #ebebeb;
   --table-header-background-color: #fafafa;
   --table-subheader-background-color: #eaeaea;
+  --tooltip-background-color: #333;
+  --unresolved-comment-background-color: #fcfaa6;
+  --view-background-color: white;
+  --vote-color-approved: #9fcc6b;
+  --vote-color-disliked: #f7c4cb;
+  --vote-color-neutral: #ebf5fb;
+  --vote-color-recommended: #c9dfaf;
+  --vote-color-rejected: #f7a1ad;
 
-  --chip-background-color: #eee;
+  /* misc colors */
+  --border-color: #ddd;
 
-  --dropdown-background-color: #fff;
-
-  --select-background-color: rgb(248, 248, 248);
-
-  --assignee-highlight-color: #fcfad6;
-
-  /* Font sizes */
+  /* fonts */
+  --font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
   --font-size-normal: 1rem;
   --font-size-small: .92rem;
   --font-size-large: 1.154rem;
+    /* Used on text for change list that needs user's attention. */
+  --font-weight-bold: 500;
+  --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
 
-  --link-color: #2a66d9;
-  --primary-button-background-color: var(--link-color);
-  --primary-button-text-color: #fff;
-  --secondary-button-background-color: #fff;
-  --secondary-button-text-color: #212121;
-  --default-button-background-color: #fff;
-  --default-button-text-color: var(--link-color);
-  --dialog-background-color: #fff;
+  /* spacing */
+  --default-horizontal-margin: 1rem;
 
-  /* Used for both the old patchset header and for indicating that a particular
-    change message was selected. */
-  --emphasis-color: #fff9c4;
+  /* header and footer */
+  --footer-background-color: #eee;
+  --footer-border-top: 1px solid var(--border-color);
+  --header-background-color: #eee;
+  --header-border-bottom: 1px solid var(--border-color);
+  --header-border-image: '';
+  --header-icon-size: 0em;
+  --header-icon: none;
+  --header-text-color: black;
+  --header-title-content: 'Gerrit';
+  --header-title-font-size: 1.75rem;
 
-  --error-text-color: red;
-
-  --vote-color-approved: #9fcc6b;
-  --vote-color-recommended: #c9dfaf;
-  --vote-color-rejected: #f7a1ad;
-  --vote-color-disliked: #f7c4cb;
-  --vote-color-neutral: #ebf5fb;
-
-  --vote-text-color-recommended: #388E3C;
-  --vote-text-color-disliked: #D32F2F;
-
-  /* Diff colors */
-  --diff-selection-background-color: #c7dbf9;
-  --light-remove-highlight-color: #FFEBEE;
-  --light-add-highlight-color: #D8FED8;
-  --light-remove-add-highlight-color: #FFF8DC;
-  --light-rebased-add-highlight-color: #EEEEFF;
-  --dark-remove-highlight-color: #FFCDD2;
-  --dark-add-highlight-color: #AAF2AA;
-  --dark-rebased-remove-highlight-color: #F7E8B7;
-  --dark-rebased-add-highlight-color: #D7D7F9;
-  --diff-context-control-color: var(--deemphasized-text-color);
+  /* diff colors */
+  --dark-add-highlight-color: #aaf2aa;
+  --dark-rebased-add-highlight-color: #d7d7f9;
+  --dark-rebased-remove-highlight-color: #f7e8b7;
+  --dark-remove-highlight-color: #ffcdd2;
+  --diff-blank-background-color: white;
   --diff-context-control-background-color: #fff7d4;
   --diff-context-control-border-color: #f6e6a5;
-  --diff-tab-indicator-color: var(--deemphasized-text-color);
-  --diff-trailing-whitespace-indicator: #ff9ad2;
+  --diff-context-control-color: var(--deemphasized-text-color);
   --diff-highlight-range-color: rgba(255, 213, 0, 0.5);
   --diff-highlight-range-hover-color: rgba(255, 255, 0, 0.5);
-  --diff-blank-background-color: #fff;
+  --diff-selection-background-color: #c7dbf9;
+  --diff-tab-indicator-color: var(--deemphasized-text-color);
+  --diff-trailing-whitespace-indicator: #ff9ad2;
+  --light-add-highlight-color: #d8fed8;
+  --light-rebased-add-highlight-color: #eef;
+  --light-remove-add-highlight-color: #fff8dc;
+  --light-remove-highlight-color: #ffebee;
 
-  --shell-command-background-color: #f5f5f5;
-  --shell-command-decoration-background-color: #ebebeb;
-
-  --comment-text-color: #000;
-  --comment-background-color: #fcfad6;
-  --unresolved-comment-background-color: #fcfaa6;
-
-  --edit-mode-background-color: #ebf5fb;
-
-  --tooltip-background-color: #333;
-  --tooltip-text-color: #fff;
-
-  --syntax-default-color: var(--primary-text-color);
-  --syntax-attribute-color: var(--primary-text-color);
-  --syntax-function-color: var(--primary-text-color);
-  --syntax-meta-color: #FF1717;
-  --syntax-keyword-color: #9E0069;
-  --syntax-number-color: #164;
-  --syntax-selector-class-color: #164;
-  --syntax-variable-color: black;
-  --syntax-template-variable-color: #0000C0;
-  --syntax-comment-color: #3F7F5F;
-  --syntax-string-color: #2A00FF;
-  --syntax-selector-id-color: #2A00FF;
-  --syntax-built_in-color: #30a;
-  --syntax-tag-color: #170;
-  --syntax-link-color: #219;
-  --syntax-meta-keyword-color: #219;
-  --syntax-type-color: var(--color-link);
-  --syntax-title-color: #0000C0;
+  /* syntax colors */
   --syntax-attr-color: #219;
+  --syntax-attribute-color: var(--primary-text-color);
+  --syntax-built_in-color: #30a;
+  --syntax-comment-color: #3f7f5f;
+  --syntax-default-color: var(--primary-text-color);
+  --syntax-function-color: var(--primary-text-color);
+  --syntax-keyword-color: #9e0069;
+  --syntax-link-color: #219;
   --syntax-literal-color: #219;
-  --syntax-selector-pseudo-color: #FA8602;
-  --syntax-regexp-color: #FA8602;
-  --syntax-selector-attr-color: #FA8602;
-  --syntax-template-tag-color: #FA8602;
+  --syntax-meta-color: #ff1717;
+  --syntax-meta-keyword-color: #219;
+  --syntax-number-color: #164;
   --syntax-param-color: var(--primary-text-color);
+  --syntax-regexp-color: #fa8602;
+  --syntax-selector-attr-color: #fa8602;
+  --syntax-selector-class-color: #164;
+  --syntax-selector-id-color: #2a00ff;
+  --syntax-selector-pseudo-color: #fa8602;
+  --syntax-string-color: #2a00ff;
+  --syntax-tag-color: #170;
+  --syntax-template-tag-color: #fa8602;
+  --syntax-template-variable-color: #0000c0;
+  --syntax-title-color: #0000c0;
+  --syntax-type-color: #2a66d9;
+  --syntax-variable-color: var(--primary-text-color);
 
+  /* misc */
   --reply-overlay-z-index: 1000;
+  --iron-overlay-backdrop: {
+    transition: none;
+  };
 }
 @media screen and (max-width: 50em) {
   html {
diff --git a/polygerrit-ui/app/styles/themes/dark-theme.html b/polygerrit-ui/app/styles/themes/dark-theme.html
index 2324fd5..9027ddc 100644
--- a/polygerrit-ui/app/styles/themes/dark-theme.html
+++ b/polygerrit-ui/app/styles/themes/dark-theme.html
@@ -17,97 +17,123 @@
 <dom-module id="dark-theme">
   <custom-style><style is="custom-style">
     html {
+      /**
+       * Sections and variables must stay consistent with app-theme.html.
+       *
+       * Only modify color variables in this theme file. dark-theme extends
+       * app-theme, so there is no need to repeat all variables, but for colors
+       * it does make sense to list them all: If you override one color, then
+       * you probably want to override all.
+       */
+
+      /* text colors */
       --primary-text-color: #e8eaed;
-      --view-background-color: #131416;
-      --border-color: #5f6368;
-      --header-border-bottom: 1px solid var(--border-color);
-      --header-border-image: '';
-      --footer-border-bottom: 1px solid var(--border-color);
+      --link-color: #8ab4f8;
+      --comment-text-color: var(--primary-text-color);
+      --deemphasized-text-color: #9aa0a6;
+      --default-button-text-color: #8ab4f8;
+      --error-text-color: red;
+      --primary-button-text-color: var(--primary-text-color);
+        /* Used on text color for change list doesn't need user's attention. */
+      --reviewed-text-color: #dadce0;
+      --secondary-button-text-color: var(--deemphasized-text-color);
+      --tooltip-text-color: white;
+      --vote-text-color-recommended: #388e3c;
+      --vote-text-color-disliked: #d32f2f;
+
+      /* background colors */
+      --assignee-highlight-color: #3a361c;
+      --chip-background-color: #131416;
+      --comment-background-color: #0b162b;
+      --default-button-background-color: #3c4043;
+      --dialog-background-color: #131416;
+      --dropdown-background-color: #131416;
+      --edit-mode-background-color: #5c0a36;
+      --emphasis-color: #383f4a;
+      --expanded-background-color: #26282b;
+      --hover-background-color: rgba(161, 194, 250, 0.2);
+      --primary-button-background-color: var(--link-color);
+      --secondary-button-background-color: var(--primary-text-color);
+      --select-background-color: #3c4043;
+      --selection-background-color: rgba(161, 194, 250, 0.1);
+      --shell-command-background-color: #5f5f5f;
+      --shell-command-decoration-background-color: #999;
       --table-header-background-color: #131416;
       --table-subheader-background-color: #3c4043;
-      --header-background-color: #3c4043;
-      --header-text-color: var(--primary-text-color);
-      --deemphasized-text-color: #9aa0a6;
-      /* Used on text color for change list doesn't need user's attention. */
-      --reviewed-text-color: #DADCE0;
-      /* Used on text for change list that needs user's attention. */
-      --font-weight-bold: 900;
-      --footer-background-color: var(--table-header-background-color);
-      --expanded-background-color: #26282b;
-      --link-color: #8ab4f8;
-      --primary-button-background-color: var(--link-color);
-      --primary-button-text-color: var(--primary-text-color);
-      --secondary-button-background-color: var(--primary-text-color);
-      --secondary-button-text-color: var(--deemphasized-text-color);
-      --default-button-text-color: var(--link-color);
-      --default-button-background-color: var(--table-subheader-background-color);
-      --dropdown-background-color: var(--table-header-background-color);
-      --dialog-background-color: var(--view-background-color);
-      --chip-background-color: var(--table-header-background-color);
-      --header-title-font-size: 1.75rem;
-
-      --select-background-color: var(--table-subheader-background-color);
-
-      --assignee-highlight-color: rgb(58, 54, 28);
-
-      --diff-selection-background-color: #3A71D8;
-      --light-remove-highlight-color: #320404;
-      --light-add-highlight-color: #0F401F;
-      --light-remove-add-highlight-color: #2f3f2f;
-      --light-rebased-remove-highlight-color: rgb(60, 37, 8);
-      --light-rebased-add-highlight-color: rgb(72, 113, 101);
-      --dark-remove-highlight-color: #62110F;
-      --dark-add-highlight-color: #133820;
-      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
-      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
-      --diff-context-control-color: var(--deemphasized-text-color);
-      --diff-context-control-background-color: var(--table-header-background-color);
-      --diff-context-control-border-color: var(--border-color);
-      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
-      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
-      --shell-command-background-color: #5f5f5f;
-      --shell-command-decoration-background-color: #999999;
-      --comment-text-color: var(--primary-text-color);
-      --comment-background-color: #0B162B;
-      --unresolved-comment-background-color: rgb(56, 90, 154);
-      --diff-blank-background-color: #212121;
-
-      --vote-color-approved: rgb(127, 182, 107);
-      --vote-color-recommended: rgb(63, 103, 50);
-      --vote-color-rejected: #ac2d3e;
+      --tooltip-background-color: #111;
+      --unresolved-comment-background-color: #385a9a;
+      --view-background-color: #131416;
+      --vote-color-approved: #7fb66b;
       --vote-color-disliked: #bf6874;
       --vote-color-neutral: #597280;
+      --vote-color-recommended: #3f6732;
+      --vote-color-rejected: #ac2d3e;
 
-      --edit-mode-background-color: rgb(92, 10, 54);
-      --emphasis-color: #383f4a;
+      /* misc colors */
+      --border-color: #5f6368;
 
-      --tooltip-background-color: #111;
+      /* fonts */
+        /* Used on text for change list that needs user's attention. */
+      --font-weight-bold: 900;
 
-      --syntax-default-color: var(--primary-text-color);
-      --syntax-meta-color: #6D7EEE;
-      --syntax-keyword-color: #CD4CF0;
-      --syntax-number-color: #00998A;
-      --syntax-selector-class-color: #FFCB68;
-      --syntax-variable-color: #F77669;
-      --syntax-template-variable-color: #F77669;
+      /* spacing */
+
+      /* header and footer */
+      --footer-background-color: #131416;
+      --footer-border-top: 1px solid var(--border-color);
+      --header-background-color: #3c4043;
+      --header-border-bottom: 1px solid var(--border-color);
+      --header-text-color: var(--primary-text-color);
+
+      /* diff colors */
+      --dark-add-highlight-color: #133820;
+      --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15);
+      --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15);
+      --dark-remove-highlight-color: #62110f;
+      --diff-blank-background-color: #212121;
+      --diff-context-control-background-color: #131416;
+      --diff-context-control-border-color: var(--border-color);
+      --diff-context-control-color: var(--deemphasized-text-color);
+      --diff-highlight-range-color: rgba(0, 100, 200, 0.5);
+      --diff-highlight-range-hover-color: rgba(0, 150, 255, 0.5);
+      --diff-selection-background-color: #3a71d8;
+      --diff-tab-indicator-color: var(--deemphasized-text-color);
+      --diff-trailing-whitespace-indicator: #ff9ad2;
+      --light-add-highlight-color: #0f401f;
+      --light-rebased-add-highlight-color: #487165;
+      --light-remove-add-highlight-color: #2f3f2f;
+      --light-remove-highlight-color: #320404;
+
+      /* syntax colors */
+      --syntax-attr-color: #80cbbf;
+      --syntax-attribute-color: var(--primary-text-color);
+      --syntax-built_in-color: #f7c369;
       --syntax-comment-color: var(--deemphasized-text-color);
-      --syntax-string-color: #C3E88D;
-      --syntax-selector-id-color: #F77669;
-      --syntax-built_in-color: rgb(247, 195, 105);
-      --syntax-tag-color: #F77669;
-      --syntax-link-color: #C792EA;
-      --syntax-meta-keyword-color: #EEFFF7;
-      --syntax-type-color: #DD5F5F;
-      --syntax-title-color: #75A5FF;
-      --syntax-attr-color: #80CBBF;
-      --syntax-literal-color: #EEFFF7;
-      --syntax-selector-pseudo-color: #C792EA;
-      --syntax-regexp-color: #F77669;
-      --syntax-selector-attr-color: #80CBBF;
-      --syntax-template-tag-color: #C792EA;
+      --syntax-default-color: var(--primary-text-color);
+      --syntax-function-color: var(--primary-text-color);
+      --syntax-keyword-color: #cd4cf0;
+      --syntax-link-color: #c792ea;
+      --syntax-literal-color: #eefff7;
+      --syntax-meta-color: #6d7eee;
+      --syntax-meta-keyword-color: #eefff7;
+      --syntax-number-color: #00998a;
+      --syntax-param-color: var(--primary-text-color);
+      --syntax-regexp-color: #f77669;
+      --syntax-selector-attr-color: #80cbbf;
+      --syntax-selector-class-color: #ffcb68;
+      --syntax-selector-id-color: #f77669;
+      --syntax-selector-pseudo-color: #c792ea;
+      --syntax-string-color: #c3e88d;
+      --syntax-tag-color: #f77669;
+      --syntax-template-tag-color: #c792ea;
+      --syntax-template-variable-color: #f77669;
+      --syntax-title-color: #75a5ff;
+      --syntax-type-color: #dd5f5f;
+      --syntax-variable-color: #f77669;
 
-      --reply-overlay-z-index: 1000;
+      /* misc */
 
+      /* rules applied to <html> */
       background-color: var(--view-background-color);
     }
   </style></custom-style>