441 lines
15 KiB
HTML
441 lines
15 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="../../core/gr-reporting/gr-reporting.html">
|
|
<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
|
|
<link rel="import" href="../gr-diff-comment-thread-group/gr-diff-comment-thread-group.html">
|
|
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
|
|
<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html">
|
|
<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html">
|
|
|
|
<dom-module id="gr-diff-builder">
|
|
<template>
|
|
<div class="contentWrapper">
|
|
<slot></slot>
|
|
</div>
|
|
<gr-ranged-comment-layer
|
|
id="rangeLayer"
|
|
comments="[[comments]]"></gr-ranged-comment-layer>
|
|
<gr-syntax-layer
|
|
id="syntaxLayer"
|
|
diff="[[diff]]"></gr-syntax-layer>
|
|
<gr-diff-processor
|
|
id="processor"
|
|
groups="{{_groups}}"></gr-diff-processor>
|
|
<gr-reporting id="reporting"></gr-reporting>
|
|
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
|
|
</template>
|
|
<script src="../gr-diff/gr-diff-line.js"></script>
|
|
<script src="../gr-diff/gr-diff-group.js"></script>
|
|
<script src="../gr-diff-highlight/gr-annotation.js"></script>
|
|
<script src="gr-diff-builder.js"></script>
|
|
<script src="gr-diff-builder-side-by-side.js"></script>
|
|
<script src="gr-diff-builder-unified.js"></script>
|
|
<script src="gr-diff-builder-image.js"></script>
|
|
<script src="gr-diff-builder-binary.js"></script>
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const DiffViewMode = {
|
|
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
|
|
UNIFIED: 'UNIFIED_DIFF',
|
|
};
|
|
|
|
const TimingLabel = {
|
|
TOTAL: 'Diff Total Render',
|
|
CONTENT: 'Diff Content Render',
|
|
SYNTAX: 'Diff Syntax Render',
|
|
};
|
|
|
|
// If any line of the diff is more than the character limit, then disable
|
|
// syntax highlighting for the entire file.
|
|
const SYNTAX_MAX_LINE_LENGTH = 500;
|
|
|
|
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
|
|
|
|
Polymer({
|
|
is: 'gr-diff-builder',
|
|
|
|
/**
|
|
* Fired when the diff begins rendering.
|
|
*
|
|
* @event render-start
|
|
*/
|
|
|
|
/**
|
|
* Fired when the diff is rendered.
|
|
*
|
|
* @event render
|
|
*/
|
|
|
|
/**
|
|
* Fired when the diff finishes rendering text content, but not
|
|
* necessarily syntax highlights.
|
|
*
|
|
* @event render-content
|
|
*/
|
|
|
|
properties: {
|
|
diff: Object,
|
|
diffPath: String,
|
|
changeNum: String,
|
|
patchNum: String,
|
|
viewMode: String,
|
|
comments: Object,
|
|
isImageDiff: Boolean,
|
|
baseImage: Object,
|
|
revisionImage: Object,
|
|
projectName: String,
|
|
parentIndex: Number,
|
|
_builder: Object,
|
|
_groups: Array,
|
|
_layers: Array,
|
|
_showTabs: Boolean,
|
|
},
|
|
|
|
get diffElement() {
|
|
return this.queryEffectiveChildren('#diffTable');
|
|
},
|
|
|
|
observers: [
|
|
'_groupsChanged(_groups.splices)',
|
|
],
|
|
|
|
attached() {
|
|
// Setup annotation layers.
|
|
const layers = [
|
|
this._createTrailingWhitespaceLayer(),
|
|
this.$.syntaxLayer,
|
|
this._createIntralineLayer(),
|
|
this._createTabIndicatorLayer(),
|
|
this.$.rangeLayer,
|
|
];
|
|
|
|
// Get layers from plugins (if any).
|
|
for (const pluginLayer of this.$.jsAPI.getDiffLayers(
|
|
this.diffPath, this.changeNum, this.patchNum)) {
|
|
layers.push(pluginLayer);
|
|
}
|
|
|
|
this._layers = layers;
|
|
|
|
this.async(() => {
|
|
this._preRenderThread();
|
|
});
|
|
},
|
|
|
|
render(comments, prefs) {
|
|
this.$.syntaxLayer.enabled = prefs.syntax_highlighting;
|
|
this._showTabs = !!prefs.show_tabs;
|
|
this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
|
|
|
|
// Stop the processor and syntax layer (if they're running).
|
|
this.cancel();
|
|
|
|
this._builder = this._getDiffBuilder(this.diff, comments, prefs);
|
|
|
|
this.$.processor.context = prefs.context;
|
|
this.$.processor.keyLocations = this._getCommentLocations(comments);
|
|
|
|
this._clearDiffContent();
|
|
this._builder.addColumns(this.diffElement, prefs.font_size);
|
|
|
|
const reporting = this.$.reporting;
|
|
const isBinary = !!(this.isImageDiff || this.diff.binary);
|
|
|
|
reporting.time(TimingLabel.TOTAL);
|
|
reporting.time(TimingLabel.CONTENT);
|
|
this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
|
|
return this.$.processor.process(this.diff.content, isBinary)
|
|
.then(() => {
|
|
if (this.isImageDiff) {
|
|
this._builder.renderDiffImages();
|
|
}
|
|
this.dispatchEvent(new CustomEvent('render-content',
|
|
{bubbles: true}));
|
|
|
|
if (this._anyLineTooLong()) {
|
|
this.$.syntaxLayer.enabled = false;
|
|
}
|
|
|
|
reporting.timeEnd(TimingLabel.CONTENT);
|
|
reporting.time(TimingLabel.SYNTAX);
|
|
return this.$.syntaxLayer.process().then(() => {
|
|
reporting.timeEnd(TimingLabel.SYNTAX);
|
|
reporting.timeEnd(TimingLabel.TOTAL);
|
|
this.dispatchEvent(
|
|
new CustomEvent('render', {bubbles: true}));
|
|
});
|
|
});
|
|
},
|
|
|
|
getLineElByChild(node) {
|
|
while (node) {
|
|
if (node instanceof Element) {
|
|
if (node.classList.contains('lineNum')) {
|
|
return node;
|
|
}
|
|
if (node.classList.contains('section')) {
|
|
return null;
|
|
}
|
|
}
|
|
node = node.previousSibling || node.parentElement;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getLineNumberByChild(node) {
|
|
const lineEl = this.getLineElByChild(node);
|
|
return lineEl ?
|
|
parseInt(lineEl.getAttribute('data-value'), 10) :
|
|
null;
|
|
},
|
|
|
|
getContentByLine(lineNumber, opt_side, opt_root) {
|
|
return this._builder.getContentByLine(lineNumber, opt_side, opt_root);
|
|
},
|
|
|
|
getContentByLineEl(lineEl) {
|
|
const root = Polymer.dom(lineEl.parentElement);
|
|
const side = this.getSideByLineEl(lineEl);
|
|
const line = lineEl.getAttribute('data-value');
|
|
return this.getContentByLine(line, side, root);
|
|
},
|
|
|
|
getLineElByNumber(lineNumber, opt_side) {
|
|
const sideSelector = opt_side ? ('.' + opt_side) : '';
|
|
return this.diffElement.querySelector(
|
|
'.lineNum[data-value="' + lineNumber + '"]' + sideSelector);
|
|
},
|
|
|
|
getContentsByLineRange(startLine, endLine, opt_side) {
|
|
const result = [];
|
|
this._builder.findLinesByRange(startLine, endLine, opt_side, null,
|
|
result);
|
|
return result;
|
|
},
|
|
|
|
getSideByLineEl(lineEl) {
|
|
return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ?
|
|
GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT;
|
|
},
|
|
|
|
createCommentThreadGroup(changeNum, patchNum, path,
|
|
isOnParent, commentSide) {
|
|
return this._builder.createCommentThreadGroup(changeNum, patchNum,
|
|
path, isOnParent, commentSide);
|
|
},
|
|
|
|
emitGroup(group, sectionEl) {
|
|
this._builder.emitGroup(group, sectionEl);
|
|
},
|
|
|
|
showContext(newGroups, sectionEl) {
|
|
const groups = this._builder.groups;
|
|
|
|
const contextIndex = groups.findIndex(group =>
|
|
group.element === sectionEl
|
|
);
|
|
groups.splice(...[contextIndex, 1].concat(newGroups));
|
|
|
|
for (const newGroup of newGroups) {
|
|
this._builder.emitGroup(newGroup, sectionEl);
|
|
}
|
|
sectionEl.parentNode.removeChild(sectionEl);
|
|
|
|
this.async(() => this.fire('render-content'), 1);
|
|
},
|
|
|
|
cancel() {
|
|
this.$.processor.cancel();
|
|
this.$.syntaxLayer.cancel();
|
|
},
|
|
|
|
_getDiffBuilder(diff, comments, prefs) {
|
|
let builder = null;
|
|
if (this.isImageDiff) {
|
|
builder = new GrDiffBuilderImage(diff, comments, prefs,
|
|
this.projectName, this.diffElement, this.baseImage,
|
|
this.revisionImage);
|
|
} else if (diff.binary) {
|
|
// If the diff is binary, but not an image.
|
|
return new GrDiffBuilderBinary(diff, comments, prefs,
|
|
this.projectName, this.diffElement);
|
|
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
|
|
builder = new GrDiffBuilderSideBySide(diff, comments, prefs,
|
|
this.projectName, this.diffElement, this._layers);
|
|
} else if (this.viewMode === DiffViewMode.UNIFIED) {
|
|
builder = new GrDiffBuilderUnified(diff, comments, prefs,
|
|
this.projectName, this.diffElement, this._layers);
|
|
}
|
|
if (!builder) {
|
|
throw Error('Unsupported diff view mode: ' + this.viewMode);
|
|
}
|
|
if (this.parentIndex) {
|
|
builder.setParentIndex(this.parentIndex);
|
|
}
|
|
return builder;
|
|
},
|
|
|
|
_clearDiffContent() {
|
|
this.diffElement.innerHTML = null;
|
|
},
|
|
|
|
_getCommentLocations(comments) {
|
|
const result = {
|
|
left: {},
|
|
right: {},
|
|
};
|
|
for (const side in comments) {
|
|
if (side !== GrDiffBuilder.Side.LEFT &&
|
|
side !== GrDiffBuilder.Side.RIGHT) {
|
|
continue;
|
|
}
|
|
for (const c of comments[side]) {
|
|
result[side][c.line || GrDiffLine.FILE] = true;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
_groupsChanged(changeRecord) {
|
|
if (!changeRecord) { return; }
|
|
for (const splice of changeRecord.indexSplices) {
|
|
let group;
|
|
for (let i = 0; i < splice.addedCount; i++) {
|
|
group = splice.object[splice.index + i];
|
|
this._builder.groups.push(group);
|
|
this._builder.emitGroup(group);
|
|
}
|
|
}
|
|
},
|
|
|
|
_createIntralineLayer() {
|
|
return {
|
|
// Take a DIV.contentText element and a line object with intraline
|
|
// differences to highlight and apply them to the element as
|
|
// annotations.
|
|
annotate(el, line) {
|
|
const HL_CLASS = 'style-scope gr-diff intraline';
|
|
for (const highlight of line.highlights) {
|
|
// The start and end indices could be the same if a highlight is
|
|
// meant to start at the end of a line and continue onto the
|
|
// next one. Ignore it.
|
|
if (highlight.startIndex === highlight.endIndex) { continue; }
|
|
|
|
// If endIndex isn't present, continue to the end of the line.
|
|
const endIndex = highlight.endIndex === undefined ?
|
|
line.text.length :
|
|
highlight.endIndex;
|
|
|
|
GrAnnotation.annotateElement(
|
|
el,
|
|
highlight.startIndex,
|
|
endIndex - highlight.startIndex,
|
|
HL_CLASS);
|
|
}
|
|
},
|
|
};
|
|
},
|
|
|
|
_createTabIndicatorLayer() {
|
|
const show = () => this._showTabs;
|
|
return {
|
|
annotate(el, line) {
|
|
// If visible tabs are disabled, do nothing.
|
|
if (!show()) { return; }
|
|
|
|
// Find and annotate the locations of tabs.
|
|
const split = line.text.split('\t');
|
|
if (!split) { return; }
|
|
for (let i = 0, pos = 0; i < split.length - 1; i++) {
|
|
// Skip forward by the length of the content
|
|
pos += split[i].length;
|
|
|
|
GrAnnotation.annotateElement(el, pos, 1,
|
|
'style-scope gr-diff tab-indicator');
|
|
|
|
// Skip forward by one tab character.
|
|
pos++;
|
|
}
|
|
},
|
|
};
|
|
},
|
|
|
|
_createTrailingWhitespaceLayer() {
|
|
const show = function() {
|
|
return this._showTrailingWhitespace;
|
|
}.bind(this);
|
|
|
|
return {
|
|
annotate(el, line) {
|
|
if (!show()) { return; }
|
|
|
|
const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
|
|
if (match) {
|
|
// Normalize string positions in case there is unicode before or
|
|
// within the match.
|
|
const index = GrAnnotation.getStringLength(
|
|
line.text.substr(0, match.index));
|
|
const length = GrAnnotation.getStringLength(match[0]);
|
|
GrAnnotation.annotateElement(el, index, length,
|
|
'style-scope gr-diff trailing-whitespace');
|
|
}
|
|
},
|
|
};
|
|
},
|
|
|
|
/**
|
|
* In pages with large diffs, creating the first comment thread can be
|
|
* slow because nested Polymer elements (particularly
|
|
* iron-autogrow-textarea) add style elements to the document head,
|
|
* which, in turn, triggers a reflow on the page. Create a hidden
|
|
* thread, attach it to the page, and remove it so the stylesheet will
|
|
* already exist and the user's comment will be quick to load.
|
|
* @see https://gerrit-review.googlesource.com/c/82213/
|
|
*/
|
|
_preRenderThread() {
|
|
const thread = document.createElement('gr-diff-comment-thread');
|
|
thread.setAttribute('hidden', true);
|
|
thread.addDraft();
|
|
const parent = Polymer.dom(this.root);
|
|
parent.appendChild(thread);
|
|
Polymer.dom.flush();
|
|
parent.removeChild(thread);
|
|
},
|
|
|
|
/**
|
|
* @return {boolean} whether any of the lines in _groups are longer
|
|
* than SYNTAX_MAX_LINE_LENGTH.
|
|
*/
|
|
_anyLineTooLong() {
|
|
return this._groups.reduce((acc, group) => {
|
|
return acc || group.lines.reduce((acc, line) => {
|
|
return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH;
|
|
}, false);
|
|
}, false);
|
|
},
|
|
|
|
setBlame(blame) {
|
|
if (!this._builder || !blame) { return; }
|
|
this._builder.setBlame(blame);
|
|
},
|
|
});
|
|
})();
|
|
</script>
|
|
</dom-module>
|