Move gr-diff-new to gr-diff

Change-Id: Ifaad016f806c31f3df43143b3238b757faa18b20
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index 39477bc..d293060 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -44,7 +44,6 @@
             draft="[[comment.__draft]]"
             show-actions="[[_showActions]]"
             project-config="[[projectConfig]]"
-            on-height-change="_handleCommentHeightChange"
             on-reply="_handleCommentReply"
             on-comment-discard="_handleCommentDiscard"
             on-done="_handleCommentDone"></gr-diff-comment>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index c7bbff2..b827d26 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -18,12 +18,6 @@
     is: 'gr-diff-comment-thread',
 
     /**
-     * Fired when the height of the thread changes.
-     *
-     * @event height-change
-     */
-
-    /**
      * Fired when the thread should be discarded.
      *
      * @event thread-discard
@@ -44,11 +38,6 @@
       },
 
       _showActions: Boolean,
-      _boundWindowResizeHandler: {
-        type: Function,
-        value: function() { return this._handleWindowResize.bind(this); }
-      },
-      _lastHeight: Number,
       _orderedComments: Array,
     },
 
@@ -64,12 +53,6 @@
       this._getLoggedIn().then(function(loggedIn) {
         this._showActions = loggedIn;
       }.bind(this));
-
-      window.addEventListener('resize', this._boundWindowResizeHandler);
-    },
-
-    detached: function() {
-      window.removeEventListener('resize', this._boundWindowResizeHandler);
     },
 
     addDraft: function(opt_lineNum) {
@@ -88,10 +71,6 @@
       return this.$.restAPI.getLoggedIn();
     },
 
-    _handleWindowResize: function(e) {
-      this._heightChanged();
-    },
-
     _commentsChanged: function(changeRecord) {
       this._orderedComments = this._sortedComments(this.comments);
     },
@@ -133,11 +112,6 @@
       }
     },
 
-    _handleCommentHeightChange: function(e) {
-      e.stopPropagation();
-      this._heightChanged();
-    },
-
     _handleCommentReply: function(e) {
       var comment = e.detail.comment;
       var quoteStr;
@@ -153,7 +127,6 @@
       this.async(function() {
         var commentEl = this._commentElWithDraftID(reply.__draftID);
         commentEl.editing = true;
-        this.async(this._heightChanged.bind(this), 1);
       }.bind(this), 1);
     },
 
@@ -166,7 +139,6 @@
       this.async(function() {
         var commentEl = this._commentElWithDraftID(reply.__draftID);
         commentEl.save();
-        this.async(this._heightChanged.bind(this), 1);
       }.bind(this), 1);
     },
 
@@ -215,15 +187,6 @@
         this.fire('thread-discard');
         return;
       }
-      this.async(this._heightChanged.bind(this), 1);
-    },
-
-    _heightChanged: function() {
-      var height = this.$.container.offsetHeight;
-      if (height == this._lastHeight) { return; }
-
-      this.fire('height-change', {height: height}, {bubbles: false});
-      this._lastHeight = height;
     },
 
     _indexOf: function(comment, arr) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index ca6815b..a6a8c4d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -129,7 +129,6 @@
           disabled="{{disabled}}"
           rows="4"
           bind-value="{{_editDraft}}"
-          on-keyup="_handleTextareaKeyup"
           on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea>
       <gr-linked-text class="message"
           pre
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index dcdb881..81449b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -18,12 +18,6 @@
     is: 'gr-diff-comment',
 
     /**
-     * Fired when the height of the comment changes.
-     *
-     * @event height-change
-     */
-
-    /**
      * Fired when the Reply action is triggered.
      *
      * @event reply
@@ -75,10 +69,6 @@
       this.editing = this._editDraft.length == 0;
     },
 
-    attached: function() {
-      this._heightChanged();
-    },
-
     save: function() {
       this.comment.message = this._editDraft;
       this.disabled = true;
@@ -101,13 +91,6 @@
       }.bind(this));
     },
 
-    _heightChanged: function() {
-      this.async(function() {
-        this.fire('height-change', {height: this.offsetHeight},
-            {bubbles: false});
-      }.bind(this));
-    },
-
     _draftChanged: function(draft) {
       this.$.container.classList.toggle('draft', draft);
     },
@@ -126,7 +109,6 @@
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
       }
-      this._heightChanged();
     },
 
     _computeLinkToComment: function(comment) {
@@ -137,12 +119,6 @@
       return draft == null || draft.trim() == '';
     },
 
-    _handleTextareaKeyup: function(e) {
-      // TODO(andybons): This isn't always true, but I can't currently think
-      // of a better metric.
-      this._heightChanged();
-    },
-
     _handleTextareaKeydown: function(e) {
       if (e.keyCode == 27) {  // 'esc'
         this._handleCancel(e);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
deleted file mode 100644
index 972dc2d..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.html
+++ /dev/null
@@ -1,97 +0,0 @@
-<!--
-Copyright (C) 2015 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="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-
-<dom-module id="gr-diff-side">
-  <template>
-    <style>
-      :host,
-      .container {
-        display: flex;
-        flex: 0 0 auto;
-      }
-      .lineNum:before,
-      .code:before {
-        /* To ensure the height is non-zero in these elements, a
-           zero-width space is set as its content. The character
-           itself doesn't matter. Just that there is something
-           there. */
-        content: '\200B';
-      }
-      .lineNum {
-        background-color: #eee;
-        color: #666;
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .canComment .lineNum {
-        cursor: pointer;
-        text-decoration: underline;
-      }
-      .canComment .lineNum:hover {
-        background-color: #ccc;
-      }
-      .lightHighlight {
-        background-color: var(--light-highlight-color);
-      }
-      hl,
-      .darkHighlight {
-        background-color: var(--dark-highlight-color);
-      }
-      .br:after {
-        /* Line feed */
-        content: '\A';
-      }
-      .tab {
-        display: inline-block;
-      }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
-      }
-      .numbers,
-      .content {
-        white-space: pre;
-      }
-      .numbers .filler {
-        background-color: #eee;
-      }
-      .contextControl {
-        background-color: #fef;
-      }
-      .contextControl a:link,
-      .contextControl a:visited {
-        display: block;
-        text-decoration: none;
-      }
-      .numbers .contextControl {
-        padding: 0 .75em;
-        text-align: right;
-      }
-      .content .contextControl {
-        text-align: center;
-      }
-    </style>
-    <div class$="[[_computeContainerClass(canComment)]]">
-      <div class="numbers" id="numbers"></div>
-      <div class="content" id="content"></div>
-    </div>
-  </template>
-  <script src="gr-diff-side.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
deleted file mode 100644
index b97d7b7..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side.js
+++ /dev/null
@@ -1,612 +0,0 @@
-// 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.
-(function() {
-  'use strict';
-
-  var CharCode = {
-    LESS_THAN: '<'.charCodeAt(0),
-    GREATER_THAN: '>'.charCodeAt(0),
-    AMPERSAND: '&'.charCodeAt(0),
-    SEMICOLON: ';'.charCodeAt(0),
-  };
-
-  var TAB_REGEX = /\t/g;
-
-  Polymer({
-    is: 'gr-diff-side',
-
-    /**
-     * Fired when an expand context control is clicked.
-     *
-     * @event expand-context
-     */
-
-    /**
-     * Fired when a thread's height is changed.
-     *
-     * @event thread-height-change
-     */
-
-    /**
-     * Fired when a draft should be added.
-     *
-     * @event add-draft
-     */
-
-    /**
-     * Fired when a thread is removed.
-     *
-     * @event remove-thread
-     */
-
-    properties: {
-      canComment: {
-        type: Boolean,
-        value: false,
-      },
-      content: {
-        type: Array,
-        notify: true,
-        observer: '_contentChanged',
-      },
-      prefs: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      changeNum: String,
-      patchNum: String,
-      path: String,
-      projectConfig: {
-        type: Object,
-        observer: '_projectConfigChanged',
-      },
-
-      _lineFeedHTML: {
-        type: String,
-        value: '<span class="style-scope gr-diff-side br"></span>',
-        readOnly: true,
-      },
-      _highlightStartTag: {
-        type: String,
-        value: '<hl class="style-scope gr-diff-side">',
-        readOnly: true,
-      },
-      _highlightEndTag: {
-        type: String,
-        value: '</hl>',
-        readOnly: true,
-      },
-      _diffChunkLineNums: {
-        type: Array,
-        value: function() { return []; },
-      },
-      _commentThreadLineNums: {
-        type: Array,
-        value: function() { return []; },
-      },
-      _focusedLineNum: {
-        type: Number,
-        value: 1,
-      },
-    },
-
-    listeners: {
-      'tap': '_tapHandler',
-    },
-
-    observers: [
-      '_prefsChanged(prefs.*)',
-    ],
-
-    rowInserted: function(index) {
-      this.renderLineIndexRange(index, index);
-      this._updateDOMIndices();
-      this._updateJumpIndices();
-    },
-
-    rowRemoved: function(index) {
-      var removedEls = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < removedEls.length; i++) {
-        removedEls[i].parentNode.removeChild(removedEls[i]);
-      }
-      this._updateDOMIndices();
-      this._updateJumpIndices();
-    },
-
-    rowUpdated: function(index) {
-      var removedEls = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < removedEls.length; i++) {
-        removedEls[i].parentNode.removeChild(removedEls[i]);
-      }
-      this.renderLineIndexRange(index, index);
-    },
-
-    scrollToLine: function(lineNum) {
-      if (isNaN(lineNum) || lineNum < 1) { return; }
-
-      var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
-      if (!el) { return; }
-
-      // Calculate where the line is relative to the window.
-      var top = el.offsetTop;
-      for (var offsetParent = el.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-
-      // Scroll the element to the middle of the window. Dividing by a third
-      // instead of half the inner height feels a bit better otherwise the
-      // element appears to be below the center of the window even when it
-      // isn't.
-      window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
-    },
-
-    scrollToNextDiffChunk: function() {
-      this._scrollToNextChunkOrThread(this._diffChunkLineNums);
-    },
-
-    scrollToPreviousDiffChunk: function() {
-      this._scrollToPreviousChunkOrThread(this._diffChunkLineNums);
-    },
-
-    scrollToNextCommentThread: function() {
-      this._scrollToNextChunkOrThread(this._commentThreadLineNums);
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this._scrollToPreviousChunkOrThread(this._commentThreadLineNums);
-    },
-
-    renderLineIndexRange: function(startIndex, endIndex) {
-      this._render(this.content, startIndex, endIndex);
-    },
-
-    hideElementsWithIndex: function(index) {
-      var els = Polymer.dom(this.root).querySelectorAll(
-          '[data-index="' + index + '"]');
-      for (var i = 0; i < els.length; i++) {
-        els[i].setAttribute('hidden', true);
-      }
-    },
-
-    getRowHeight: function(index) {
-      var row = this.content[index];
-      // Filler elements should not be taken into account when determining
-      // height calculations.
-      if (row.type == 'FILLER') {
-        return 0;
-      }
-      if (row.height != null) {
-        return row.height;
-      }
-
-      var selector = '[data-index="' + index + '"]';
-      var els = Polymer.dom(this.root).querySelectorAll(selector);
-      if (els.length != 2) {
-        throw Error('Rows should only consist of two elements');
-      }
-      return Math.max(els[0].offsetHeight, els[1].offsetHeight);
-    },
-
-    getRowNaturalHeight: function(index) {
-      var contentEl = this.$$('.content [data-index="' + index + '"]');
-      return contentEl.naturalHeight || contentEl.offsetHeight;
-    },
-
-    setRowNaturalHeight: function(index) {
-      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-      var contentEl = this.$$('.content [data-index="' + index + '"]');
-      contentEl.style.height = null;
-      var height = contentEl.offsetHeight;
-      lineEl.style.height = height + 'px';
-      this.content[index].height = height;
-      return height;
-    },
-
-    setRowHeight: function(index, height) {
-      var selector = '[data-index="' + index + '"]';
-      var els = Polymer.dom(this.root).querySelectorAll(selector);
-      for (var i = 0; i < els.length; i++) {
-        els[i].style.height = height + 'px';
-      }
-      this.content[index].height = height;
-    },
-
-    _scrollToNextChunkOrThread: function(lineNums) {
-      for (var i = 0; i < lineNums.length; i++) {
-        if (lineNums[i] > this._focusedLineNum) {
-          this._focusedLineNum = lineNums[i];
-          this.scrollToLine(this._focusedLineNum);
-          return;
-        }
-      }
-    },
-
-    _scrollToPreviousChunkOrThread: function(lineNums) {
-      for (var i = lineNums.length - 1; i >= 0; i--) {
-        if (this._focusedLineNum > lineNums[i]) {
-          this._focusedLineNum = lineNums[i];
-          this.scrollToLine(this._focusedLineNum);
-          return;
-        }
-      }
-    },
-
-    _updateJumpIndices: function() {
-      this._commentThreadLineNums = [];
-      this._diffChunkLineNums = [];
-      var inHighlight = false;
-      for (var i = 0; i < this.content.length; i++) {
-        switch (this.content[i].type) {
-          case 'COMMENT_THREAD':
-            this._commentThreadLineNums.push(
-                this.content[i].comments[0].line);
-            break;
-          case 'CODE':
-            // Only grab the first line of the highlighted chunk.
-            if (!inHighlight && this.content[i].highlight) {
-              this._diffChunkLineNums.push(this.content[i].lineNum);
-              inHighlight = true;
-            } else if (!this.content[i].highlight) {
-              inHighlight = false;
-            }
-            break;
-        }
-      }
-    },
-
-    _updateDOMIndices: function() {
-      // There is no way to select elements with a data-index greater than a
-      // given value. For now, just update all DOM elements.
-      var lineEls = Polymer.dom(this.root).querySelectorAll(
-          '.numbers [data-index]');
-      var contentEls = Polymer.dom(this.root).querySelectorAll(
-          '.content [data-index]');
-      if (lineEls.length != contentEls.length) {
-        throw Error(
-            'There must be the same number of line and content elements');
-      }
-      var index = 0;
-      for (var i = 0; i < this.content.length; i++) {
-        if (this.content[i].hidden) { continue; }
-
-        lineEls[index].setAttribute('data-index', i);
-        contentEls[index].setAttribute('data-index', i);
-        index++;
-      }
-    },
-
-    _prefsChanged: function(changeRecord) {
-      var prefs = changeRecord.base;
-      this.$.content.style.width = prefs.line_length + 'ch';
-    },
-
-    _projectConfigChanged: function(projectConfig) {
-      var threadEls =
-          Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
-      for (var i = 0; i < threadEls.length; i++) {
-        threadEls[i].projectConfig = projectConfig;
-      }
-    },
-
-    _contentChanged: function(diff) {
-      this._clearChildren(this.$.numbers);
-      this._clearChildren(this.$.content);
-      this._render(diff, 0, diff.length - 1);
-      this._updateJumpIndices();
-    },
-
-    _computeContainerClass: function(canComment) {
-      return 'container' + (canComment ? ' canComment' : '');
-    },
-
-    _tapHandler: function(e) {
-      var lineEl = Polymer.dom(e).rootTarget;
-      if (!this.canComment || !lineEl.classList.contains('lineNum')) {
-        return;
-      }
-
-      e.preventDefault();
-      var index = parseInt(lineEl.getAttribute('data-index'), 10);
-      var line = parseInt(lineEl.getAttribute('data-line-num'), 10);
-      this.fire('add-draft', {
-        index: index,
-        line: line
-      }, {bubbles: false});
-    },
-
-    _clearChildren: function(el) {
-      while (el.firstChild) {
-        el.removeChild(el.firstChild);
-      }
-    },
-
-    _handleContextControlClick: function(context, e) {
-      e.preventDefault();
-      this.fire('expand-context', {context: context}, {bubbles: false});
-    },
-
-    _render: function(diff, startIndex, endIndex) {
-      var beforeLineEl;
-      var beforeContentEl;
-      if (endIndex != diff.length - 1) {
-        beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
-        beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
-        if (!beforeLineEl && !beforeContentEl) {
-          // `endIndex` may be present within the model, but not in the DOM.
-          // Insert it before its successive element.
-          beforeLineEl = this.$$(
-              '.numbers [data-index="' + (endIndex + 1) + '"]');
-          beforeContentEl = this.$$(
-              '.content [data-index="' + (endIndex + 1) + '"]');
-        }
-      }
-
-      for (var i = startIndex; i <= endIndex; i++) {
-        if (diff[i].hidden) { continue; }
-
-        switch (diff[i].type) {
-          case 'CODE':
-            this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
-            break;
-          case 'FILLER':
-            this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
-            break;
-          case 'CONTEXT_CONTROL':
-            this._renderContextControl(diff[i], i, beforeLineEl,
-                beforeContentEl);
-            break;
-          case 'COMMENT_THREAD':
-            this._renderCommentThread(diff[i], i, beforeLineEl,
-                beforeContentEl);
-            break;
-        }
-      }
-    },
-
-    _handleCommentThreadHeightChange: function(e) {
-      var threadEl = Polymer.dom(e).rootTarget;
-      var index = parseInt(threadEl.getAttribute('data-index'), 10);
-      this.content[index].height = e.detail.height;
-      var lineEl = this.$$('.numbers [data-index="' + index + '"]');
-      lineEl.style.height = e.detail.height + 'px';
-      this.fire('thread-height-change', {
-        index: index,
-        height: e.detail.height,
-      }, {bubbles: false});
-    },
-
-    _handleCommentThreadDiscard: function(e) {
-      var threadEl = Polymer.dom(e).rootTarget;
-      var index = parseInt(threadEl.getAttribute('data-index'), 10);
-      this.fire('remove-thread', {index: index}, {bubbles: false});
-    },
-
-    _renderCommentThread: function(thread, index, beforeLineEl,
-        beforeContentEl) {
-      var lineEl = this._createElement('div', 'commentThread');
-      lineEl.classList.add('filler');
-      lineEl.setAttribute('data-index', index);
-      var threadEl = document.createElement('gr-diff-comment-thread');
-      threadEl.addEventListener('height-change',
-          this._handleCommentThreadHeightChange.bind(this));
-      threadEl.addEventListener('thread-discard',
-          this._handleCommentThreadDiscard.bind(this));
-      threadEl.setAttribute('data-index', index);
-      threadEl.changeNum = this.changeNum;
-      threadEl.patchNum = thread.patchNum || this.patchNum;
-      threadEl.path = this.path;
-      threadEl.comments = thread.comments;
-      threadEl.projectConfig = this.projectConfig;
-
-      this.$.numbers.insertBefore(lineEl, beforeLineEl);
-      this.$.content.insertBefore(threadEl, beforeContentEl);
-    },
-
-    _renderContextControl: function(control, index, beforeLineEl,
-        beforeContentEl) {
-      var lineEl = this._createElement('div', 'contextControl');
-      lineEl.setAttribute('data-index', index);
-      lineEl.textContent = '@@';
-      var contentEl = this._createElement('div', 'contextControl');
-      contentEl.setAttribute('data-index', index);
-      var a = this._createElement('a');
-      a.href = '#';
-      a.textContent = 'Show ' + control.numLines + ' common ' +
-          (control.numLines == 1 ? 'line' : 'lines') + '...';
-      a.addEventListener('click',
-          this._handleContextControlClick.bind(this, control));
-      contentEl.appendChild(a);
-
-      this.$.numbers.insertBefore(lineEl, beforeLineEl);
-      this.$.content.insertBefore(contentEl, beforeContentEl);
-    },
-
-    _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
-      var lineFillerEl = this._createElement('div', 'filler');
-      lineFillerEl.setAttribute('data-index', index);
-      var fillerEl = this._createElement('div', 'filler');
-      fillerEl.setAttribute('data-index', index);
-      var numLines = filler.numLines || 1;
-
-      lineFillerEl.textContent = '\n'.repeat(numLines);
-      for (var i = 0; i < numLines; i++) {
-        var newlineEl = this._createElement('span', 'br');
-        fillerEl.appendChild(newlineEl);
-      }
-
-      this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
-      this.$.content.insertBefore(fillerEl, beforeContentEl);
-    },
-
-    _renderCode: function(code, index, beforeLineEl, beforeContentEl) {
-      var lineNumEl = this._createElement('div', 'lineNum');
-      lineNumEl.setAttribute('data-line-num', code.lineNum);
-      lineNumEl.setAttribute('data-index', index);
-      var numLines = code.numLines || 1;
-      lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
-
-      var contentEl = this._createElement('div', 'code');
-      contentEl.setAttribute('data-line-num', code.lineNum);
-      contentEl.setAttribute('data-index', index);
-
-      if (code.highlight) {
-        contentEl.classList.add(code.intraline.length > 0 ?
-            'lightHighlight' : 'darkHighlight');
-      }
-
-      var html = util.escapeHTML(code.content);
-      if (code.highlight && code.intraline.length > 0) {
-        html = this._addIntralineHighlights(code.content, html,
-            code.intraline);
-      }
-      if (numLines > 1) {
-        html = this._addNewLines(code.content, html, numLines);
-      }
-      html = this._addTabWrappers(code.content, html);
-
-      // If the html is equivalent to the text then it didn't get highlighted
-      // or escaped. Use textContent which is faster than innerHTML.
-      if (code.content == html) {
-        contentEl.textContent = code.content;
-      } else {
-        contentEl.innerHTML = html;
-      }
-
-      this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
-      this.$.content.insertBefore(contentEl, beforeContentEl);
-    },
-
-    // Advance `index` by the appropriate number of characters that would
-    // represent one source code character and return that index. For
-    // example, for source code '<span>' the escaped html string is
-    // '&lt;span&gt;'. Advancing from index 0 on the prior html string would
-    // return 4, since &lt; maps to one source code character ('<').
-    _advanceChar: function(html, index) {
-      // Any tags don't count as characters
-      while (index < html.length &&
-             html.charCodeAt(index) == CharCode.LESS_THAN) {
-        while (index < html.length &&
-               html.charCodeAt(index) != CharCode.GREATER_THAN) {
-          index++;
-        }
-        index++;  // skip the ">" itself
-      }
-      // An HTML entity (e.g., &lt;) counts as one character.
-      if (index < html.length &&
-          html.charCodeAt(index) == CharCode.AMPERSAND) {
-        while (index < html.length &&
-               html.charCodeAt(index) != CharCode.SEMICOLON) {
-          index++;
-        }
-      }
-      return index + 1;
-    },
-
-    _addIntralineHighlights: function(content, html, highlights) {
-      var startTag = this._highlightStartTag;
-      var endTag = this._highlightEndTag;
-
-      for (var i = 0; i < highlights.length; i++) {
-        var hl = highlights[i];
-
-        var htmlStartIndex = 0;
-        for (var j = 0; j < hl.startIndex; j++) {
-          htmlStartIndex = this._advanceChar(html, htmlStartIndex);
-        }
-
-        var htmlEndIndex = 0;
-        if (hl.endIndex != null) {
-          for (var j = 0; j < hl.endIndex; j++) {
-            htmlEndIndex = this._advanceChar(html, htmlEndIndex);
-          }
-        } else {
-          // If endIndex isn't present, continue to the end of the line.
-          htmlEndIndex = html.length;
-        }
-        // 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 (htmlStartIndex != htmlEndIndex) {
-          html = html.slice(0, htmlStartIndex) + startTag +
-                html.slice(htmlStartIndex, htmlEndIndex) + endTag +
-                html.slice(htmlEndIndex);
-        }
-      }
-      return html;
-    },
-
-    _addNewLines: function(content, html, numLines) {
-      var htmlIndex = 0;
-      var indices = [];
-      var numChars = 0;
-      for (var i = 0; i < content.length; i++) {
-        if (numChars > 0 && numChars % this.prefs.line_length == 0) {
-          indices.push(htmlIndex);
-        }
-        htmlIndex = this._advanceChar(html, htmlIndex);
-        if (content[i] == '\t') {
-          numChars += this.prefs.tab_size;
-        } else {
-          numChars++;
-        }
-      }
-      var result = html;
-      var linesLeft = numLines;
-      // Since the result string is being altered in place, start from the end
-      // of the string so that the insertion indices are not affected as the
-      // result string changes.
-      for (var i = indices.length - 1; i >= 0; i--) {
-        result = result.slice(0, indices[i]) + this._lineFeedHTML +
-            result.slice(indices[i]);
-        linesLeft--;
-      }
-      // numLines is the total number of lines this code block should take up.
-      // Fill in the remaining ones.
-      for (var i = 0; i < linesLeft; i++) {
-        result += this._lineFeedHTML;
-      }
-      return result;
-    },
-
-    _addTabWrappers: function(content, html) {
-      // TODO(andybons): CSS tab-size is not supported in IE.
-      // Force this to be a number to prevent arbitrary injection.
-      var tabSize = +this.prefs.tab_size;
-      var htmlStr = '<span class="style-scope gr-diff-side tab ' +
-          (this.prefs.show_tabs ? 'withIndicator" ' : '" ') +
-          'style="tab-size:' + tabSize + ';' +
-          '-moz-tab-size:' + tabSize + ';">\t</span>';
-      return html.replace(TAB_REGEX, htmlStr);
-    },
-
-    _createElement: function(tagName, className) {
-      var el = document.createElement(tagName);
-      // When Shady DOM is being used, these classes are added to account for
-      // Polymer's polyfill behavior. In order to guarantee sufficient
-      // specificity within the CSS rules, these are added to every element.
-      // Since the Polymer DOM utility functions (which would do this
-      // automatically) are not being used for performance reasons, this is
-      // done manually.
-      el.classList.add('style-scope', 'gr-diff-side');
-      if (!!className) {
-        el.classList.add(className);
-      }
-      return el;
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html b/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
deleted file mode 100644
index dbae6cb..0000000
--- a/polygerrit-ui/app/elements/diff/gr-diff-side/gr-diff-side_test.html
+++ /dev/null
@@ -1,300 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-diff-side</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../scripts/util.js"></script>
-
-<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="gr-diff-side.html">
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff-side></gr-diff-side>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-diff-side tests', function() {
-    var element;
-
-    function isVisibleInWindow(el) {
-      var rect = el.getBoundingClientRect();
-      return rect.top >= 0 && rect.left >= 0 &&
-          rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
-    }
-
-    setup(function() {
-      element = fixture('basic');
-    });
-
-    test('comments', function() {
-      assert.isFalse(element.$$('.container').classList.contains('canComment'));
-      element.canComment = true;
-      assert.isTrue(element.$$('.container').classList.contains('canComment'));
-      // TODO(andybons): Check for comment creation events firing/not firing
-      // when implemented.
-    });
-
-    test('scroll to line', function() {
-      var content = [];
-      for (var i = 0; i < 300; i++) {
-        content.push({
-          type: 'CODE',
-          content: 'All work and no play makes Jack a dull boy',
-          numLines: 1,
-          lineNum: i + 1,
-          highlight: false,
-          intraline: [],
-        });
-      }
-      element.content = content;
-
-      window.scrollTo(0, 0);
-      element.scrollToLine(-12849);
-      assert.equal(window.scrollY, 0);
-      element.scrollToLine('sup');
-      assert.equal(window.scrollY, 0);
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]');
-      assert.ok(lineEl);
-      element.scrollToLine(150);
-      assert.isAbove(window.scrollY, 0);
-      assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
-    });
-
-    test('intraline highlights', function() {
-      var content = '        <gr-linked-text content="' +
-          '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>';
-      var html = util.escapeHTML(content);
-      var highlights = [
-        {startIndex: 0, endIndex: 33},
-        {startIndex: 75},
-      ];
-      assert.equal(
-          content.slice(highlights[0].startIndex, highlights[0].endIndex),
-          '        <gr-linked-text content="');
-      assert.equal(content.slice(highlights[1].startIndex),
-          '"></gr-linked-text>');
-      var result = element._addIntralineHighlights(content, html, highlights);
-      var expected = element._highlightStartTag +
-          '        &lt;gr-linked-text content=&quot;' +
-          element._highlightEndTag +
-          '[[_computeCurrentRevisionMessage(change)]]' +
-          element._highlightStartTag +
-          '&quot;&gt;&lt;&#x2F;gr-linked-text&gt;' +
-          element._highlightEndTag;
-      assert.equal(result, expected);
-    });
-
-    test('newlines', function() {
-      element.prefs = {
-        line_length: 80,
-        tab_size: 4,
-      };
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50),
-        numLines: 1,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      var contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      assert.equal(contentEl.innerHTML, 'A'.repeat(50));
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(100),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      assert.equal(contentEl.innerHTML,
-          'A'.repeat(80) + element._lineFeedHTML +
-          'A'.repeat(20) + element._lineFeedHTML);
-    });
-
-    test('tabs', function(done) {
-      element.prefs = {
-        line_length: 80,
-        tab_size: 4,
-        show_tabs: true,
-      };
-
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-
-      var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
-      assert.ok(lineEl);
-      var contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      var spanEl = contentEl.childNodes[1];
-      assert.equal(spanEl.tagName, 'SPAN');
-      assert.isTrue(spanEl.classList.contains(
-          'style-scope', 'gr-diff-side', 'tab', 'withIndicator'));
-
-      element.prefs.show_tabs = false;
-      element.content = [{
-        type: 'CODE',
-        content: 'A'.repeat(50) + '\t' + 'A'.repeat(50),
-        numLines: 2,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-      contentEl = element.$$('.content .code[data-line-num="1"]');
-      assert.ok(contentEl);
-      spanEl = contentEl.childNodes[1];
-      assert.equal(spanEl.tagName, 'SPAN');
-      assert.isTrue(spanEl.classList.contains(
-          'style-scope', 'gr-diff-side', 'tab'));
-
-      var alertStub = sinon.stub(window, 'alert');
-      element.prefs.tab_size =
-          '"><img src="/" onerror="alert(1);"><span class="';
-      element.content = [{
-        type: 'CODE',
-        content: '\t',
-        numLines: 1,
-        lineNum: 1,
-        highlight: false,
-        intraline: [],
-      }];
-      flush(function() {
-        assert.isFalse(alertStub.called);
-        alertStub.restore();
-        done();
-      });
-    });
-
-    test('diff context', function() {
-      var content = [
-        {type: 'CODE', hidden: true, content: '<!DOCTYPE html>'},
-        {type: 'CODE', hidden: true, content: '<meta charset="utf-8">'},
-        {type: 'CODE', hidden: true, content: '<title>My great page</title>'},
-        {type: 'CODE', hidden: true, content: '<style>'},
-        {type: 'CODE', hidden: true, content: '  *,'},
-        {type: 'CODE', hidden: true, content: '  *:before,'},
-        {type: 'CODE', hidden: true, content: '  *:after {'},
-        {type: 'CODE', hidden: true, content: '    box-sizing: border-box;'},
-        {type: 'CONTEXT_CONTROL', numLines: 8, start: 0, end: 8},
-        {type: 'CODE', hidden: false, content: '  }'},
-      ];
-      element.content = content;
-
-      // Only the context elements and the following code line elements should
-      // be present in the DOM.
-      var contextEls =
-          Polymer.dom(element.root).querySelectorAll('.contextControl');
-      assert.equal(contextEls.length, 2);
-      var codeLineEls =
-          Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
-      assert.equal(codeLineEls.length, 2);
-
-      for (var i = 0; i <= 8; i++) {
-        element.content[i].hidden = false;
-      }
-      element.renderLineIndexRange(0, 8);
-      element.hideElementsWithIndex(8);
-
-      contextEls =
-          Polymer.dom(element.root).querySelectorAll('.contextControl');
-      for (var i = 0; i < contextEls.length; i++) {
-        assert.isTrue(contextEls[i].hasAttribute('hidden'));
-      }
-
-      codeLineEls =
-          Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
-
-      // Nine lines should now be present in the DOM.
-      assert.equal(codeLineEls.length, 9 * 2);
-    });
-
-    test('tap line to add a draft', function() {
-      var numAddDraftEvents = 0;
-      sinon.stub(element, 'fire', function(eventName) {
-        if (eventName == 'add-draft') {
-          numAddDraftEvents++;
-        }
-      });
-      element.content = [{type: 'CODE', content: '<!DOCTYPE html>'}];
-      element.canComment = false;
-      flushAsynchronousOperations();
-
-      var lineEl = element.$$('.lineNum');
-      assert.ok(lineEl);
-      MockInteractions.tap(lineEl);
-      assert.equal(numAddDraftEvents, 0);
-
-      element.canComment = true;
-      MockInteractions.tap(lineEl);
-      assert.equal(numAddDraftEvents, 1);
-    });
-
-    test('jump to diff chunk/thread', function() {
-      element.content = [
-        {type: 'CODE', content: '', intraline: [], lineNum: 1, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 2, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 3 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 4 },
-        {type: 'COMMENT_THREAD', comments: [ { line: 4 }]},
-        {type: 'CODE', content: '', intraline: [], lineNum: 5 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 6, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 7, highlight: true},
-        {type: 'CODE', content: '', intraline: [], lineNum: 8 },
-        {type: 'COMMENT_THREAD', comments: [ { line: 8 }]},
-        {type: 'CODE', content: '', intraline: [], lineNum: 9 },
-        {type: 'CODE', content: '', intraline: [], lineNum: 10,
-            highlight: true},
-      ];
-
-      var scrollToLineStub = sinon.stub(element, 'scrollToLine');
-      element.scrollToNextDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
-      element.scrollToPreviousDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(1));
-      element.scrollToNextCommentThread();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(4));
-      element.scrollToNextCommentThread();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(8));
-      element.scrollToPreviousDiffChunk();
-      assert.isTrue(scrollToLineStub.lastCall.calledWithExactly(6));
-
-      scrollToLineStub.restore();
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 0dc18ae..e7ec202 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -162,7 +162,6 @@
     </h3>
     <gr-diff id="diff"
         change-num="[[_changeNum]]"
-        prefs="{{prefs}}"
         patch-range="[[_patchRange]]"
         path="[[_path]]"
         project-config="[[_projectConfig]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 557c213..190a286 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -26,10 +26,6 @@
      */
 
     properties: {
-      prefs: {
-        type: Object,
-        notify: true,
-      },
       /**
        * URL params passed from the router.
        */
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-side-by-side.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder-unified.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
similarity index 98%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
index e49a0a3..338845e 100644
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -32,7 +32,7 @@
   GrDiffBuilder.TAB_REGEX = /\t/g;
 
   GrDiffBuilder.LINE_FEED_HTML =
-      '<span class="style-scope gr-new-diff br"></span>';
+      '<span class="style-scope gr-diff br"></span>';
 
   GrDiffBuilder.GroupType = {
     ADDED: 'b',
@@ -509,7 +509,7 @@
 
   GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
       highlights) {
-    var START_TAG = '<hl class="style-scope gr-new-diff">';
+    var START_TAG = '<hl class="style-scope gr-diff">';
     var END_TAG = '</hl>';
 
     for (var i = 0; i < highlights.length; i++) {
@@ -549,7 +549,7 @@
       throw Error('Invalid tab size from preferences.');
     }
 
-    var str = '<span class="style-scope gr-new-diff tab ';
+    var str = '<span class="style-scope gr-diff tab ';
     if (showTabs) {
       str += 'withIndicator';
     }
@@ -569,7 +569,7 @@
     // Since the Polymer DOM utility functions (which would do this
     // automatically) are not being used for performance reasons, this is
     // done manually.
-    el.classList.add('style-scope', 'gr-new-diff');
+    el.classList.add('style-scope', 'gr-diff');
     if (!!className) {
       el.classList.add(className);
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-builder_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-group_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-new-diff/gr-diff-line.js
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
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 a6d92a2..e0306ad 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -20,13 +20,19 @@
 <link rel="import" href="../../shared/gr-request/gr-request.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
 <link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../gr-diff-side/gr-diff-side.html">
 <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
 
 <dom-module id="gr-diff">
   <template>
     <style>
+      :host {
+        --light-remove-highlight-color: #fee;
+        --dark-remove-highlight-color: #ffd4d4;
+        --light-add-highlight-color: #efe;
+        --dark-add-highlight-color: #d4ffd4;
+      }
       .loading {
         padding: 0 var(--default-horizontal-margin) 1em;
         color: #666;
@@ -45,16 +51,99 @@
         display: flex;
         font: 12px var(--monospace-font-family);
         overflow-x: auto;
+        will-change: transform;
       }
-      gr-diff-side:first-of-type {
-        --light-highlight-color: #fee;
-        --dark-highlight-color: #ffd4d4;
-      }
-      gr-diff-side:last-of-type {
-        --light-highlight-color: #efe;
-        --dark-highlight-color: #d4ffd4;
+      table {
+        border-collapse: collapse;
         border-right: 1px solid #ddd;
       }
+      .section {
+        background-color: #eee;
+      }
+      .blank,
+      .content {
+        background-color: #fff;
+      }
+      .lineNum,
+      .content {
+        vertical-align: top;
+        white-space: pre;
+      }
+      .contextLineNum:before,
+      .lineNum:before {
+        display: inline-block;
+        color: #666;
+        content: attr(data-value);
+        padding: 0 .75em;
+        text-align: right;
+        width: 100%;
+      }
+      .canComment .lineNum[data-value] {
+        cursor: pointer;
+      }
+      .canComment .lineNum[data-value]:before {
+        text-decoration: underline;
+      }
+      .canComment .lineNum[data-value]:hover:before {
+        background-color: #ccc;
+      }
+      .canComment .lineNum[data-value="FILE"]:before {
+        content: 'File';
+      }
+      .content {
+        overflow: hidden;
+        min-width: var(--content-width, 80ch);
+      }
+      .content.left {
+        -webkit-user-select: var(--left-user-select, text);
+        -moz-user-select: var(--left-user-select, text);
+        -ms-user-select: var(--left-user-select, text);
+        user-select: var(--left-user-select, text);
+      }
+      .content.right {
+        -webkit-user-select: var(--right-user-select, text);
+        -moz-user-select: var(--right-user-select, text);
+        -ms-user-select: var(--right-user-select, text);
+        user-select: var(--right-user-select, text);
+      }
+      .content.add hl,
+      .content.add.darkHighlight {
+        background-color: var(--dark-add-highlight-color);
+      }
+      .content.add.lightHighlight {
+        background-color: var(--light-add-highlight-color);
+      }
+      .content.remove hl,
+      .content.remove.darkHighlight {
+        background-color: var(--dark-remove-highlight-color);
+      }
+      .content.remove.lightHighlight {
+        background-color: var(--light-remove-highlight-color);
+      }
+      .contextControl {
+        color: #849;
+        background-color: #fef;
+      }
+      .contextControl gr-button {
+        display: block;
+        font-family: var(--monospace-font-family);
+        text-decoration: none;
+      }
+      .contextControl td:not(.lineNum) {
+        text-align: center;
+      }
+      .br:after {
+        /* Line feed */
+        content: '\A';
+      }
+      .tab {
+        display: inline-block;
+      }
+      .tab.withIndicator:before {
+        color: #C62828;
+        /* >> character */
+        content: '\00BB';
+      }
     </style>
     <div class="loading" hidden$="[[!_loading]]">Loading...</div>
     <div hidden$="[[_loading]]" hidden>
@@ -67,44 +156,29 @@
         <gr-button link
            class="prefsButton"
            on-tap="_handlePrefsTap"
-           hidden$="[[!prefs]]"
+           hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
            hidden>Diff View Preferences</gr-button>
       </div>
       <gr-overlay id="prefsOverlay" with-backdrop>
         <gr-diff-preferences
-            prefs="{{prefs}}"
+            prefs="{{_prefs}}"
             on-save="_handlePrefsSave"
             on-cancel="_handlePrefsCancel"></gr-diff-preferences>
       </gr-overlay>
 
-      <div class="diffContainer">
-        <gr-diff-side id="leftDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.basePatchNum]]"
-            path="[[path]]"
-            content="{{_diff.leftSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
-        <gr-diff-side id="rightDiff"
-            change-num="[[changeNum]]"
-            patch-num="[[patchRange.patchNum]]"
-            path="[[path]]"
-            content="{{_diff.rightSide}}"
-            prefs="[[prefs]]"
-            can-comment="[[_loggedIn]]"
-            project-config="[[projectConfig]]"
-            on-expand-context="_handleExpandContext"
-            on-thread-height-change="_handleThreadHeightChange"
-            on-add-draft="_handleAddDraft"
-            on-remove-thread="_handleRemoveThread"></gr-diff-side>
+      <div class$="[[_computeContainerClass(_loggedIn, _viewMode)]]"
+          on-tap="_handleTap"
+          on-mousedown="_handleMouseDown"
+          on-copy="_handleCopy">
+        <table id="diffTable"></table>
       </div>
     </div>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
+  <script src="gr-diff-line.js"></script>
+  <script src="gr-diff-group.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.js"></script>
 </dom-module>
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 cac1f3e..12ceacb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,6 +14,16 @@
 (function() {
   'use strict';
 
+  var DiffViewMode = {
+    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+    UNIFIED: 'UNIFIED_DIFF',
+  };
+
+  var DiffSide = {
+    LEFT: 'left',
+    RIGHT: 'right',
+  };
+
   Polymer({
     is: 'gr-diff',
 
@@ -26,148 +36,379 @@
     properties: {
       availablePatches: Array,
       changeNum: String,
-      /*
-       * A single object to encompass basePatchNum and patchNum is used
-       * so that both can be set at once without incremental observers
-       * firing after each property changes.
-       */
       patchRange: Object,
       path: String,
-      prefs: {
-        type: Object,
-        notify: true,
-      },
-      projectConfig: Object,
 
-      _prefsReady: {
+      projectConfig: {
         type: Object,
-        readOnly: true,
-        value: function() {
-          return new Promise(function(resolve) {
-            this._resolvePrefsReady = resolve;
-          }.bind(this));
-        },
+        observer: '_projectConfigChanged',
       },
-      _baseComments: Array,
-      _comments: Array,
-      _drafts: Array,
-      _baseDrafts: Array,
-      /**
-       * Base (left side) comments and drafts grouped by line number.
-       * Only used for initial rendering.
-       */
-      _groupedBaseComments: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      /**
-       * Comments and drafts (right side) grouped by line number.
-       * Only used for initial rendering.
-       */
-      _groupedComments: {
-        type: Object,
-        value: function() { return {}; },
-      },
-      _diffResponse: Object,
-      _diff: {
-        type: Object,
-        value: function() { return {}; },
-      },
+
       _loggedIn: {
         type: Boolean,
         value: false,
       },
-      _initialRenderComplete: {
-        type: Boolean,
-        value: false,
-      },
       _loading: {
         type: Boolean,
         value: true,
       },
-      _savedPrefs: Object,
-
-      _diffPreferencesPromise: Object,  // Used for testing.
+      _viewMode: {
+        type: String,
+        value: DiffViewMode.SIDE_BY_SIDE,
+      },
+      _diff: Object,
+      _diffBuilder: Object,
+      _prefs: Object,
+      _selectionSide: {
+        type: String,
+        observer: '_selectionSideChanged',
+      },
+      _comments: Object,
+      _focusedSection: {
+        type: Number,
+        value: -1,
+      },
+      _focusedThread: {
+        type: Number,
+        value: -1,
+      },
     },
 
     observers: [
-      '_prefsChanged(prefs.*)',
+      '_prefsChanged(_prefs.*)',
     ],
 
-    ready: function() {
-      app.accountReady.then(function() {
-        this._loggedIn = app.loggedIn;
+    attached: function() {
+      this._getLoggedIn().then(function(loggedIn) {
+        this._loggedIn = loggedIn;
       }.bind(this));
-    },
 
-    scrollToLine: function(lineNum) {
-      // TODO(andybons): Should this always be the right side?
-      this.$.rightDiff.scrollToLine(lineNum);
-    },
-
-    scrollToNextDiffChunk: function() {
-      this.$.rightDiff.scrollToNextDiffChunk();
-    },
-
-    scrollToPreviousDiffChunk: function() {
-      this.$.rightDiff.scrollToPreviousDiffChunk();
-    },
-
-    scrollToNextCommentThread: function() {
-      this.$.rightDiff.scrollToNextCommentThread();
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this.$.rightDiff.scrollToPreviousCommentThread();
+      this.addEventListener('thread-discard',
+          this._handleThreadDiscard.bind(this));
+      this.addEventListener('comment-discard',
+          this._handleCommentDiscard.bind(this));
     },
 
     reload: function() {
+      this._clearDiffContent();
       this._loading = true;
-      // If a diff takes a considerable amount of time to render, the previous
-      // diff can end up showing up while the DOM is constructed. Clear the
-      // content on a reload to prevent this.
-      this._diff = {
-        leftSide: [],
-        rightSide: [],
-      };
 
-      var diffLoaded = this._getDiff().then(function(diff) {
-        this._diffResponse = diff;
+      var promises = [];
+
+      promises.push(this._getDiff().then(function(diff) {
+        this._diff = diff;
+        this._loading = false;
+      }.bind(this)));
+
+      promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
+        this._comments = comments;
+      }.bind(this)));
+
+      promises.push(this._getDiffPreferences().then(function(prefs) {
+        this._prefs = prefs;
+      }.bind(this)));
+
+      return Promise.all(promises).then(function() {
+        this._render();
       }.bind(this));
+    },
 
-      var promises = [
-        this._prefsReady,
-        diffLoaded,
-      ];
+    showDiffPreferences: function() {
+      this.$.prefsOverlay.open();
+    },
 
-      return app.accountReady.then(function() {
-        promises.push(this._getDiffComments().then(function(res) {
-          this._baseComments = res.baseComments;
-          this._comments = res.comments;
-        }.bind(this)));
+    scrollToLine: function(lineNum) {
+      if (isNaN(lineNum) || lineNum < 1) { return; }
 
-        if (!app.loggedIn) {
-          this._baseDrafts = [];
-          this._drafts = [];
-        } else {
-          promises.push(this._getDiffDrafts().then(function(res) {
-            this._baseDrafts = res.baseComments;
-            this._drafts = res.comments;
-          }.bind(this)));
-        }
+      var lineEls = Polymer.dom(this.root).querySelectorAll(
+          '.lineNum[data-value="' + lineNum + '"]');
 
-        return Promise.all(promises).then(function() {
-          this._render();
-          this._loading = false;
-        }.bind(this)).catch(function(err) {
-          this._loading = false;
-          alert('Oops. Something went wrong. Check the console and bug the ' +
+      // Always choose the right side.
+      var el = lineEls.length === 2 ? lineEls[1] : lineEls[0];
+      this._scrollToElement(el);
+    },
+
+    scrollToNextDiffChunk: function() {
+      this._focusedSection = this._advanceElementWithinNodeList(
+          this._getDeltaSections(), this._focusedSection, 1);
+    },
+
+    scrollToPreviousDiffChunk: function() {
+      this._focusedSection = this._advanceElementWithinNodeList(
+          this._getDeltaSections(), this._focusedSection, -1);
+    },
+
+    scrollToNextCommentThread: function() {
+      this._focusedThread = this._advanceElementWithinNodeList(
+          this._getCommentThreads(), this._focusedThread, 1);
+    },
+
+    scrollToPreviousCommentThread: function() {
+      this._focusedThread = this._advanceElementWithinNodeList(
+          this._getCommentThreads(), this._focusedThread, -1);
+    },
+
+    _advanceElementWithinNodeList: function(els, curIndex, direction) {
+      var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction));
+      if (curIndex !== idx) {
+        this._scrollToElement(els[idx]);
+        return idx;
+      }
+      return curIndex;
+    },
+
+    _getCommentThreads: function() {
+      return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
+    },
+
+    _getDeltaSections: function() {
+      return Polymer.dom(this.root).querySelectorAll('.section.delta');
+    },
+
+    _scrollToElement: function(el) {
+      if (!el) { return; }
+
+      // Calculate where the element is relative to the window.
+      var top = el.offsetTop;
+      for (var offsetParent = el.offsetParent;
+           offsetParent;
+           offsetParent = offsetParent.offsetParent) {
+        top += offsetParent.offsetTop;
+      }
+
+      // Scroll the element to the middle of the window. Dividing by a third
+      // instead of half the inner height feels a bit better otherwise the
+      // element appears to be below the center of the window even when it
+      // isn't.
+      window.scrollTo(0, top - (window.innerHeight / 3) +
+          (el.offsetHeight / 2));
+    },
+
+    _computeContainerClass: function(loggedIn, viewMode) {
+      var classes = ['diffContainer'];
+      switch (viewMode) {
+        case DiffViewMode.UNIFIED:
+          classes.push('unified');
+          break;
+        case DiffViewMode.SIDE_BY_SIDE:
+          classes.push('sideBySide');
+          break
+        default:
+          throw Error('Invalid view mode: ', viewMode);
+      }
+      if (loggedIn) {
+        classes.push('canComment');
+      }
+      return classes.join(' ');
+    },
+
+    _computePrefsButtonHidden: function(prefs, loggedIn) {
+      return !loggedIn || !prefs;
+    },
+
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+      this.$.prefsOverlay.open();
+    },
+
+    _handlePrefsSave: function(e) {
+      e.stopPropagation();
+      var el = Polymer.dom(e).rootTarget;
+      el.disabled = true;
+      this._saveDiffPreferences().then(function() {
+        this.$.prefsOverlay.close();
+        el.disabled = false;
+      }.bind(this)).catch(function(err) {
+        el.disabled = false;
+        alert('Oops. Something went wrong. Check the console and bug the ' +
               'PolyGerrit team for assistance.');
-          throw err;
-        }.bind(this));
+        throw err;
+      });
+    },
+
+    _saveDiffPreferences: function() {
+      return this.$.restAPI.saveDiffPreferences(this._prefs);
+    },
+
+    _handlePrefsCancel: function(e) {
+      e.stopPropagation();
+      this.$.prefsOverlay.close();
+    },
+
+    _handleTap: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+
+      if (el.classList.contains('showContext')) {
+        this._showContext(e.detail.group, e.detail.section);
+      } else if (el.classList.contains('lineNum')) {
+        this._handleLineTap(el);
+      }
+    },
+
+    _handleLineTap: function(el) {
+      this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) { return; }
+
+        var value = el.getAttribute('data-value');
+        if (value === GrDiffLine.FILE) {
+          this._addDraft(el);
+          return;
+        }
+        var lineNum = parseInt(value, 10);
+        if (isNaN(lineNum)) {
+          throw Error('Invalid line number: ' + value);
+        }
+        this._addDraft(el, lineNum);
       }.bind(this));
     },
 
+    _addDraft: function(lineEl, opt_lineNum) {
+      var threadEl;
+
+      // Does a thread already exist at this line?
+      var contentEl = lineEl.nextSibling;
+      while (contentEl && !contentEl.classList.contains('content')) {
+        contentEl = contentEl.nextSibling;
+      }
+      if (contentEl.childNodes.length > 0 &&
+          contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') {
+        threadEl = contentEl.lastChild;
+      } else {
+        var patchNum = this.patchRange.patchNum;
+        var side = 'REVISION';
+        if (contentEl.classList.contains(DiffSide.LEFT) ||
+            contentEl.classList.contains('remove')) {
+          if (this.patchRange.basePatchNum === 'PARENT') {
+            side = 'PARENT';
+          } else {
+            patchNum = this.patchRange.basePatchNum;
+          }
+        }
+        threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
+            this.path, side, this.projectConfig);
+        contentEl.appendChild(threadEl);
+      }
+      threadEl.addDraft(opt_lineNum);
+    },
+
+    _handleThreadDiscard: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+      el.parentNode.removeChild(el);
+    },
+
+    _handleCommentDiscard: function(e) {
+      var comment = Polymer.dom(e).rootTarget.comment;
+      this._removeComment(comment);
+    },
+
+    _removeComment: function(comment) {
+      if (!comment.id) { return; }
+      this._removeCommentFromSide(comment, DiffSide.LEFT) ||
+          this._removeCommentFromSide(comment, DiffSide.RIGHT);
+    },
+
+    _removeCommentFromSide: function(comment, side) {
+      var idx = -1;
+      for (var i = 0; i < this._comments[side].length; i++) {
+        if (this._comments[side][i].id === comment.id) {
+          idx = i;
+          break;
+        }
+      }
+      if (idx !== -1) {
+        this.splice('_comments.' + side, idx, 1);
+        return true;
+      }
+      return false;
+    },
+
+    _handleMouseDown: function(e) {
+      var el = Polymer.dom(e).rootTarget;
+      var side;
+      for (var node = el; node != null; node = node.parentNode) {
+        if (!node.classList) { continue; }
+
+        if (node.classList.contains(DiffSide.LEFT)) {
+          side = DiffSide.LEFT;
+          break;
+        } else if (node.classList.contains(DiffSide.RIGHT)) {
+          side = DiffSide.RIGHT;
+          break;
+        }
+      }
+      this._selectionSide = side;
+    },
+
+    _selectionSideChanged: function(side) {
+      if (side) {
+        var oppositeSide = side === DiffSide.RIGHT ?
+            DiffSide.LEFT : DiffSide.RIGHT;
+        this.customStyle['--' + side + '-user-select'] = 'text';
+        this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
+      } else {
+        this.customStyle['--left-user-select'] = 'text';
+        this.customStyle['--right-user-select'] = 'text';
+      }
+      this.updateStyles();
+    },
+
+    _handleCopy: function(e) {
+      var text = this._getSelectedText(this._selectionSide);
+      e.clipboardData.setData('Text', text);
+      e.preventDefault();
+    },
+
+    _getSelectedText: function(opt_side) {
+      var sel = window.getSelection();
+      var range = sel.getRangeAt(0);
+      var doc = range.cloneContents();
+      var selector = '.content';
+      if (opt_side) {
+        selector += '.' + opt_side;
+      }
+      var contentEls = Polymer.dom(doc).querySelectorAll(selector);
+
+      if (contentEls.length === 0) {
+        return doc.textContent;
+      }
+
+      var text = '';
+      for (var i = 0; i < contentEls.length; i++) {
+        text += contentEls[i].textContent + '\n';
+      }
+      return text;
+    },
+
+    _showContext: function(group, sectionEl) {
+      this._builder.emitGroup(group, sectionEl);
+      sectionEl.parentNode.removeChild(sectionEl);
+    },
+
+    _prefsChanged: function(prefsChangeRecord) {
+      var prefs = prefsChangeRecord.base;
+      this.customStyle['--content-width'] = prefs.line_length + 'ch';
+      this.updateStyles();
+
+      if (this._diff && this._comments) {
+        this._render();
+      }
+    },
+
+    _render: function() {
+      this._clearDiffContent();
+      this._builder = this._getDiffBuilder(this._diff, this._comments,
+          this._prefs);
+      this._builder.emitDiff(this._diff.content);
+
+      this.async(function() {
+        this.fire('render', null, {bubbles: false});
+      }.bind(this), 1);
+    },
+
+    _clearDiffContent: function() {
+      this.$.diffTable.innerHTML = null;
+    },
+
     _getDiff: function() {
       return this.$.restAPI.getDiff(
           this.changeNum,
@@ -185,528 +426,95 @@
     },
 
     _getDiffDrafts: function() {
-      return this.$.restAPI.getDiffDrafts(
-          this.changeNum,
-          this.patchRange.basePatchNum,
-          this.patchRange.patchNum,
-          this.path);
-    },
-
-    showDiffPreferences: function() {
-      this.$.prefsOverlay.open();
-    },
-
-    _prefsChanged: function(changeRecord) {
-      if (this._initialRenderComplete) {
-        this._render();
-      }
-      this._resolvePrefsReady(changeRecord.base);
-    },
-
-    _render: function() {
-      this._groupCommentsAndDrafts();
-      this._processContent();
-
-      // Allow for the initial rendering to complete before firing the event.
-      this.async(function() {
-        this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
-
-      this._initialRenderComplete = true;
-    },
-
-    _handlePrefsTap: function(e) {
-      e.preventDefault();
-
-      // TODO(andybons): This is not supported in IE. Implement a polyfill.
-      // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
-      // an object as a value, it must be marked enumerable.
-      this._savedPrefs = Object.assign({}, this.prefs);
-      this.$.prefsOverlay.open();
-    },
-
-    _handlePrefsSave: function(e) {
-      e.stopPropagation();
-      var el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      app.accountReady.then(function() {
-        if (!this._loggedIn) {
-          el.disabled = false;
-          this.$.prefsOverlay.close();
-          return;
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          return Promise.resolve({baseComments: [], comments: []});
         }
-        this._saveDiffPreferences().then(function() {
-          this.$.prefsOverlay.close();
-          el.disabled = false;
-        }.bind(this)).catch(function(err) {
-          el.disabled = false;
-          alert('Oops. Something went wrong. Check the console and bug the ' +
-                'PolyGerrit team for assistance.');
-          throw err;
-        });
+        return this.$.restAPI.getDiffDrafts(
+            this.changeNum,
+            this.patchRange.basePatchNum,
+            this.patchRange.patchNum,
+            this.path);
       }.bind(this));
     },
 
-    _saveDiffPreferences: function() {
-      var xhr = document.createElement('gr-request');
-      this._diffPreferencesPromise = xhr.send({
-        method: 'PUT',
-        url: '/accounts/self/preferences.diff',
-        body: this.prefs,
-      });
-      return this._diffPreferencesPromise;
+    _getDiffCommentsAndDrafts: function() {
+      var promises = [];
+      promises.push(this._getDiffComments());
+      promises.push(this._getDiffDrafts());
+      return Promise.all(promises).then(function(results) {
+        return Promise.resolve({
+          comments: results[0],
+          drafts: results[1],
+        });
+      }).then(this._normalizeDiffCommentsAndDrafts.bind(this));
     },
 
-    _handlePrefsCancel: function(e) {
-      e.stopPropagation();
-      this.prefs = this._savedPrefs;
-      this.$.prefsOverlay.close();
-    },
-
-    _handleExpandContext: function(e) {
-      var ctx = e.detail.context;
-      var contextControlIndex = -1;
-      for (var i = ctx.start; i <= ctx.end; i++) {
-        this._diff.leftSide[i].hidden = false;
-        this._diff.rightSide[i].hidden = false;
-        if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
-            this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
-          contextControlIndex = i;
+    _getDiffPreferences: function() {
+      return this._getLoggedIn().then(function(loggedIn) {
+        if (!loggedIn) {
+          // These defaults should match the defaults in
+          // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+          // NOTE: There are some settings that don't apply to PolyGerrit
+          // (Render mode being at least one of them).
+          return Promise.resolve({
+            auto_hide_diff_table_header: true,
+            context: 10,
+            cursor_blink_rate: 0,
+            ignore_whitespace: 'IGNORE_NONE',
+            intraline_difference: true,
+            line_length: 100,
+            show_line_endings: true,
+            show_tabs: true,
+            show_whitespace_errors: true,
+            syntax_highlighting: true,
+            tab_size: 8,
+            theme: 'DEFAULT',
+          });
         }
-      }
-      this._diff.leftSide[contextControlIndex].hidden = true;
-      this._diff.rightSide[contextControlIndex].hidden = true;
-
-      this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
-      this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
-
-      this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
-      this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
+        return this.$.restAPI.getDiffPreferences();
+      }.bind(this));
     },
 
-    _handleThreadHeightChange: function(e) {
-      var index = e.detail.index;
-      var diffEl = Polymer.dom(e).rootTarget;
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-
-      var threadHeight = e.detail.height;
-      var otherSideHeight;
-      if (otherSide.content[index].type == 'COMMENT_THREAD') {
-        otherSideHeight = otherSide.getRowNaturalHeight(index);
-      } else {
-        otherSideHeight = otherSide.getRowHeight(index);
+    _normalizeDiffCommentsAndDrafts: function(results) {
+      function markAsDraft(d) {
+        d.__draft = true;
+        return d;
       }
-      var maxHeight = Math.max(threadHeight, otherSideHeight);
-      this.$.leftDiff.setRowHeight(index, maxHeight);
-      this.$.rightDiff.setRowHeight(index, maxHeight);
-    },
-
-    _handleAddDraft: function(e) {
-      var insertIndex = e.detail.index + 1;
-      var diffEl = Polymer.dom(e).rootTarget;
-      var content = diffEl.content;
-      if (content[insertIndex] &&
-          content[insertIndex].type == 'COMMENT_THREAD') {
-        // A thread is already here. Do nothing.
-        return;
-      }
-      var comment = {
-        type: 'COMMENT_THREAD',
-        comments: [{
-          __draft: true,
-          __draftID: Math.random().toString(36),
-          line: e.detail.line,
+      var baseDrafts = results.drafts.baseComments.map(markAsDraft);
+      var drafts = results.drafts.comments.map(markAsDraft);
+      return Promise.resolve({
+        meta: {
           path: this.path,
-        }]
-      };
-      if (diffEl == this.$.leftDiff &&
-          this.patchRange.basePatchNum == 'PARENT') {
-        comment.comments[0].side = 'PARENT';
-        comment.patchNum = this.patchRange.patchNum;
-      }
-
-      if (content[insertIndex] &&
-          content[insertIndex].type == 'FILLER') {
-        content[insertIndex] = comment;
-        diffEl.rowUpdated(insertIndex);
-      } else {
-        content.splice(insertIndex, 0, comment);
-        diffEl.rowInserted(insertIndex);
-      }
-
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-      if (otherSide.content[insertIndex] == null ||
-          otherSide.content[insertIndex].type != 'COMMENT_THREAD') {
-        otherSide.content.splice(insertIndex, 0, {
-          type: 'FILLER',
-        });
-        otherSide.rowInserted(insertIndex);
-      }
-    },
-
-    _handleRemoveThread: function(e) {
-      var diffEl = Polymer.dom(e).rootTarget;
-      var otherSide = diffEl == this.$.leftDiff ?
-          this.$.rightDiff : this.$.leftDiff;
-      var index = e.detail.index;
-
-      if (otherSide.content[index].type == 'FILLER') {
-        otherSide.content.splice(index, 1);
-        otherSide.rowRemoved(index);
-        diffEl.content.splice(index, 1);
-        diffEl.rowRemoved(index);
-      } else if (otherSide.content[index].type == 'COMMENT_THREAD') {
-        diffEl.content[index] = {type: 'FILLER'};
-        diffEl.rowUpdated(index);
-        var height = otherSide.setRowNaturalHeight(index);
-        diffEl.setRowHeight(index, height);
-      } else {
-        throw Error('A thread cannot be opposite anything but filler or ' +
-            'another thread');
-      }
-    },
-
-    _processContent: function() {
-      var leftSide = [];
-      var rightSide = [];
-      var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
-      var ctx = {
-        hidingLines: false,
-        lastNumLinesHidden: 0,
-        left: {
-          lineNum: initialLineNum,
+          changeNum: this.changeNum,
+          patchRange: this.patchRange,
+          projectConfig: this.projectConfig,
         },
-        right: {
-          lineNum: initialLineNum,
-        }
-      };
-      var content = this._breakUpCommonChunksWithComments(ctx,
-          this._diffResponse.content);
-      var context = this.prefs.context;
-      if (context == -1) {
-        // Show the entire file.
-        context = Infinity;
-      }
-      for (var i = 0; i < content.length; i++) {
-        if (i == 0) {
-          ctx.skipRange = [0, context];
-        } else if (i == content.length - 1) {
-          ctx.skipRange = [context, 0];
-        } else {
-          ctx.skipRange = [context, context];
-        }
-        ctx.diffChunkIndex = i;
-        this._addDiffChunk(ctx, content[i], leftSide, rightSide);
-      }
-
-      this._diff = {
-        leftSide: leftSide,
-        rightSide: rightSide,
-      };
+        left: results.comments.baseComments.concat(baseDrafts),
+        right: results.comments.comments.concat(drafts),
+      });
     },
 
-    // In order to show comments out of the bounds of the selected context,
-    // treat them as diffs within the model so that the content (and context
-    // surrounding it) renders correctly.
-    _breakUpCommonChunksWithComments: function(ctx, content) {
-      var result = [];
-      var leftLineNum = ctx.left.lineNum;
-      var rightLineNum = ctx.right.lineNum;
-      for (var i = 0; i < content.length; i++) {
-        if (!content[i].ab) {
-          result.push(content[i]);
-          if (content[i].a) {
-            leftLineNum += content[i].a.length;
-          }
-          if (content[i].b) {
-            rightLineNum += content[i].b.length;
-          }
-          continue;
-        }
-        var chunk = content[i].ab;
-        var currentChunk = {ab: []};
-        for (var j = 0; j < chunk.length; j++) {
-          leftLineNum++;
-          rightLineNum++;
-          if (this._groupedBaseComments[leftLineNum] == null &&
-              this._groupedComments[rightLineNum] == null) {
-            currentChunk.ab.push(chunk[j]);
-          } else {
-            if (currentChunk.ab && currentChunk.ab.length > 0) {
-              result.push(currentChunk);
-              currentChunk = {ab: []};
-            }
-            // Append an annotation to indicate that this line should not be
-            // highlighted even though it's implied with both `a` and `b`
-            // defined. This is needed since there may be two lines that
-            // should be highlighted but are equal (blank lines, for example).
-            result.push({
-              __noHighlight: true,
-              a: [chunk[j]],
-              b: [chunk[j]],
-            });
-          }
-        }
-        if (currentChunk.ab != null && currentChunk.ab.length > 0) {
-          result.push(currentChunk);
-        }
-      }
-      return result;
+    _getLoggedIn: function() {
+      return this.$.restAPI.getLoggedIn();
     },
 
-    _groupCommentsAndDrafts: function() {
-      this._baseDrafts.forEach(function(d) { d.__draft = true; });
-      this._drafts.forEach(function(d) { d.__draft = true; });
-      var allLeft = this._baseComments.concat(this._baseDrafts);
-      var allRight = this._comments.concat(this._drafts);
-
-      var leftByLine = {};
-      var rightByLine = {};
-      var mapFunc = function(byLine) {
-        return function(c) {
-          // File comments/drafts are grouped with line 1 for now.
-          var line = c.line || 1;
-          if (byLine[line] == null) {
-            byLine[line] = [];
-          }
-          byLine[line].push(c);
-        };
-      };
-      allLeft.forEach(mapFunc(leftByLine));
-      allRight.forEach(mapFunc(rightByLine));
-
-      this._groupedBaseComments = leftByLine;
-      this._groupedComments = rightByLine;
+    _getDiffBuilder: function(diff, comments, prefs) {
+      if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
+        return new GrDiffBuilderSideBySide(diff, comments, prefs,
+            this.$.diffTable);
+      } else if (this._viewMode === DiffViewMode.UNIFIED) {
+        return new GrDiffBuilderUnified(diff, comments, prefs,
+            this.$.diffTable);
+      }
+      throw Error('Unsupported diff view mode: ' + this._viewMode);
     },
 
-    _addContextControl: function(ctx, leftSide, rightSide) {
-      var numLinesHidden = ctx.lastNumLinesHidden;
-      var leftStart = leftSide.length - numLinesHidden;
-      var leftEnd = leftSide.length;
-      var rightStart = rightSide.length - numLinesHidden;
-      var rightEnd = rightSide.length;
-      if (leftStart != rightStart || leftEnd != rightEnd) {
-        throw Error(
-            'Left and right ranges for context control should be equal:' +
-            'Left: [' + leftStart + ', ' + leftEnd + '] ' +
-            'Right: [' + rightStart + ', ' + rightEnd + ']');
+    _projectConfigChanged: function(projectConfig) {
+      var threadEls = this._getCommentThreads();
+      for (var i = 0; i < threadEls.length; i++) {
+        threadEls[i].projectConfig = projectConfig;
       }
-      var obj = {
-        type: 'CONTEXT_CONTROL',
-        numLines: numLinesHidden,
-        start: leftStart,
-        end: leftEnd,
-      };
-      // NOTE: Be careful, here. This object is meant to be immutable. If the
-      // object is altered within one side's array it will reflect the
-      // alterations in another.
-      leftSide.push(obj);
-      rightSide.push(obj);
-    },
-
-    _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-      for (var i = 0; i < chunk.ab.length; i++) {
-        var numLines = Math.ceil(
-            this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
-        var hidden = i >= ctx.skipRange[0] &&
-            i < chunk.ab.length - ctx.skipRange[1];
-        if (ctx.hidingLines && hidden == false) {
-          // No longer hiding lines. Add a context control.
-          this._addContextControl(ctx, leftSide, rightSide);
-          ctx.lastNumLinesHidden = 0;
-        }
-        ctx.hidingLines = hidden;
-        if (hidden) {
-          ctx.lastNumLinesHidden++;
-        }
-
-        // Blank lines within a diff content array indicate a newline.
-        leftSide.push({
-          type: 'CODE',
-          hidden: hidden,
-          content: chunk.ab[i] || '\n',
-          numLines: numLines,
-          lineNum: ++ctx.left.lineNum,
-        });
-        rightSide.push({
-          type: 'CODE',
-          hidden: hidden,
-          content: chunk.ab[i] || '\n',
-          numLines: numLines,
-          lineNum: ++ctx.right.lineNum,
-        });
-
-        this._addCommentsIfPresent(ctx, leftSide, rightSide);
-      }
-      if (ctx.lastNumLinesHidden > 0) {
-        this._addContextControl(ctx, leftSide, rightSide);
-      }
-    },
-
-    _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
-      if (chunk.ab) {
-        this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
-        return;
-      }
-
-      var leftHighlights = [];
-      if (chunk.edit_a) {
-        leftHighlights =
-            this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
-      }
-      var rightHighlights = [];
-      if (chunk.edit_b) {
-        rightHighlights =
-            this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
-      }
-
-      var aLen = (chunk.a && chunk.a.length) || 0;
-      var bLen = (chunk.b && chunk.b.length) || 0;
-      var maxLen = Math.max(aLen, bLen);
-      for (var i = 0; i < maxLen; i++) {
-        var hasLeftContent = chunk.a && i < chunk.a.length;
-        var hasRightContent = chunk.b && i < chunk.b.length;
-        var leftContent = hasLeftContent ? chunk.a[i] : '';
-        var rightContent = hasRightContent ? chunk.b[i] : '';
-        var highlight = !chunk.__noHighlight;
-        var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
-        if (hasLeftContent) {
-          leftSide.push({
-            type: 'CODE',
-            content: leftContent || '\n',
-            numLines: maxNumLines,
-            lineNum: ++ctx.left.lineNum,
-            highlight: highlight,
-            intraline: highlight && leftHighlights.filter(function(hl) {
-              return hl.contentIndex == i;
-            }),
-          });
-        } else {
-          leftSide.push({
-            type: 'FILLER',
-            numLines: maxNumLines,
-          });
-        }
-        if (hasRightContent) {
-          rightSide.push({
-            type: 'CODE',
-            content: rightContent || '\n',
-            numLines: maxNumLines,
-            lineNum: ++ctx.right.lineNum,
-            highlight: highlight,
-            intraline: highlight && rightHighlights.filter(function(hl) {
-              return hl.contentIndex == i;
-            }),
-          });
-        } else {
-          rightSide.push({
-            type: 'FILLER',
-            numLines: maxNumLines,
-          });
-        }
-        this._addCommentsIfPresent(ctx, leftSide, rightSide);
-      }
-    },
-
-    _addCommentsIfPresent: function(ctx, leftSide, rightSide) {
-      var leftComments = this._groupedBaseComments[ctx.left.lineNum];
-      var rightComments = this._groupedComments[ctx.right.lineNum];
-      if (leftComments) {
-        var thread = {
-          type: 'COMMENT_THREAD',
-          comments: leftComments,
-        };
-        if (this.patchRange.basePatchNum == 'PARENT') {
-          thread.patchNum = this.patchRange.patchNum;
-        }
-        leftSide.push(thread);
-      }
-      if (rightComments) {
-        rightSide.push({
-          type: 'COMMENT_THREAD',
-          comments: rightComments,
-        });
-      }
-      if (leftComments && !rightComments) {
-        rightSide.push({type: 'FILLER'});
-      } else if (!leftComments && rightComments) {
-        leftSide.push({type: 'FILLER'});
-      }
-      this._groupedBaseComments[ctx.left.lineNum] = null;
-      this._groupedComments[ctx.right.lineNum] = null;
-    },
-
-    // The `highlights` array consists of a list of <skip length, mark length>
-    // pairs, where the skip length is the number of characters between the
-    // end of the previous edit and the start of this edit, and the mark
-    // length is the number of edited characters following the skip. The start
-    // of the edits is from the beginning of the related diff content lines.
-    //
-    // Note that the implied newline character at the end of each line is
-    // included in the length calculation, and thus it is possible for the
-    // edits to span newlines.
-    //
-    // A line highlight object consists of three fields:
-    // - contentIndex: The index of the diffChunk `content` field (the line
-    //   being referred to).
-    // - startIndex: Where the highlight should begin.
-    // - endIndex: (optional) Where the highlight should end. If omitted, the
-    //   highlight is meant to be a continuation onto the next line.
-    _normalizeIntralineHighlights: function(content, highlights) {
-      var contentIndex = 0;
-      var idx = 0;
-      var normalized = [];
-      for (var i = 0; i < highlights.length; i++) {
-        var line = content[contentIndex] + '\n';
-        var hl = highlights[i];
-        var j = 0;
-        while (j < hl[0]) {
-          if (idx == line.length) {
-            idx = 0;
-            line = content[++contentIndex] + '\n';
-            continue;
-          }
-          idx++;
-          j++;
-        }
-        var lineHighlight = {
-          contentIndex: contentIndex,
-          startIndex: idx,
-        };
-
-        j = 0;
-        while (line && j < hl[1]) {
-          if (idx == line.length) {
-            idx = 0;
-            line = content[++contentIndex] + '\n';
-            normalized.push(lineHighlight);
-            lineHighlight = {
-              contentIndex: contentIndex,
-              startIndex: idx,
-            };
-            continue;
-          }
-          idx++;
-          j++;
-        }
-        lineHighlight.endIndex = idx;
-        normalized.push(lineHighlight);
-      }
-      return normalized;
-    },
-
-    _visibleLineLength: function(contents) {
-      // http://jsperf.com/performance-of-match-vs-split
-      var numTabs = contents.split('\t').length - 1;
-      return contents.length - numTabs + (this.prefs.tab_size * numTabs);
-    },
-
-    _maxLinesSpanned: function(left, right) {
-      return Math.max(
-          Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
-          Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
     },
   });
 })();
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 e46bbb8..579957e 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
@@ -20,8 +20,6 @@
 
 <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/fake-app.js"></script>
-<script src="../../../scripts/util.js"></script>
 
 <link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
 <link rel="import" href="gr-diff.html">
@@ -35,458 +33,214 @@
 <script>
   suite('gr-diff tests', function() {
     var element;
-    var server;
-    var getDiffStub;
-    var getCommentsStub;
 
     setup(function() {
+      stub('gr-rest-api-interface', {
+        getLoggedIn: function() { return Promise.resolve(false); },
+      })
       element = fixture('basic');
-      element.changeNum = 42;
-      element.path = 'sieve.go';
-      element.prefs = {
-        context: 10,
-        tab_size: 8,
-      };
-
-      getDiffStub = sinon.stub(element.$.restAPI, 'getDiff', function() {
-        return Promise.resolve({
-          change_type: 'MODIFIED',
-          content: [
-            {
-              ab: [
-                '<!DOCTYPE html>',
-                '<meta charset="utf-8">',
-                '<title>My great page</title>',
-                '<style>',
-                '  *,',
-                '  *:before,',
-                '  *:after {',
-                '    box-sizing: border-box;',
-                '  }',
-                '</style>',
-                '<header>',
-              ]
-            },
-            {
-              a: [
-                '  Welcome ',
-                '  to the wooorld of tomorrow!',
-              ],
-              b: [
-                '  Hello, world!',
-              ],
-            },
-            {
-              ab: [
-                '</header>',
-                '<body>',
-                'Leela: This is the only place the ship can’t hear us, so ',
-                'everyone pretend to shower.',
-                'Fry: Same as every day. Got it.',
-              ]
-            },
-          ]
-        });
-      });
-
-      getCommentsStub = sinon.stub(element.$.restAPI, 'getDiffComments',
-        function() {
-          return Promise.resolve({
-            baseComments: [
-              {
-                author: {
-                  _account_id: 1000000,
-                  name: 'Andrew Bonventre',
-                  email: 'andybons@gmail.com',
-                },
-                id: '9af53d3f_5f2b8b82',
-                line: 1,
-                message: 'this isn’t quite right',
-                updated: '2015-12-10 02:50:21.627000000',
-              }
-            ],
-            comments: [
-              {
-                author: {
-                  _account_id: 1010008,
-                  name: 'Dave Borowitz',
-                  email: 'dborowitz@google.com',
-                },
-                id: '001a2067_f30f3048',
-                line: 12,
-                message: 'What on earth are you thinking, here?',
-                updated: '2015-12-12 02:51:37.973000000',
-              },
-              {
-                author: {
-                  _account_id: 1000000,
-                  name: 'Andrew Bonventre',
-                  email: 'andybons@gmail.com',
-                },
-                id: 'a0407443_30dfe8fb',
-                in_reply_to: '001a2067_f30f3048',
-                line: 12,
-                message: '¯\\_(ツ)_/¯',
-                updated: '2015-12-12 18:50:21.627000000',
-              },
-            ],
-          });
-        }
-      );
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'PUT',
-        '/accounts/self/preferences.diff',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({context: 25}),
-        ]
-      );
-
     });
 
-    teardown(function() {
-      getDiffStub.restore();
-      getCommentsStub.restore();
-      server.restore();
-    });
+    test('get drafts logged out', function(done) {
+      element.patchRange = {basePatchNum: 0, patchNum: 0};
 
-    test('comment rendering', function(done) {
-      element.prefs.context = -1;
-      element._loggedIn = true;
-      element.patchRange = {
-        basePatchNum: 1,
-        patchNum: 2,
-      };
-
-      element.reload().then(function() {
-        flush(function() {
-          var leftThreadEls =
-              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                  'gr-diff-comment-thread');
-          assert.equal(leftThreadEls.length, 1);
-          assert.equal(leftThreadEls[0].comments.length, 1);
-
-          var rightThreadEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  'gr-diff-comment-thread');
-          assert.equal(rightThreadEls.length, 1);
-          assert.equal(rightThreadEls[0].comments.length, 2);
-
-          var index = leftThreadEls[0].getAttribute('data-index');
-          var leftFillerEls =
-              Polymer.dom(element.$.leftDiff.root).querySelectorAll(
-                  '.commentThread.filler[data-index="' + index + '"]');
-          assert.equal(leftFillerEls.length, 1);
-          var rightFillerEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  '[data-index="' + index + '"]');
-          assert.equal(rightFillerEls.length, 2);
-
-          for (var i = 0; i < rightFillerEls.length; i++) {
-            assert.isTrue(rightFillerEls[i].classList.contains('filler'));
-          }
-          var originalHeight = rightFillerEls[0].offsetHeight;
-          assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-          assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-          assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-
-          // Create a comment on the opposite side of the first comment.
-          var rightLineEL = element.$.rightDiff.$$(
-                '.lineNum[data-index="' + (index - 1) + '"]');
-          assert.ok(rightLineEL);
-          MockInteractions.tap(rightLineEL);
-          flush(function() {
-            var newThreadEls =
-              Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                  '[data-index="' + index + '"]');
-            assert.equal(newThreadEls.length, 2);
-            for (var i = 0; i < newThreadEls.length; i++) {
-              assert.isTrue(
-                  newThreadEls[i].classList.contains('commentThread') ||
-                  newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
-            }
-            var newHeight = newThreadEls[0].offsetHeight;
-            assert.equal(newThreadEls[1].offsetHeight, newHeight);
-            assert.equal(leftFillerEls[0].offsetHeight, newHeight);
-            assert.equal(leftThreadEls[0].offsetHeight, newHeight);
-
-            // The editing mode height of the right comment will be greater than
-            // the non-editing mode height of the left comment.
-            assert.isAbove(newHeight, originalHeight);
-
-            // Discard the right thread and ensure the left comment heights are
-            // back to their original values.
-            newThreadEls[1].addEventListener('thread-discard', function() {
-              rightFillerEls =
-                  Polymer.dom(element.$.rightDiff.root).querySelectorAll(
-                      '[data-index="' + index + '"]');
-              assert.equal(rightFillerEls.length, 2);
-
-              for (var i = 0; i < rightFillerEls.length; i++) {
-                assert.isTrue(rightFillerEls[i].classList.contains('filler'));
-              }
-              var originalHeight = rightFillerEls[0].offsetHeight;
-              assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
-              assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
-              assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
-              done();
-            });
-            var commentEl = newThreadEls[1].$$('gr-diff-comment');
-            commentEl.fire('comment-discard');
-          });
-        });
-      });
-      server.respond();
-    });
-
-    test('intraline normalization', function() {
-      // The content and highlights are in the format returned by the Gerrit
-      // REST API.
-      var content = [
-        '      <section class="summary">',
-        '        <gr-linked-text content="' +
-            '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
-        '      </section>',
-      ];
-      var highlights = [
-        [31, 34], [42, 26]
-      ];
-      var results = element._normalizeIntralineHighlights(content, highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 31,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 0,
-          endIndex: 33,
-        },
-        {
-          contentIndex: 1,
-          startIndex: 75,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 0,
-          endIndex: 6,
-        }
-      ]);
-
-      content = [
-        '        this._path = value.path;',
-        '',
-        '        // When navigating away from the page, there is a possibility that the',
-        '        // patch number is no longer a part of the URL (say when navigating to',
-        '        // the top-level change info view) and therefore undefined in `params`.',
-        '        if (!this._patchRange.patchNum) {',
-      ];
-      highlights = [
-        [14, 17],
-        [11, 70],
-        [12, 67],
-        [12, 67],
-        [14, 29],
-      ];
-      results = element._normalizeIntralineHighlights(content, highlights);
-      assert.deepEqual(results, [
-        {
-          contentIndex: 0,
-          startIndex: 14,
-          endIndex: 31,
-        },
-        {
-          contentIndex: 2,
-          startIndex: 8,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 3,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 4,
-          startIndex: 11,
-          endIndex: 78,
-        },
-        {
-          contentIndex: 5,
-          startIndex: 12,
-          endIndex: 41,
-        }
-      ]);
-    });
-
-    test('context', function() {
-      element.prefs.context = 3;
-      element._diffResponse = {
-        content: [
-          {
-            ab: [
-              '<!DOCTYPE html>',
-              '<meta charset="utf-8">',
-              '<title>My great page</title>',
-              '<style>',
-              '  *,',
-              '  *:before,',
-              '  *:after {',
-              '    box-sizing: border-box;',
-              '  }',
-              '</style>',
-              '<header>',
-            ]
-          },
-          {
-            a: [
-              '  Welcome ',
-              '  to the wooorld of tomorrow!',
-            ],
-            b: [
-              '  Hello, world!',
-            ],
-          },
-          {
-            ab: [
-              '</header>',
-              '<body>',
-              'Leela: This is the only place the ship can’t hear us, so ',
-              'everyone pretend to shower.',
-              'Fry: Same as every day. Got it.',
-            ]
-          },
-        ]
-      };
-      element._processContent();
-
-      // First eight lines should be hidden on both sides.
-      for (var i = 0; i < 8; i++) {
-        assert.isTrue(element._diff.leftSide[i].hidden);
-        assert.isTrue(element._diff.rightSide[i].hidden);
-      }
-      // A context control should be at index 8 on both sides.
-      var leftContext = element._diff.leftSide[8];
-      var rightContext = element._diff.rightSide[8];
-      assert.deepEqual(leftContext, rightContext);
-      assert.equal(leftContext.numLines, 8);
-      assert.equal(leftContext.start, 0);
-      assert.equal(leftContext.end, 8);
-
-      // Line indices 9-16 should be shown.
-      for (var i = 9; i <= 16; i++) {
-        // notOk (falsy) because the `hidden` attribute may not be present.
-        assert.notOk(element._diff.leftSide[i].hidden);
-        assert.notOk(element._diff.rightSide[i].hidden);
-      }
-
-      // Lines at indices 17 and 18 should be hidden.
-      assert.isTrue(element._diff.leftSide[17].hidden);
-      assert.isTrue(element._diff.rightSide[17].hidden);
-      assert.isTrue(element._diff.leftSide[18].hidden);
-      assert.isTrue(element._diff.rightSide[18].hidden);
-
-      // Context control at index 19.
-      leftContext = element._diff.leftSide[19];
-      rightContext = element._diff.rightSide[19];
-      assert.deepEqual(leftContext, rightContext);
-      assert.equal(leftContext.numLines, 2);
-      assert.equal(leftContext.start, 17);
-      assert.equal(leftContext.end, 19);
-    });
-
-    test('save prefs', function(done) {
-      element._loggedIn = false;
-
-      element.prefs = {
-        tab_size: 4,
-        context: 50,
-      };
-      element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
-      assert.isTrue(element._diffPreferencesPromise == null);
-
-      element._loggedIn = true;
-      element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
-      server.respond();
-
-      element._diffPreferencesPromise.then(function(req) {
-        assert.equal(req.xhr.requestBody, JSON.stringify(element.prefs));
+      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(false); });
+      element._getDiffDrafts().then(function(result) {
+        assert.deepEqual(result, {baseComments: [], comments: []});
+        sinon.assert.notCalled(getDraftsStub);
+        loggedInStub.restore();
+        getDraftsStub.restore();
         done();
       });
     });
 
-    test('visible line length', function() {
-      assert.equal(element._visibleLineLength('A'.repeat(5)), 5);
-      assert.equal(
-          element._visibleLineLength('A'.repeat(5) + '\t' + 'A'.repeat(5)), 18);
+    test('get drafts logged in', function(done) {
+      element.patchRange = {basePatchNum: 0, patchNum: 0};
+      var draftsResponse = {
+        baseComments: [{id: 'foo'}],
+        comments: [{id: 'bar'}],
+      };
+      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
+          function() { return Promise.resolve(draftsResponse); });
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(true); });
+      element._getDiffDrafts().then(function(result) {
+        assert.deepEqual(result, draftsResponse);
+        loggedInStub.restore();
+        getDraftsStub.restore();
+        done();
+      });
     });
 
-    test('break up common diff chunks', function() {
-      element._groupedBaseComments = {
-        1: {},
+    test('get comments and drafts', function(done) {
+      var loggedInStub = sinon.stub(element, '_getLoggedIn',
+          function() { return Promise.resolve(true); });
+      var comments = {
+        baseComments: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+        ],
+        comments: [
+          {id: 'c1'},
+          {id: 'c2'},
+        ],
       };
-      element._groupedComments = {
-        10: {},
+      var diffCommentsStub = sinon.stub(element, '_getDiffComments',
+          function() { return Promise.resolve(comments); });
+
+      var drafts = {
+        baseComments: [
+          {id: 'bd1'},
+          {id: 'bd2'},
+        ],
+        comments: [
+          {id: 'd1'},
+          {id: 'd2'},
+        ],
       };
-      var ctx = {
-        left: {lineNum: 0},
-        right: {lineNum: 0},
+      var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
+          function() { return Promise.resolve(drafts); });
+
+      element.changeNum = '42';
+      element.patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 3,
       };
-      var content = [
-        {
-          ab: [
-            'Copyright (C) 2015 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.',
-          ]
-        }
-      ];
-      var result = element._breakUpCommonChunksWithComments(ctx, content);
-      assert.deepEqual(result, [
-        {
-          __noHighlight: true,
-          a: ['Copyright (C) 2015 The Android Open Source Project'],
-          b: ['Copyright (C) 2015 The Android Open Source Project'],
+      element.path = '/path/to/foo';
+      element.projectConfig = {foo: 'bar'};
+
+      element._getDiffCommentsAndDrafts().then(function(result) {
+        assert.deepEqual(result, {
+          meta: {
+            changeNum: '42',
+            patchRange: {
+              basePatchNum: 'PARENT',
+              patchNum: 3,
+            },
+            path: '/path/to/foo',
+            projectConfig: {foo: 'bar'},
+          },
+          left: [
+            {id: 'bc1'},
+            {id: 'bc2'},
+            {id: 'bd1', __draft: true},
+            {id: 'bd2', __draft: true},
+          ],
+          right: [
+            {id: 'c1'},
+            {id: 'c2'},
+            {id: 'd1', __draft: true},
+            {id: 'd2', __draft: true},
+          ],
+        });
+
+        diffCommentsStub.restore();
+        diffDraftsStub.restore();
+        loggedInStub.restore();
+        done();
+      });
+    });
+
+    test('remove comment', function() {
+      element._comments = {
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          ab: [
-            '',
-            '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, ',
-          ]
+        left: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      };
+
+      element._removeComment({});
+      // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem to
+      // believe that one object deepEquals another even when they do :-/.
+      assert.equal(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          __noHighlight: true,
-          a: ['software distributed under the License is distributed on an '],
-          b: ['software distributed under the License is distributed on an ']
+        left: [
+          {id: 'bc1'},
+          {id: 'bc2'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      }));
+
+      element._removeComment({id: 'bc2'});
+      assert.equal(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
         },
-        {
-          ab: [
-            '"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.',
-          ]
-        }
-      ]);
+        left: [
+          {id: 'bc1'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+          {id: 'd2', __draft: true},
+        ],
+      }));
+
+      element._removeComment({id: 'd2'});
+      assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
+        meta: {
+          changeNum: '42',
+          patchRange: {
+            basePatchNum: 'PARENT',
+            patchNum: 3,
+          },
+          path: '/path/to/foo',
+          projectConfig: {foo: 'bar'},
+        },
+        left: [
+          {id: 'bc1'},
+          {id: 'bd1', __draft: true},
+          {id: 'bd2', __draft: true},
+        ],
+        right: [
+          {id: 'c1'},
+          {id: 'c2'},
+          {id: 'd1', __draft: true},
+        ],
+      }));
     });
   });
-
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html
deleted file mode 100644
index 352092c..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.html
+++ /dev/null
@@ -1,184 +0,0 @@
-<!--
-Copyright (C) 2015 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="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-
-<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
-<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
-<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
-
-<dom-module id="gr-new-diff">
-  <template>
-    <style>
-      :host {
-        --light-remove-highlight-color: #fee;
-        --dark-remove-highlight-color: #ffd4d4;
-        --light-add-highlight-color: #efe;
-        --dark-add-highlight-color: #d4ffd4;
-      }
-      .loading {
-        padding: 0 var(--default-horizontal-margin) 1em;
-        color: #666;
-      }
-      .header {
-        display: flex;
-        justify-content: space-between;
-        margin: 0 var(--default-horizontal-margin) .75em;
-      }
-      .prefsButton {
-        text-align: right;
-      }
-      .diffContainer {
-        border-bottom: 1px solid #eee;
-        border-top: 1px solid #eee;
-        display: flex;
-        font: 12px var(--monospace-font-family);
-        overflow-x: auto;
-        will-change: transform;
-      }
-      table {
-        border-collapse: collapse;
-        border-right: 1px solid #ddd;
-      }
-      .section {
-        background-color: #eee;
-      }
-      .blank,
-      .content {
-        background-color: #fff;
-      }
-      .lineNum,
-      .content {
-        vertical-align: top;
-        white-space: pre;
-      }
-      .contextLineNum:before,
-      .lineNum:before {
-        display: inline-block;
-        color: #666;
-        content: attr(data-value);
-        padding: 0 .75em;
-        text-align: right;
-        width: 100%;
-      }
-      .canComment .lineNum[data-value] {
-        cursor: pointer;
-      }
-      .canComment .lineNum[data-value]:before {
-        text-decoration: underline;
-      }
-      .canComment .lineNum[data-value]:hover:before {
-        background-color: #ccc;
-      }
-      .canComment .lineNum[data-value="FILE"]:before {
-        content: 'File';
-      }
-      .content {
-        overflow: hidden;
-        min-width: var(--content-width, 80ch);
-      }
-      .content.left {
-        -webkit-user-select: var(--left-user-select, text);
-        -moz-user-select: var(--left-user-select, text);
-        -ms-user-select: var(--left-user-select, text);
-        user-select: var(--left-user-select, text);
-      }
-      .content.right {
-        -webkit-user-select: var(--right-user-select, text);
-        -moz-user-select: var(--right-user-select, text);
-        -ms-user-select: var(--right-user-select, text);
-        user-select: var(--right-user-select, text);
-      }
-      .content.add hl,
-      .content.add.darkHighlight {
-        background-color: var(--dark-add-highlight-color);
-      }
-      .content.add.lightHighlight {
-        background-color: var(--light-add-highlight-color);
-      }
-      .content.remove hl,
-      .content.remove.darkHighlight {
-        background-color: var(--dark-remove-highlight-color);
-      }
-      .content.remove.lightHighlight {
-        background-color: var(--light-remove.highlight-color);
-      }
-      .contextControl {
-        color: #849;
-        background-color: #fef;
-      }
-      .contextControl gr-button {
-        display: block;
-        font-family: var(--monospace-font-family);
-        text-decoration: none;
-      }
-      .contextControl td:not(.lineNum) {
-        text-align: center;
-      }
-      .br:after {
-        /* Line feed */
-        content: '\A';
-      }
-      .tab {
-        display: inline-block;
-      }
-      .tab.withIndicator:before {
-        color: #C62828;
-        /* >> character */
-        content: '\00BB';
-      }
-    </style>
-    <div class="loading" hidden$="[[!_loading]]">Loading...</div>
-    <div hidden$="[[_loading]]" hidden>
-      <div class="header">
-        <gr-patch-range-select
-            path="[[path]]"
-            change-num="[[changeNum]]"
-            patch-range="[[patchRange]]"
-            available-patches="[[availablePatches]]"></gr-patch-range-select>
-        <gr-button link
-           class="prefsButton"
-           on-tap="_handlePrefsTap"
-           hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
-           hidden>Diff View Preferences</gr-button>
-      </div>
-      <gr-overlay id="prefsOverlay" with-backdrop>
-        <gr-diff-preferences
-            prefs="{{_prefs}}"
-            on-save="_handlePrefsSave"
-            on-cancel="_handlePrefsCancel"></gr-diff-preferences>
-      </gr-overlay>
-
-      <div class$="[[_computeContainerClass(_loggedIn, _viewMode)]]"
-          on-tap="_handleTap"
-          on-mousedown="_handleMouseDown"
-          on-copy="_handleCopy">
-        <table id="diffTable"></table>
-      </div>
-    </div>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  </template>
-  <script src="gr-diff-line.js"></script>
-  <script src="gr-diff-group.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-new-diff.js"></script>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js
deleted file mode 100644
index aafea43..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff.js
+++ /dev/null
@@ -1,516 +0,0 @@
-// 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.
-(function() {
-  'use strict';
-
-  var DiffViewMode = {
-    SIDE_BY_SIDE: 'SIDE_BY_SIDE',
-    UNIFIED: 'UNIFIED_DIFF',
-  };
-
-  var DiffSide = {
-    LEFT: 'left',
-    RIGHT: 'right',
-  };
-
-  Polymer({
-    is: 'gr-new-diff',
-
-    /**
-     * Fired when the diff is rendered.
-     *
-     * @event render
-     */
-
-    properties: {
-      availablePatches: Array,
-      changeNum: String,
-      patchRange: Object,
-      path: String,
-
-      projectConfig: {
-        type: Object,
-        observer: '_projectConfigChanged',
-      },
-
-      _loggedIn: {
-        type: Boolean,
-        value: false,
-      },
-      _loading: {
-        type: Boolean,
-        value: true,
-      },
-      _viewMode: {
-        type: String,
-        value: DiffViewMode.SIDE_BY_SIDE,
-      },
-      _diff: Object,
-      _diffBuilder: Object,
-      _prefs: Object,
-      _selectionSide: {
-        type: String,
-        observer: '_selectionSideChanged',
-      },
-      _comments: Object,
-      _focusedSection: {
-        type: Number,
-        value: -1,
-      },
-      _focusedThread: {
-        type: Number,
-        value: -1,
-      },
-    },
-
-    observers: [
-      '_prefsChanged(_prefs.*)',
-    ],
-
-    attached: function() {
-      this._getLoggedIn().then(function(loggedIn) {
-        this._loggedIn = loggedIn;
-      }.bind(this));
-
-      this.addEventListener('thread-discard',
-          this._handleThreadDiscard.bind(this));
-      this.addEventListener('comment-discard',
-          this._handleCommentDiscard.bind(this));
-    },
-
-    reload: function() {
-      this._clearDiffContent();
-      this._loading = true;
-
-      var promises = [];
-
-      promises.push(this._getDiff().then(function(diff) {
-        this._diff = diff;
-        this._loading = false;
-      }.bind(this)));
-
-      promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
-        this._comments = comments;
-      }.bind(this)));
-
-      promises.push(this._getDiffPreferences().then(function(prefs) {
-        this._prefs = prefs;
-      }.bind(this)));
-
-      return Promise.all(promises).then(function() {
-        this._render();
-      }.bind(this));
-    },
-
-    scrollToLine: function(lineNum) {
-      if (isNaN(lineNum) || lineNum < 1) { return; }
-
-      var lineEls = Polymer.dom(this.root).querySelectorAll(
-          '.lineNum[data-value="' + lineNum + '"]');
-
-      // Always choose the right side.
-      var el = lineEls.length === 2 ? lineEls[1] : lineEls[0];
-      this._scrollToElement(el);
-    },
-
-    scrollToNextDiffChunk: function() {
-      this._focusedSection = this._advanceElementWithinNodeList(
-          this._getDeltaSections(), this._focusedSection, 1);
-    },
-
-    scrollToPreviousDiffChunk: function() {
-      this._focusedSection = this._advanceElementWithinNodeList(
-          this._getDeltaSections(), this._focusedSection, -1);
-    },
-
-    scrollToNextCommentThread: function() {
-      this._focusedThread = this._advanceElementWithinNodeList(
-          this._getCommentThreads(), this._focusedThread, 1);
-    },
-
-    scrollToPreviousCommentThread: function() {
-      this._focusedThread = this._advanceElementWithinNodeList(
-          this._getCommentThreads(), this._focusedThread, -1);
-    },
-
-    _advanceElementWithinNodeList: function(els, curIndex, direction) {
-      var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction));
-      if (curIndex !== idx) {
-        this._scrollToElement(els[idx]);
-        return idx;
-      }
-      return curIndex;
-    },
-
-    _getCommentThreads: function() {
-      return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
-    },
-
-    _getDeltaSections: function() {
-      return Polymer.dom(this.root).querySelectorAll('.section.delta');
-    },
-
-    _scrollToElement: function(el) {
-      if (!el) { return; }
-
-      // Calculate where the element is relative to the window.
-      var top = el.offsetTop;
-      for (var offsetParent = el.offsetParent;
-           offsetParent;
-           offsetParent = offsetParent.offsetParent) {
-        top += offsetParent.offsetTop;
-      }
-
-      // Scroll the element to the middle of the window. Dividing by a third
-      // instead of half the inner height feels a bit better otherwise the
-      // element appears to be below the center of the window even when it
-      // isn't.
-      window.scrollTo(0, top - (window.innerHeight / 3) +
-          (el.offsetHeight / 2));
-    },
-
-    _computeContainerClass: function(loggedIn, viewMode) {
-      var classes = ['diffContainer'];
-      switch (viewMode) {
-        case DiffViewMode.UNIFIED:
-          classes.push('unified');
-          break;
-        case DiffViewMode.SIDE_BY_SIDE:
-          classes.push('sideBySide');
-          break
-        default:
-          throw Error('Invalid view mode: ', viewMode);
-      }
-      if (loggedIn) {
-        classes.push('canComment');
-      }
-      return classes.join(' ');
-    },
-
-    _computePrefsButtonHidden: function(prefs, loggedIn) {
-      return !loggedIn || !prefs;
-    },
-
-    _handlePrefsTap: function(e) {
-      e.preventDefault();
-      this.$.prefsOverlay.open();
-    },
-
-    _handlePrefsSave: function(e) {
-      e.stopPropagation();
-      var el = Polymer.dom(e).rootTarget;
-      el.disabled = true;
-      this._saveDiffPreferences().then(function() {
-        this.$.prefsOverlay.close();
-        el.disabled = false;
-      }.bind(this)).catch(function(err) {
-        el.disabled = false;
-        alert('Oops. Something went wrong. Check the console and bug the ' +
-              'PolyGerrit team for assistance.');
-        throw err;
-      });
-    },
-
-    _saveDiffPreferences: function() {
-      return this.$.restAPI.saveDiffPreferences(this._prefs);
-    },
-
-    _handlePrefsCancel: function(e) {
-      e.stopPropagation();
-      this.$.prefsOverlay.close();
-    },
-
-    _handleTap: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-
-      if (el.classList.contains('showContext')) {
-        this._showContext(e.detail.group, e.detail.section);
-      } else if (el.classList.contains('lineNum')) {
-        this._handleLineTap(el);
-      }
-    },
-
-    _handleLineTap: function(el) {
-      this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) { return; }
-
-        var value = el.getAttribute('data-value');
-        if (value === GrDiffLine.FILE) {
-          this._addDraft(el);
-          return;
-        }
-        var lineNum = parseInt(value, 10);
-        if (isNaN(lineNum)) {
-          throw Error('Invalid line number: ' + value);
-        }
-        this._addDraft(el, lineNum);
-      }.bind(this));
-    },
-
-    _addDraft: function(lineEl, opt_lineNum) {
-      var threadEl;
-
-      // Does a thread already exist at this line?
-      var contentEl = lineEl.nextSibling;
-      while (contentEl && !contentEl.classList.contains('content')) {
-        contentEl = contentEl.nextSibling;
-      }
-      if (contentEl.childNodes.length > 0 &&
-          contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') {
-        threadEl = contentEl.lastChild;
-      } else {
-        var patchNum = this.patchRange.patchNum;
-        var side = 'REVISION';
-        if (contentEl.classList.contains(DiffSide.LEFT) ||
-            contentEl.classList.contains('remove')) {
-          if (this.patchRange.basePatchNum === 'PARENT') {
-            side = 'PARENT';
-          } else {
-            patchNum = this.patchRange.basePatchNum;
-          }
-        }
-        threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
-            this.path, side, this.projectConfig);
-        contentEl.appendChild(threadEl);
-      }
-      threadEl.addDraft(opt_lineNum);
-    },
-
-    _handleThreadDiscard: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-      el.parentNode.removeChild(el);
-    },
-
-    _handleCommentDiscard: function(e) {
-      var comment = Polymer.dom(e).rootTarget.comment;
-      this._removeComment(comment);
-    },
-
-    _removeComment: function(comment) {
-      if (!comment.id) { return; }
-      this._removeCommentFromSide(comment, DiffSide.LEFT) ||
-          this._removeCommentFromSide(comment, DiffSide.RIGHT);
-    },
-
-    _removeCommentFromSide: function(comment, side) {
-      var idx = -1;
-      for (var i = 0; i < this._comments[side].length; i++) {
-        if (this._comments[side][i].id === comment.id) {
-          idx = i;
-          break;
-        }
-      }
-      if (idx !== -1) {
-        this.splice('_comments.' + side, idx, 1);
-        return true;
-      }
-      return false;
-    },
-
-    _handleMouseDown: function(e) {
-      var el = Polymer.dom(e).rootTarget;
-      var side;
-      for (var node = el; node != null; node = node.parentNode) {
-        if (!node.classList) { continue; }
-
-        if (node.classList.contains(DiffSide.LEFT)) {
-          side = DiffSide.LEFT;
-          break;
-        } else if (node.classList.contains(DiffSide.RIGHT)) {
-          side = DiffSide.RIGHT;
-          break;
-        }
-      }
-      this._selectionSide = side;
-    },
-
-    _selectionSideChanged: function(side) {
-      if (side) {
-        var oppositeSide = side === DiffSide.RIGHT ?
-            DiffSide.LEFT : DiffSide.RIGHT;
-        this.customStyle['--' + side + '-user-select'] = 'text';
-        this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
-      } else {
-        this.customStyle['--left-user-select'] = 'text';
-        this.customStyle['--right-user-select'] = 'text';
-      }
-      this.updateStyles();
-    },
-
-    _handleCopy: function(e) {
-      var text = this._getSelectedText(this._selectionSide);
-      e.clipboardData.setData('Text', text);
-      e.preventDefault();
-    },
-
-    _getSelectedText: function(opt_side) {
-      var sel = window.getSelection();
-      var range = sel.getRangeAt(0);
-      var doc = range.cloneContents();
-      var selector = '.content';
-      if (opt_side) {
-        selector += '.' + opt_side;
-      }
-      var contentEls = Polymer.dom(doc).querySelectorAll(selector);
-
-      if (contentEls.length === 0) {
-        return doc.textContent;
-      }
-
-      var text = '';
-      for (var i = 0; i < contentEls.length; i++) {
-        text += contentEls[i].textContent + '\n';
-      }
-      return text;
-    },
-
-    _showContext: function(group, sectionEl) {
-      this._builder.emitGroup(group, sectionEl);
-      sectionEl.parentNode.removeChild(sectionEl);
-    },
-
-    _prefsChanged: function(prefsChangeRecord) {
-      var prefs = prefsChangeRecord.base;
-      this.customStyle['--content-width'] = prefs.line_length + 'ch';
-      this.updateStyles();
-
-      if (this._diff && this._comments) {
-        this._render();
-      }
-    },
-
-    _render: function() {
-      this._clearDiffContent();
-      this._builder = this._getDiffBuilder(this._diff, this._comments,
-          this._prefs);
-      this._builder.emitDiff(this._diff.content);
-
-      this.async(function() {
-        this.fire('render', null, {bubbles: false});
-      }.bind(this), 1);
-    },
-
-    _clearDiffContent: function() {
-      this.$.diffTable.innerHTML = null;
-    },
-
-    _getDiff: function() {
-      return this.$.restAPI.getDiff(
-          this.changeNum,
-          this.patchRange.basePatchNum,
-          this.patchRange.patchNum,
-          this.path);
-    },
-
-    _getDiffComments: function() {
-      return this.$.restAPI.getDiffComments(
-          this.changeNum,
-          this.patchRange.basePatchNum,
-          this.patchRange.patchNum,
-          this.path);
-    },
-
-    _getDiffDrafts: function() {
-      return this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) {
-          return Promise.resolve({baseComments: [], comments: []});
-        }
-        return this.$.restAPI.getDiffDrafts(
-            this.changeNum,
-            this.patchRange.basePatchNum,
-            this.patchRange.patchNum,
-            this.path);
-      }.bind(this));
-    },
-
-    _getDiffCommentsAndDrafts: function() {
-      var promises = [];
-      promises.push(this._getDiffComments());
-      promises.push(this._getDiffDrafts());
-      return Promise.all(promises).then(function(results) {
-        return Promise.resolve({
-          comments: results[0],
-          drafts: results[1],
-        });
-      }).then(this._normalizeDiffCommentsAndDrafts.bind(this));
-    },
-
-    _getDiffPreferences: function() {
-      return this._getLoggedIn().then(function(loggedIn) {
-        if (!loggedIn) {
-          // These defaults should match the defaults in
-          // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
-          // NOTE: There are some settings that don't apply to PolyGerrit
-          // (Render mode being at least one of them).
-          return Promise.resolve({
-            auto_hide_diff_table_header: true,
-            context: 10,
-            cursor_blink_rate: 0,
-            ignore_whitespace: 'IGNORE_NONE',
-            intraline_difference: true,
-            line_length: 100,
-            show_line_endings: true,
-            show_tabs: true,
-            show_whitespace_errors: true,
-            syntax_highlighting: true,
-            tab_size: 8,
-            theme: 'DEFAULT',
-          });
-        }
-        return this.$.restAPI.getDiffPreferences();
-      }.bind(this));
-    },
-
-    _normalizeDiffCommentsAndDrafts: function(results) {
-      function markAsDraft(d) {
-        d.__draft = true;
-        return d;
-      }
-      var baseDrafts = results.drafts.baseComments.map(markAsDraft);
-      var drafts = results.drafts.comments.map(markAsDraft);
-      return Promise.resolve({
-        meta: {
-          path: this.path,
-          changeNum: this.changeNum,
-          patchRange: this.patchRange,
-          projectConfig: this.projectConfig,
-        },
-        left: results.comments.baseComments.concat(baseDrafts),
-        right: results.comments.comments.concat(drafts),
-      });
-    },
-
-    _getLoggedIn: function() {
-      return this.$.restAPI.getLoggedIn();
-    },
-
-    _getDiffBuilder: function(diff, comments, prefs) {
-      if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
-        return new GrDiffBuilderSideBySide(diff, comments, prefs,
-            this.$.diffTable);
-      } else if (this._viewMode === DiffViewMode.UNIFIED) {
-        return new GrDiffBuilderUnified(diff, comments, prefs,
-            this.$.diffTable);
-      }
-      throw Error('Unsupported diff view mode: ' + this._viewMode);
-    },
-
-    _projectConfigChanged: function(projectConfig) {
-      var threadEls = this._getCommentThreads();
-      for (var i = 0; i < threadEls.length; i++) {
-        threadEls[i].projectConfig = projectConfig;
-      }
-    },
-  });
-})();
diff --git a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html b/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html
deleted file mode 100644
index b015df4..0000000
--- a/polygerrit-ui/app/elements/diff/gr-new-diff/gr-new-diff_test.html
+++ /dev/null
@@ -1,244 +0,0 @@
-<!DOCTYPE html>
-<!--
-Copyright (C) 2015 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.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-new-diff</title>
-
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
-<script src="../../../bower_components/web-component-tester/browser.js"></script>
-
-<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
-<link rel="import" href="gr-new-diff.html">
-
-<test-fixture id="basic">
-  <template>
-    <gr-new-diff></gr-new-diff>
-  </template>
-</test-fixture>
-
-<script>
-  suite('gr-new-diff tests', function() {
-    var element;
-
-    setup(function() {
-      stub('gr-rest-api-interface', {
-        getLoggedIn: function() { return Promise.resolve(false); },
-      })
-      element = fixture('basic');
-    });
-
-    test('get drafts logged out', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
-
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(false); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, {baseComments: [], comments: []});
-        sinon.assert.notCalled(getDraftsStub);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
-      });
-    });
-
-    test('get drafts logged in', function(done) {
-      element.patchRange = {basePatchNum: 0, patchNum: 0};
-      var draftsResponse = {
-        baseComments: [{id: 'foo'}],
-        comments: [{id: 'bar'}],
-      };
-      var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
-          function() { return Promise.resolve(draftsResponse); });
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      element._getDiffDrafts().then(function(result) {
-        assert.deepEqual(result, draftsResponse);
-        loggedInStub.restore();
-        getDraftsStub.restore();
-        done();
-      });
-    });
-
-    test('get comments and drafts', function(done) {
-      var loggedInStub = sinon.stub(element, '_getLoggedIn',
-          function() { return Promise.resolve(true); });
-      var comments = {
-        baseComments: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-        ],
-        comments: [
-          {id: 'c1'},
-          {id: 'c2'},
-        ],
-      };
-      var diffCommentsStub = sinon.stub(element, '_getDiffComments',
-          function() { return Promise.resolve(comments); });
-
-      var drafts = {
-        baseComments: [
-          {id: 'bd1'},
-          {id: 'bd2'},
-        ],
-        comments: [
-          {id: 'd1'},
-          {id: 'd2'},
-        ],
-      };
-      var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
-          function() { return Promise.resolve(drafts); });
-
-      element.changeNum = '42';
-      element.patchRange = {
-        basePatchNum: 'PARENT',
-        patchNum: 3,
-      };
-      element.path = '/path/to/foo';
-      element.projectConfig = {foo: 'bar'};
-
-      element._getDiffCommentsAndDrafts().then(function(result) {
-        assert.deepEqual(result, {
-          meta: {
-            changeNum: '42',
-            patchRange: {
-              basePatchNum: 'PARENT',
-              patchNum: 3,
-            },
-            path: '/path/to/foo',
-            projectConfig: {foo: 'bar'},
-          },
-          left: [
-            {id: 'bc1'},
-            {id: 'bc2'},
-            {id: 'bd1', __draft: true},
-            {id: 'bd2', __draft: true},
-          ],
-          right: [
-            {id: 'c1'},
-            {id: 'c2'},
-            {id: 'd1', __draft: true},
-            {id: 'd2', __draft: true},
-          ],
-        });
-
-        diffCommentsStub.restore();
-        diffDraftsStub.restore();
-        loggedInStub.restore();
-        done();
-      });
-    });
-
-    test('remove comment', function() {
-      element._comments = {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      };
-
-      element._removeComment({});
-      assert.deepEqual(element._comments, {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bc2'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      });
-
-      element._removeComment({id: 'bc2'});
-      assert.deepEqual(element._comments, {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-          {id: 'd2', __draft: true},
-        ],
-      });
-
-      element._removeComment({id: 'd2'});
-      assert.deepEqual(element._comments, {
-        meta: {
-          changeNum: '42',
-          patchRange: {
-            basePatchNum: 'PARENT',
-            patchNum: 3,
-          },
-          path: '/path/to/foo',
-          projectConfig: {foo: 'bar'},
-        },
-        left: [
-          {id: 'bc1'},
-          {id: 'bd1', __draft: true},
-          {id: 'bd2', __draft: true},
-        ],
-        right: [
-          {id: 'c1'},
-          {id: 'c2'},
-          {id: 'd1', __draft: true},
-        ],
-      });
-    });
-  });
-</script>