Wyatt Allen db881fa0ad Asynchronous diff rendering
Building on existing support for asynchronous diff processing, the
rendering stage is now also asynchronous. The `emitGroup` method of
gr-diff-builder (which constructs a DOM fragment and attaches it to the
document) is now called whenever the processor emits a new group, rather
than waiting for all groups to be available and looping over them.

Adds support for canceling the processor, and removes a behavior where
the diff was being re-rendered needlessly, causing visible flicker.
Updates a test that broke when rendering became asynchronous.

Change-Id: I37fcd65efc8c901b85fc93e91611c022edc10dc4
2016-06-30 10:09:19 -07:00

265 lines
9.0 KiB
HTML

<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
<dom-module id="gr-diff-builder">
<template>
<div class="contentWrapper">
<content></content>
</div>
<gr-diff-processor
id="processor"
groups="{{_groups}}"></gr-diff-processor>
</template>
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
<script src="gr-diff-builder.js"></script>
<script src="gr-diff-builder-side-by-side.js"></script>
<script src="gr-diff-builder-unified.js"></script>
<script src="gr-diff-builder-image.js"></script>
<script>
(function() {
'use strict';
var DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
};
Polymer({
is: 'gr-diff-builder',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
viewMode: String,
isImageDiff: Boolean,
baseImage: Object,
revisionImage: Object,
_builder: Object,
_groups: Array,
},
get diffElement() {
return this.queryEffectiveChildren('#diffTable');
},
observers: [
'_groupsChanged(_groups.splices)',
],
render: function(diff, comments, prefs) {
// Stop the processor (if it's running).
this.$.processor.cancel();
this._builder = this._getDiffBuilder(diff, comments, prefs);
this.$.processor.context = prefs.context;
this.$.processor.keyLocations = this._getCommentLocations(comments);
this._clearDiffContent();
this.$.processor.process(diff.content).then(function() {
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
this.fire('render');
}.bind(this));
},
getLineElByChild: function(node) {
while (node) {
if (node instanceof Element) {
if (node.classList.contains('lineNum')) {
return node;
}
if (node.classList.contains('section')) {
return null;
}
}
node = node.previousSibling || node.parentElement;
}
return null;
},
renderLineRange: function(startLine, endLine, opt_side) {
var groups =
this._builder.getGroupsByLineRange(startLine, endLine, opt_side);
groups.forEach(function(group) {
var newElement = this._builder.buildSectionElement(group);
var oldElement = group.element;
// Transfer comment threads from existing section to new one.
var threads = Polymer.dom(newElement).querySelectorAll(
'gr-diff-comment-thread');
threads.forEach(function(threadEl) {
var lineEl = this.getLineElByChild(threadEl, oldElement);
if (!lineEl) { // New comment thread.
return;
}
var side = this.getSideByLineEl(lineEl);
var line = lineEl.getAttribute('data-value');
var oldThreadEl =
this.getCommentThreadByLine(line, side, oldElement);
threadEl.parentNode.replaceChild(oldThreadEl, threadEl);
}, this);
// Replace old group elements with new ones.
group.element.parentNode.replaceChild(newElement, group.element);
group.element = newElement;
}, this);
this.async(function() {
this.fire('render');
}, 1);
},
getContentByLine: function(lineNumber, opt_side, opt_root) {
var root = Polymer.dom(opt_root || this.diffElement);
var sideSelector = !!opt_side ? ('.' + opt_side) : '';
return root.querySelector('td.lineNum[data-value="' + lineNumber +
'"]' + sideSelector + ' ~ td.content');
},
getContentByLineEl: function(lineEl) {
var root = Polymer.dom(lineEl.parentElement);
var side = this.getSideByLineEl(lineEl);
var line = lineEl.getAttribute('data-value');
return this.getContentByLine(line, side, root);
},
getLineElByNumber: function(lineNumber, opt_side) {
var sideSelector = !!opt_side ? ('.' + opt_side) : '';
return this.diffElement.querySelector(
'.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
},
getContentsByLineRange: function(startLine, endLine, opt_side) {
var groups =
this._builder.getGroupsByLineRange(startLine, endLine, opt_side);
// In each group, search Element for lines in range.
return groups.reduce((function(acc, group) {
for (var line = startLine; line <= endLine; line++) {
var content =
this.getContentByLine(line, opt_side, group.element);
if (content) {
acc.push(content);
}
}
return acc;
}).bind(this), []);
},
getCommentThreadByLine: function(lineNumber, opt_side, opt_root) {
var root = Polymer.dom(opt_root || this.diffElement);
var sideSelector = !!opt_side ? ('.' + opt_side) : '';
var content = this.getContentByLine(lineNumber, opt_side, opt_root);
return this.getCommentThreadByContentEl(content);
},
getCommentThreadByContentEl: function(contentEl) {
return contentEl.querySelector('gr-diff-comment-thread');
},
getSideByLineEl: function(lineEl) {
return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
},
createCommentThread: function(changeNum, patchNum, path, side,
projectConfig) {
return this._builder.createCommentThread(changeNum, patchNum, path,
side, projectConfig);
},
emitGroup: function(group, sectionEl) {
this._builder.emitGroup(group, sectionEl);
},
showContext: function(newGroups, sectionEl) {
var groups = this._builder.groups;
// TODO(viktard): Polyfill findIndex for IE10.
var contextIndex = groups.findIndex(function(group) {
return group.element == sectionEl;
});
groups.splice.apply(groups, [contextIndex, 1].concat(newGroups));
newGroups.forEach(function(newGroup) {
this._builder.emitGroup(newGroup, sectionEl);
}, this);
sectionEl.parentNode.removeChild(sectionEl);
this.async(function() {
this.fire('render');
}, 1);
},
_getDiffBuilder: function(diff, comments, prefs) {
if (this.isImageDiff) {
return new GrDiffBuilderImage(diff, comments, prefs,
this.diffElement, this.baseImage, this.revisionImage);
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
return new GrDiffBuilderSideBySide(
diff, comments, prefs, this.diffElement);
} else if (this.viewMode === DiffViewMode.UNIFIED) {
return new GrDiffBuilderUnified(
diff, comments, prefs, this.diffElement);
}
throw Error('Unsupported diff view mode: ' + this.viewMode);
},
_clearDiffContent: function() {
this.diffElement.innerHTML = null;
},
_getCommentLocations: function(comments) {
var result = {
left: {},
right: {},
};
for (var side in comments) {
if (side !== GrDiffBuilder.Side.LEFT &&
side !== GrDiffBuilder.Side.RIGHT) {
continue;
}
comments[side].forEach(function(c) {
result[side][c.line || GrDiffLine.FILE] = true;
});
}
return result;
},
_groupsChanged: function(changeRecord) {
if (!changeRecord) { return; }
changeRecord.indexSplices.forEach(function(splice) {
var group;
for (var i = 0; i < splice.addedCount; i++) {
group = splice.object[splice.index + i];
this._builder.groups.push(group);
this._builder.emitGroup(group);
}
}, this);
},
});
})();
</script>
</dom-module>