Merge "Converting to class and renaming gr-diff-builder element"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html
new file mode 100644
index 0000000..e8e60c4
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.html
@@ -0,0 +1,54 @@
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="/bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
+<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="../../../elements/shared/gr-hovercard/gr-hovercard.html">
+<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
+
+<dom-module id="gr-diff-builder">
+  <template>
+    <div class="contentWrapper">
+      <slot></slot>
+    </div>
+    <gr-ranged-comment-layer
+        id="rangeLayer"
+        comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
+    <gr-coverage-layer
+        id="coverageLayerLeft"
+        coverage-ranges="[[_leftCoverageRanges]]"
+        side="left"></gr-coverage-layer>
+    <gr-coverage-layer
+        id="coverageLayerRight"
+        coverage-ranges="[[_rightCoverageRanges]]"
+        side="right"></gr-coverage-layer>
+    <gr-diff-processor
+        id="processor"
+        groups="{{_groups}}"></gr-diff-processor>
+  </template>
+  <script src="../../../scripts/util.js"></script>
+  <script src="../gr-diff/gr-diff-line.js"></script>
+  <script src="../gr-diff/gr-diff-group.js"></script>
+  <script src="../gr-diff-highlight/gr-annotation.js"></script>
+  <script src="gr-diff-builder.js"></script>
+  <script src="gr-diff-builder-side-by-side.js"></script>
+  <script src="gr-diff-builder-unified.js"></script>
+  <script src="gr-diff-builder-image.js"></script>
+  <script src="gr-diff-builder-binary.js"></script>
+  <script src="gr-diff-builder-element.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
new file mode 100644
index 0000000..a6382c4
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -0,0 +1,389 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+  'use strict';
+
+  const DiffViewMode = {
+    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+    UNIFIED: 'UNIFIED_DIFF',
+  };
+
+  const TRAILING_WHITESPACE_PATTERN = /\s+$/;
+
+  /**
+   * @appliesMixin Gerrit.FireMixin
+   */
+  class GrDiffBuilderElement extends Polymer.mixinBehaviors( [
+    Gerrit.FireBehavior,
+  ], Polymer.GestureEventListeners(
+      Polymer.LegacyElementMixin(
+          Polymer.Element))) {
+    static get is() { return 'gr-diff-builder'; }
+    /**
+     * Fired when the diff begins rendering.
+     *
+     * @event render-start
+     */
+
+    /**
+     * Fired when the diff finishes rendering text content.
+     *
+     * @event render-content
+     */
+
+    static get properties() {
+      return {
+        diff: Object,
+        changeNum: String,
+        patchNum: String,
+        viewMode: String,
+        isImageDiff: Boolean,
+        baseImage: Object,
+        revisionImage: Object,
+        parentIndex: Number,
+        path: String,
+        projectName: String,
+
+        _builder: Object,
+        _groups: Array,
+        _layers: Array,
+        _showTabs: Boolean,
+        /** @type {!Array<!Gerrit.HoveredRange>} */
+        commentRanges: {
+          type: Array,
+          value: () => [],
+        },
+        /** @type {!Array<!Gerrit.CoverageRange>} */
+        coverageRanges: {
+          type: Array,
+          value: () => [],
+        },
+        _leftCoverageRanges: {
+          type: Array,
+          computed: '_computeLeftCoverageRanges(coverageRanges)',
+        },
+        _rightCoverageRanges: {
+          type: Array,
+          computed: '_computeRightCoverageRanges(coverageRanges)',
+        },
+        /**
+         * The promise last returned from `render()` while the asynchronous
+         * rendering is running - `null` otherwise. Provides a `cancel()`
+         * method that rejects it with `{isCancelled: true}`.
+         *
+         * @type {?Object}
+         */
+        _cancelableRenderPromise: Object,
+        layers: {
+          type: Array,
+          value: [],
+        },
+      };
+    }
+
+    get diffElement() {
+      return this.queryEffectiveChildren('#diffTable');
+    }
+
+    static get observers() {
+      return [
+        '_groupsChanged(_groups.splices)',
+      ];
+    }
+
+    _computeLeftCoverageRanges(coverageRanges) {
+      return coverageRanges.filter(range => range && range.side === 'left');
+    }
+
+    _computeRightCoverageRanges(coverageRanges) {
+      return coverageRanges.filter(range => range && range.side === 'right');
+    }
+
+    render(keyLocations, prefs) {
+      // Setting up annotation layers must happen after plugins are
+      // installed, and |render| satisfies the requirement, however,
+      // |attached| doesn't because in the diff view page, the element is
+      // attached before plugins are installed.
+      this._setupAnnotationLayers();
+
+      this._showTabs = !!prefs.show_tabs;
+      this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
+
+      // Stop the processor if it's running.
+      this.cancel();
+
+      this._builder = this._getDiffBuilder(this.diff, prefs);
+
+      this.$.processor.context = prefs.context;
+      this.$.processor.keyLocations = keyLocations;
+
+      this._clearDiffContent();
+      this._builder.addColumns(this.diffElement, prefs.font_size);
+
+      const isBinary = !!(this.isImageDiff || this.diff.binary);
+
+      this.dispatchEvent(new CustomEvent(
+          'render-start', {bubbles: true, composed: true}));
+      this._cancelableRenderPromise = util.makeCancelable(
+          this.$.processor.process(this.diff.content, isBinary)
+              .then(() => {
+                if (this.isImageDiff) {
+                  this._builder.renderDiff();
+                }
+                this.dispatchEvent(new CustomEvent('render-content',
+                    {bubbles: true, composed: true}));
+              }));
+      return this._cancelableRenderPromise
+          .finally(() => { this._cancelableRenderPromise = null; })
+      // Mocca testing does not like uncaught rejections, so we catch
+      // the cancels which are expected and should not throw errors in
+      // tests.
+          .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
+    }
+
+    _setupAnnotationLayers() {
+      const layers = [
+        this._createTrailingWhitespaceLayer(),
+        this._createIntralineLayer(),
+        this._createTabIndicatorLayer(),
+        this.$.rangeLayer,
+        this.$.coverageLayerLeft,
+        this.$.coverageLayerRight,
+      ];
+
+      if (this.layers) {
+        layers.push(...this.layers);
+      }
+      this._layers = layers;
+    }
+
+    getLineElByChild(node) {
+      while (node) {
+        if (node instanceof Element) {
+          if (node.classList.contains('lineNum')) {
+            return node;
+          }
+          if (node.classList.contains('section')) {
+            return null;
+          }
+        }
+        node = node.previousSibling || node.parentElement;
+      }
+      return null;
+    }
+
+    getLineNumberByChild(node) {
+      const lineEl = this.getLineElByChild(node);
+      return lineEl ?
+        parseInt(lineEl.getAttribute('data-value'), 10) :
+        null;
+    }
+
+    getContentByLine(lineNumber, opt_side, opt_root) {
+      return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
+    }
+
+    getContentByLineEl(lineEl) {
+      const root = Polymer.dom(lineEl.parentElement);
+      const side = this.getSideByLineEl(lineEl);
+      const line = lineEl.getAttribute('data-value');
+      return this.getContentByLine(line, side, root);
+    }
+
+    getLineElByNumber(lineNumber, opt_side) {
+      const sideSelector = opt_side ? ('.' + opt_side) : '';
+      return this.diffElement.querySelector(
+          '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
+    }
+
+    getContentsByLineRange(startLine, endLine, opt_side) {
+      const result = [];
+      this._builder.findLinesByRange(startLine, endLine, opt_side, null,
+          result);
+      return result;
+    }
+
+    getSideByLineEl(lineEl) {
+      return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
+        GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
+    }
+
+    emitGroup(group, sectionEl) {
+      this._builder.emitGroup(group, sectionEl);
+    }
+
+    showContext(newGroups, sectionEl) {
+      const groups = this._builder.groups;
+
+      const contextIndex = groups.findIndex(group =>
+        group.element === sectionEl
+      );
+      groups.splice(contextIndex, 1, ...newGroups);
+
+      for (const newGroup of newGroups) {
+        this._builder.emitGroup(newGroup, sectionEl);
+      }
+      sectionEl.parentNode.removeChild(sectionEl);
+
+      this.async(() => this.fire('render-content'), 1);
+    }
+
+    cancel() {
+      this.$.processor.cancel();
+      if (this._cancelableRenderPromise) {
+        this._cancelableRenderPromise.cancel();
+        this._cancelableRenderPromise = null;
+      }
+    }
+
+    _handlePreferenceError(pref) {
+      const message = `The value of the '${pref}' user preference is ` +
+          `invalid. Fix in diff preferences`;
+      this.dispatchEvent(new CustomEvent('show-alert', {
+        detail: {
+          message,
+        }, bubbles: true, composed: true}));
+      throw Error(`Invalid preference value: ${pref}`);
+    }
+
+    _getDiffBuilder(diff, prefs) {
+      if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
+        this._handlePreferenceError('tab size');
+        return;
+      }
+
+      if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
+        this._handlePreferenceError('diff width');
+        return;
+      }
+
+      let builder = null;
+      if (this.isImageDiff) {
+        builder = new GrDiffBuilderImage(diff, prefs, this.diffElement,
+            this.baseImage, this.revisionImage);
+      } else if (diff.binary) {
+        // If the diff is binary, but not an image.
+        return new GrDiffBuilderBinary(diff, prefs, this.diffElement);
+      } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
+        builder = new GrDiffBuilderSideBySide(diff, prefs, this.diffElement,
+            this._layers);
+      } else if (this.viewMode === DiffViewMode.UNIFIED) {
+        builder = new GrDiffBuilderUnified(diff, prefs, this.diffElement,
+            this._layers);
+      }
+      if (!builder) {
+        throw Error('Unsupported diff view mode: ' + this.viewMode);
+      }
+      return builder;
+    }
+
+    _clearDiffContent() {
+      this.diffElement.innerHTML = null;
+    }
+
+    _groupsChanged(changeRecord) {
+      if (!changeRecord) { return; }
+      for (const splice of changeRecord.indexSplices) {
+        let group;
+        for (let i = 0; i < splice.addedCount; i++) {
+          group = splice.object[splice.index + i];
+          this._builder.groups.push(group);
+          this._builder.emitGroup(group);
+        }
+      }
+    }
+
+    _createIntralineLayer() {
+      return {
+        // Take a DIV.contentText element and a line object with intraline
+        // differences to highlight and apply them to the element as
+        // annotations.
+        annotate(contentEl, lineNumberEl, line) {
+          const HL_CLASS = 'style-scope gr-diff intraline';
+          for (const highlight of line.highlights) {
+            // The start and end indices could be the same if a highlight is
+            // meant to start at the end of a line and continue onto the
+            // next one. Ignore it.
+            if (highlight.startIndex === highlight.endIndex) { continue; }
+
+            // If endIndex isn't present, continue to the end of the line.
+            const endIndex = highlight.endIndex === undefined ?
+              line.text.length :
+              highlight.endIndex;
+
+            GrAnnotation.annotateElement(
+                contentEl,
+                highlight.startIndex,
+                endIndex - highlight.startIndex,
+                HL_CLASS);
+          }
+        },
+      };
+    }
+
+    _createTabIndicatorLayer() {
+      const show = () => this._showTabs;
+      return {
+        annotate(contentEl, lineNumberEl, line) {
+          // If visible tabs are disabled, do nothing.
+          if (!show()) { return; }
+
+          // Find and annotate the locations of tabs.
+          const split = line.text.split('\t');
+          if (!split) { return; }
+          for (let i = 0, pos = 0; i < split.length - 1; i++) {
+            // Skip forward by the length of the content
+            pos += split[i].length;
+
+            GrAnnotation.annotateElement(contentEl, pos, 1,
+                'style-scope gr-diff tab-indicator');
+
+            // Skip forward by one tab character.
+            pos++;
+          }
+        },
+      };
+    }
+
+    _createTrailingWhitespaceLayer() {
+      const show = function() {
+        return this._showTrailingWhitespace;
+      }.bind(this);
+
+      return {
+        annotate(contentEl, lineNumberEl, line) {
+          if (!show()) { return; }
+
+          const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
+          if (match) {
+            // Normalize string positions in case there is unicode before or
+            // within the match.
+            const index = GrAnnotation.getStringLength(
+                line.text.substr(0, match.index));
+            const length = GrAnnotation.getStringLength(match[0]);
+            GrAnnotation.annotateElement(contentEl, index, length,
+                'style-scope gr-diff trailing-whitespace');
+          }
+        },
+      };
+    }
+
+    setBlame(blame) {
+      if (!this._builder || !blame) { return; }
+      this._builder.setBlame(blame);
+    }
+  }
+
+  customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
+})();
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-element_test.html
similarity index 99%
rename from polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index 838ac68..6a9d903 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-element_test.html
@@ -33,7 +33,7 @@
 
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
-<link rel="import" href="gr-diff-builder.html">
+<link rel="import" href="gr-diff-builder-element.html">
 
 <script>void(0);</script>
 
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
deleted file mode 100644
index 021d962..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ /dev/null
@@ -1,423 +0,0 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<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="../../../elements/shared/gr-hovercard/gr-hovercard.html">
-<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
-
-<dom-module id="gr-diff-builder">
-  <template>
-    <div class="contentWrapper">
-      <slot></slot>
-    </div>
-    <gr-ranged-comment-layer
-        id="rangeLayer"
-        comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
-    <gr-coverage-layer
-        id="coverageLayerLeft"
-        coverage-ranges="[[_leftCoverageRanges]]"
-        side="left"></gr-coverage-layer>
-    <gr-coverage-layer
-        id="coverageLayerRight"
-        coverage-ranges="[[_rightCoverageRanges]]"
-        side="right"></gr-coverage-layer>
-    <gr-diff-processor
-        id="processor"
-        groups="{{_groups}}"></gr-diff-processor>
-  </template>
-  <script src="../../../scripts/util.js"></script>
-  <script src="../gr-diff/gr-diff-line.js"></script>
-  <script src="../gr-diff/gr-diff-group.js"></script>
-  <script src="../gr-diff-highlight/gr-annotation.js"></script>
-  <script src="gr-diff-builder.js"></script>
-  <script src="gr-diff-builder-side-by-side.js"></script>
-  <script src="gr-diff-builder-unified.js"></script>
-  <script src="gr-diff-builder-image.js"></script>
-  <script src="gr-diff-builder-binary.js"></script>
-  <script>
-    (function() {
-      'use strict';
-
-      const DiffViewMode = {
-        SIDE_BY_SIDE: 'SIDE_BY_SIDE',
-        UNIFIED: 'UNIFIED_DIFF',
-      };
-
-      const TRAILING_WHITESPACE_PATTERN = /\s+$/;
-
-      Polymer({
-        is: 'gr-diff-builder',
-
-        /**
-         * Fired when the diff begins rendering.
-         *
-         * @event render-start
-         */
-
-        /**
-         * Fired when the diff finishes rendering text content.
-         *
-         * @event render-content
-         */
-
-        properties: {
-          diff: Object,
-          changeNum: String,
-          patchNum: String,
-          viewMode: String,
-          isImageDiff: Boolean,
-          baseImage: Object,
-          revisionImage: Object,
-          parentIndex: Number,
-          path: String,
-          projectName: String,
-
-          _builder: Object,
-          _groups: Array,
-          _layers: Array,
-          _showTabs: Boolean,
-          /** @type {!Array<!Gerrit.HoveredRange>} */
-          commentRanges: {
-            type: Array,
-            value: () => [],
-          },
-          /** @type {!Array<!Gerrit.CoverageRange>} */
-          coverageRanges: {
-            type: Array,
-            value: () => [],
-          },
-          _leftCoverageRanges: {
-            type: Array,
-            computed: '_computeLeftCoverageRanges(coverageRanges)',
-          },
-          _rightCoverageRanges: {
-            type: Array,
-            computed: '_computeRightCoverageRanges(coverageRanges)',
-          },
-          /**
-           * The promise last returned from `render()` while the asynchronous
-           * rendering is running - `null` otherwise. Provides a `cancel()`
-           * method that rejects it with `{isCancelled: true}`.
-           *
-           * @type {?Object}
-           */
-          _cancelableRenderPromise: Object,
-          layers: {
-            type: Array,
-            value: [],
-          },
-        },
-
-        behaviors: [
-          Gerrit.FireBehavior,
-        ],
-
-        get diffElement() {
-          return this.queryEffectiveChildren('#diffTable');
-        },
-
-        observers: [
-          '_groupsChanged(_groups.splices)',
-        ],
-
-        _computeLeftCoverageRanges(coverageRanges) {
-          return coverageRanges.filter(range => range && range.side === 'left');
-        },
-
-        _computeRightCoverageRanges(coverageRanges) {
-          return coverageRanges.filter(range => range && range.side === 'right');
-        },
-
-        render(keyLocations, prefs) {
-          // Setting up annotation layers must happen after plugins are
-          // installed, and |render| satisfies the requirement, however,
-          // |attached| doesn't because in the diff view page, the element is
-          // attached before plugins are installed.
-          this._setupAnnotationLayers();
-
-          this._showTabs = !!prefs.show_tabs;
-          this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
-
-          // Stop the processor if it's running.
-          this.cancel();
-
-          this._builder = this._getDiffBuilder(this.diff, prefs);
-
-          this.$.processor.context = prefs.context;
-          this.$.processor.keyLocations = keyLocations;
-
-          this._clearDiffContent();
-          this._builder.addColumns(this.diffElement, prefs.font_size);
-
-          const isBinary = !!(this.isImageDiff || this.diff.binary);
-
-          this.dispatchEvent(new CustomEvent(
-              'render-start', {bubbles: true, composed: true}));
-          this._cancelableRenderPromise = util.makeCancelable(
-              this.$.processor.process(this.diff.content, isBinary)
-                  .then(() => {
-                    if (this.isImageDiff) {
-                      this._builder.renderDiff();
-                    }
-                    this.dispatchEvent(new CustomEvent('render-content',
-                        {bubbles: true, composed: true}));
-                  }));
-          return this._cancelableRenderPromise
-              .finally(() => { this._cancelableRenderPromise = null; })
-              // Mocca testing does not like uncaught rejections, so we catch
-              // the cancels which are expected and should not throw errors in
-              // tests.
-              .catch(e => { if (!e.isCanceled) return Promise.reject(e); });
-        },
-
-        _setupAnnotationLayers() {
-          const layers = [
-            this._createTrailingWhitespaceLayer(),
-            this._createIntralineLayer(),
-            this._createTabIndicatorLayer(),
-            this.$.rangeLayer,
-            this.$.coverageLayerLeft,
-            this.$.coverageLayerRight,
-          ];
-
-          if (this.layers) {
-            layers.push(...this.layers);
-          }
-          this._layers = layers;
-        },
-
-        getLineElByChild(node) {
-          while (node) {
-            if (node instanceof Element) {
-              if (node.classList.contains('lineNum')) {
-                return node;
-              }
-              if (node.classList.contains('section')) {
-                return null;
-              }
-            }
-            node = node.previousSibling || node.parentElement;
-          }
-          return null;
-        },
-
-        getLineNumberByChild(node) {
-          const lineEl = this.getLineElByChild(node);
-          return lineEl ?
-            parseInt(lineEl.getAttribute('data-value'), 10) :
-            null;
-        },
-
-        getContentByLine(lineNumber, opt_side, opt_root) {
-          return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
-        },
-
-        getContentByLineEl(lineEl) {
-          const root = Polymer.dom(lineEl.parentElement);
-          const side = this.getSideByLineEl(lineEl);
-          const line = lineEl.getAttribute('data-value');
-          return this.getContentByLine(line, side, root);
-        },
-
-        getLineElByNumber(lineNumber, opt_side) {
-          const sideSelector = opt_side ? ('.' + opt_side) : '';
-          return this.diffElement.querySelector(
-              '.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
-        },
-
-        getContentsByLineRange(startLine, endLine, opt_side) {
-          const result = [];
-          this._builder.findLinesByRange(startLine, endLine, opt_side, null,
-              result);
-          return result;
-        },
-
-        getSideByLineEl(lineEl) {
-          return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
-            GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
-        },
-
-        emitGroup(group, sectionEl) {
-          this._builder.emitGroup(group, sectionEl);
-        },
-
-        showContext(newGroups, sectionEl) {
-          const groups = this._builder.groups;
-
-          const contextIndex = groups.findIndex(group =>
-            group.element === sectionEl
-          );
-          groups.splice(contextIndex, 1, ...newGroups);
-
-          for (const newGroup of newGroups) {
-            this._builder.emitGroup(newGroup, sectionEl);
-          }
-          sectionEl.parentNode.removeChild(sectionEl);
-
-          this.async(() => this.fire('render-content'), 1);
-        },
-
-        cancel() {
-          this.$.processor.cancel();
-          if (this._cancelableRenderPromise) {
-            this._cancelableRenderPromise.cancel();
-            this._cancelableRenderPromise = null;
-          }
-        },
-
-        _handlePreferenceError(pref) {
-          const message = `The value of the '${pref}' user preference is ` +
-              `invalid. Fix in diff preferences`;
-          this.dispatchEvent(new CustomEvent('show-alert', {
-            detail: {
-              message,
-            }, bubbles: true, composed: true}));
-          throw Error(`Invalid preference value: ${pref}`);
-        },
-
-        _getDiffBuilder(diff, prefs) {
-          if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
-            this._handlePreferenceError('tab size');
-            return;
-          }
-
-          if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
-            this._handlePreferenceError('diff width');
-            return;
-          }
-
-          let builder = null;
-          if (this.isImageDiff) {
-            builder = new GrDiffBuilderImage(diff, prefs, this.diffElement,
-                this.baseImage, this.revisionImage);
-          } else if (diff.binary) {
-            // If the diff is binary, but not an image.
-            return new GrDiffBuilderBinary(diff, prefs, this.diffElement);
-          } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
-            builder = new GrDiffBuilderSideBySide(diff, prefs, this.diffElement,
-                this._layers);
-          } else if (this.viewMode === DiffViewMode.UNIFIED) {
-            builder = new GrDiffBuilderUnified(diff, prefs, this.diffElement,
-                this._layers);
-          }
-          if (!builder) {
-            throw Error('Unsupported diff view mode: ' + this.viewMode);
-          }
-          return builder;
-        },
-
-        _clearDiffContent() {
-          this.diffElement.innerHTML = null;
-        },
-
-        _groupsChanged(changeRecord) {
-          if (!changeRecord) { return; }
-          for (const splice of changeRecord.indexSplices) {
-            let group;
-            for (let i = 0; i < splice.addedCount; i++) {
-              group = splice.object[splice.index + i];
-              this._builder.groups.push(group);
-              this._builder.emitGroup(group);
-            }
-          }
-        },
-
-        _createIntralineLayer() {
-          return {
-            // Take a DIV.contentText element and a line object with intraline
-            // differences to highlight and apply them to the element as
-            // annotations.
-            annotate(contentEl, lineNumberEl, line) {
-              const HL_CLASS = 'style-scope gr-diff intraline';
-              for (const highlight of line.highlights) {
-                // The start and end indices could be the same if a highlight is
-                // meant to start at the end of a line and continue onto the
-                // next one. Ignore it.
-                if (highlight.startIndex === highlight.endIndex) { continue; }
-
-                // If endIndex isn't present, continue to the end of the line.
-                const endIndex = highlight.endIndex === undefined ?
-                  line.text.length :
-                  highlight.endIndex;
-
-                GrAnnotation.annotateElement(
-                    contentEl,
-                    highlight.startIndex,
-                    endIndex - highlight.startIndex,
-                    HL_CLASS);
-              }
-            },
-          };
-        },
-
-        _createTabIndicatorLayer() {
-          const show = () => this._showTabs;
-          return {
-            annotate(contentEl, lineNumberEl, line) {
-              // If visible tabs are disabled, do nothing.
-              if (!show()) { return; }
-
-              // Find and annotate the locations of tabs.
-              const split = line.text.split('\t');
-              if (!split) { return; }
-              for (let i = 0, pos = 0; i < split.length - 1; i++) {
-                // Skip forward by the length of the content
-                pos += split[i].length;
-
-                GrAnnotation.annotateElement(contentEl, pos, 1,
-                    'style-scope gr-diff tab-indicator');
-
-                // Skip forward by one tab character.
-                pos++;
-              }
-            },
-          };
-        },
-
-        _createTrailingWhitespaceLayer() {
-          const show = function() {
-            return this._showTrailingWhitespace;
-          }.bind(this);
-
-          return {
-            annotate(contentEl, lineNumberEl, line) {
-              if (!show()) { return; }
-
-              const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
-              if (match) {
-                // Normalize string positions in case there is unicode before or
-                // within the match.
-                const index = GrAnnotation.getStringLength(
-                    line.text.substr(0, match.index));
-                const length = GrAnnotation.getStringLength(match[0]);
-                GrAnnotation.annotateElement(contentEl, index, length,
-                    'style-scope gr-diff trailing-whitespace');
-              }
-            },
-          };
-        },
-
-        setBlame(blame) {
-          if (!this._builder || !blame) { return; }
-          this._builder.setBlame(blame);
-        },
-      });
-    })();
-  </script>
-</dom-module>
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 590537d..003be05 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -20,7 +20,7 @@
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
+<link rel="import" href="../gr-diff-builder/gr-diff-builder-element.html">
 <link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
 <link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
 <link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html">
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 7decb61..38dde97 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -112,7 +112,7 @@
     'core/gr-smart-search/gr-smart-search_test.html',
     'diff/gr-comment-api/gr-comment-api_test.html',
     'diff/gr-coverage-layer/gr-coverage-layer_test.html',
-    'diff/gr-diff-builder/gr-diff-builder_test.html',
+    'diff/gr-diff-builder/gr-diff-builder-element_test.html',
     'diff/gr-diff-builder/gr-diff-builder-unified_test.html',
     'diff/gr-diff-cursor/gr-diff-cursor_test.html',
     'diff/gr-diff-highlight/gr-annotation_test.html',