blob: 2e0e0d042efb1bba898e8bd33592a7d702cf469f [file] [log] [blame]
<!--
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.html">
<dom-module id="gr-diff-view">
<template>
<style>
:host {
background-color: var(--view-background-color);
display: block;
margin: 1em 1.25rem;
}
h3 {
border-bottom: 1px solid #eee;
padding: .75em 1em;
}
.mainContainer {
max-width: 100%;
overflow: auto;
}
.diffContainer {
display: flex;
font-family: 'Source Code Pro', monospace;
white-space: pre;
}
.diffNumbers {
background-color: #ddd;
color: #666;
padding: 0 .75em;
text-align: right;
}
.diffContent {
border-right: 1px solid #ddd;
min-width: calc(80ch + 2px);
overflow-x: auto;
padding-left: 2px;
width: calc(80ch + 2px);
}
.diffContainer.leftOnly .diffContent,
.diffContainer.rightOnly .diffContent {
overflow: visible;
}
.diffContainer.leftOnly .right {
display: none;
}
.diffContainer.rightOnly .left {
display: none;
}
.ruler {
display: block;
background-color: #ddd;
height: 1.3em;
position: absolute;
top: 0;
width: 1px;
}
.lineNum:before,
.content: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';
}
.content {
position: relative;
}
.lineNum.blank {
border-right: 2px solid #F34D4D;
margin-right: 3px;
}
.lineNum:not(.blank) {
cursor: pointer;
}
.lineNum:hover {
text-decoration: underline;
}
.lightRed {
background-color: #ffecec;
}
.darkRed {
background-color: #faa;
}
.lightGreen {
background-color: #eaffea;
}
.darkGreen {
background-color: #9f9;
}
</style>
<iron-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
json-prefix=")]}'"
last-response="{{_change}}"
debounce-duration="300"></iron-ajax>
<iron-ajax
id="diffXHR"
url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]"
json-prefix=")]}'"
on-response="_handleDiffResponse"></iron-ajax>
<iron-ajax
id="leftCommentsXHR"
url="[[_computeCommentsPath(_changeNum, _basePatchNum)]]"
json-prefix=")]}'"
on-response="_handleLeftCommentsResponse"></iron-ajax>
<iron-ajax
id="rightCommentsXHR"
url="[[_computeCommentsPath(_changeNum, _patchNum)]]"
json-prefix=")]}'"
on-response="_handleRightCommentsResponse"></iron-ajax>
<h3>
<a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
<span>[[_change.subject]]</span><span>[[params.path]]</span>
</h3>
<div class="mainContainer">
<div class="diffContainer" id="diffContainer">
<div class="diffNumbers left" id="leftDiffNumbers"></div>
<div class="diffContent left" id="leftDiffContent"></div>
<div class="diffNumbers right" id="rightDiffNumbers"></div>
<div class="diffContent right" id="rightDiffContent"></div>
</div>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff-view',
properties: {
/**
* URL params passed from the router.
*/
params: {
type: Object,
observer: '_paramsChanged',
},
rulerWidth: {
type: Number,
value: 80,
observer: '_rulerWidthChanged',
},
_change: Object,
_changeNum: String,
_diff: Object,
_basePatchNum: String,
_patchNum: String,
_path: String,
_leftComments: Array,
_rightComments: Array,
_rendered: Boolean,
},
listeners: {
'diffContainer.tap': '_diffContainerTapHandler',
},
_paramsChanged: function(value) {
this._changeNum = value.changeNum;
this._patchNum = value.patchNum;
this._basePatchNum = value.basePatchNum;
this._path = value.path;
if (!this._patchNum) {
this._change = null;
this._basePatchNum = null;
this._patchNum = null;
this._diff = null;
this._path = null;
this._leftComments = null;
this._rightComments = null;
this._rendered = false;
return;
}
// Assign the params here since a computed binding relying on
// `_basePatchNum` won't fire in the case where it's not defined.
this.$.diffXHR.params = this._diffQueryParams(this._basePatchNum);
this.$.diffXHR.generateRequest();
if (this._basePatchNum) {
this.$.leftCommentsXHR.generateRequest();
}
this.$.rightCommentsXHR.generateRequest();
},
_rulerWidthChanged: function(newValue, oldValue) {
if (newValue < 0) {
throw Error('ruler width must be greater than zero.');
}
if (oldValue == 0) {
this._renderRulerElements();
}
var remove = newValue == 0;
var rulerEls = document.querySelectorAll('.ruler');
for (var i = 0; i < rulerEls.length; i++) {
if (remove) {
rulerEls[i].parentNode.removeChild(rulerEls[i]);
} else {
rulerEls[i].style.left = newValue + 'ch';
}
}
},
_computeChangePath: function(changeNum) {
return '/c/' + changeNum;
},
_computeChangeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_computeChangeDetailQueryParams: function() {
var options = Changes.listChangesOptionsToHex(
Changes.ListChangesOption.ALL_REVISIONS
);
return { O: options };
},
_computeDiffPath: function(changeNum, patchNum, path) {
return '/changes/' + changeNum + '/revisions/' + patchNum + '/files/' +
encodeURIComponent(path) + '/diff';
},
_computeCommentsPath: function(changeNum, patchNum) {
return '/changes/' + changeNum + '/revisions/' + patchNum + '/comments';
},
_diffQueryParams: function(basePatchNum) {
var params = {
context: 'ALL',
intraline: null
};
if (!!basePatchNum) {
params.base = basePatchNum;
}
return params;
},
_diffContainerTapHandler: function(e) {
var el = e.detail.sourceEvent.target;
if (el.classList.contains('lineNum')) {
// TODO: Implement adding draft comments.
}
},
_handleLeftCommentsResponse: function(e, req) {
this._leftComments = e.detail.response[this._path] || [];
this._maybeRenderDiff(this._diff, this._leftComments,
this._rightComments);
},
_handleRightCommentsResponse: function(e, req) {
this._rightComments = e.detail.response[this._path] || [];
this._maybeRenderDiff(this._diff, this._leftComments,
this._rightComments);
},
_handleDiffResponse: function(e, req) {
this._diff = e.detail.response;
this._maybeRenderDiff(this._diff, this._leftComments,
this._rightComments);
},
_threadID: function(patchNum, lineNum) {
return 'thread-' + patchNum + '-' + lineNum;
},
_renderComments: function(comments, patchNum) {
// Group the comments by line number. Absense of a line number indicates
// a top-level file comment.
var threads = {};
for (var i = 0; i < comments.length; i++) {
var line = comments[i].line || 'FILE';
if (threads[line] == null) {
threads[line] = []
}
threads[line].push(comments[i]);
}
for (var lineNum in threads) {
this._addThread(threads[lineNum], patchNum, lineNum);
}
},
_addThread: function(comments, patchNum, lineNum) {
var el = document.createElement('gr-diff-comment-thread');
el.comments = comments;
var threadID = this._threadID(patchNum, lineNum);
el.setAttribute('data-thread-id', threadID);
// Find the element that the thread should be appended after. In the
// case of a file comment, it will be appended after the first line.
// TODO: Show file comment above the file itself.
var fileComment = lineNum == 'FILE';
if (fileComment) {
lineNum = 1;
}
var contentEl = this.$$('.content' +
'[data-patch-num="' + patchNum + '"]' +
'[data-line-num="' + lineNum + '"]');
var rowNum = contentEl.getAttribute('data-row-num');
el.addEventListener('gr-diff-comment-thread-height-changed',
this._handleCommentThreadHeightChange.bind(this, rowNum, threadID));
Polymer.dom(contentEl.parentNode).insertBefore(
el, contentEl.nextSibling);
},
_handleCommentThreadHeightChange: function(rowNum, threadID, e) {
// Adjust the filler element heights if they're present in the DOM.
var els = this.querySelectorAll(
'.js-threadFiller[data-thread-id="' + threadID + '"]');
if (els.length > 0) {
for (var i = 0; i < els.length; i++) {
els[i].style.height = e.detail.height + 'px';
}
return;
}
// Create the filler elements if they're not already present.
var els = this.querySelectorAll('[data-row-num="' + rowNum + '"]');
for (var i = 0; i < els.length; i++) {
// Is this is the side with the comment? Skip if so.
if (els[i].nextSibling &&
els[i].nextSibling.tagName == 'GR-DIFF-COMMENT-THREAD') {
continue;
}
var fillerEl = document.createElement('div');
fillerEl.setAttribute('data-thread-id', threadID);
fillerEl.className = 'js-threadFiller';
fillerEl.style.height = e.detail.height + 'px';
Polymer.dom(els[i].parentNode).insertBefore(
fillerEl, els[i].nextSibling);
}
},
_maybeRenderDiff: function(diff, leftComments, rightComments) {
if (this._rendered) {
throw Error('diff has already been rendered');
}
if (!diff || !diff.content) { return; }
if (this._basePatchNum && leftComments == null) { return; }
if (rightComments == null) { return; }
this.$.diffContainer.classList.toggle('rightOnly',
diff.change_type == Changes.DiffType.ADDED);
this.$.diffContainer.classList.toggle('leftOnly',
diff.change_type == Changes.DiffType.DELETED);
var initialLineNum = 0 + (diff.content.skip || 0);
var ctx = {
rowNum: 0,
left: {
lineNum: initialLineNum,
content: '',
cssClass: '',
},
right: {
lineNum: initialLineNum,
content: '',
cssClass: '',
}
};
for (var i = 0; i < diff.content.length; i++) {
this._addDiffChunk(ctx, diff.content[i]);
}
if (leftComments) {
this._renderComments(leftComments, this._basePatchNum);
}
if (rightComments) {
this._renderComments(rightComments, this._patchNum);
}
if (this.rulerWidth) {
this._renderRulerElements();
}
this._rendered = true;
},
_addDiffChunk: function(ctx, diffChunk) {
// Simplest case where both sides have the same content.
if (diffChunk.ab) {
for (var i = 0; i < diffChunk.ab.length; i++) {
ctx.left.lineNum++;
ctx.right.lineNum++;
ctx.left.content = ctx.right.content = diffChunk.ab[i];
ctx.left.cssClass = ctx.right.cssClass = null;
this._addRow(ctx);
}
return;
}
if (diffChunk.a) {
ctx.left.cssClass = 'lightRed';
} else {
delete(ctx.left.cssClass);
}
if (diffChunk.b) {
ctx.right.cssClass = 'lightGreen';
} else {
delete(ctx.right.cssClass);
}
var aLen = (diffChunk.a && diffChunk.a.length) || 0;
var bLen = (diffChunk.b && diffChunk.b.length) || 0;
var maxLen = Math.max(aLen, bLen);
for (var i = 0; i < maxLen; i++) {
if (diffChunk.a && i < diffChunk.a.length) {
ctx.left.lineNum++;
ctx.left.content = diffChunk.a[i];
} else {
delete(ctx.left.content);
}
if (diffChunk.b && i < diffChunk.b.length) {
ctx.right.lineNum++;
ctx.right.content = diffChunk.b[i];
} else {
delete(ctx.right.content);
}
this._addRow(ctx);
}
},
_addRow: function(ctx) {
var leftLineNumEl = this._createElement('div', 'lineNum');
var leftColEl = this._createElement('div', 'content');
var rightLineNumEl = this._createElement('div', 'lineNum');
var rightColEl = this._createElement('div', 'content');
[leftColEl,
rightColEl,
leftLineNumEl,
rightLineNumEl].forEach(function(el) {
el.setAttribute('data-row-num', ctx.rowNum);
});
var self = this;
if (this._basePatchNum) {
[leftLineNumEl, leftColEl].forEach(function(el) {
el.setAttribute('data-patch-num', self._basePatchNum);
});
}
[rightLineNumEl, rightColEl].forEach(function(el) {
el.setAttribute('data-patch-num', self._patchNum);
});
if (ctx.left.content != null) {
leftLineNumEl.textContent = ctx.left.lineNum;
[leftLineNumEl, leftColEl].forEach(function(el) {
el.setAttribute('data-line-num', ctx.left.lineNum);
});
} else {
leftLineNumEl.classList.add('blank');
}
if (ctx.right.content != null) {
rightLineNumEl.textContent = ctx.right.lineNum;
[rightLineNumEl, rightColEl].forEach(function(el) {
el.setAttribute('data-line-num', ctx.right.lineNum);
});
} else {
rightLineNumEl.classList.add('blank');
}
// Content must be defined to prevent the HTML from showing 'undefined'.
// Additionally, a newline is used place of an empty string to ensure
// copy/paste works correctly.
ctx.left.content = ctx.left.content || '\n';
ctx.right.content = ctx.right.content || '\n';
if (!!ctx.left.cssClass) {
leftColEl.classList.add(ctx.left.cssClass);
}
if (!!ctx.right.cssClass) {
rightColEl.classList.add(ctx.right.cssClass);
}
var leftHTML = util.escapeHTML(ctx.left.content);
var rightHTML = util.escapeHTML(ctx.right.content);
// If the html is equivalent to the text then it didn't get highlighted
// or escaped. Use textContent which is faster than innerHTML.
if (ctx.left.content == leftHTML) {
leftColEl.textContent = ctx.left.content;
} else {
leftColEl.innerHTML = leftHTML;
}
if (ctx.right.content == rightHTML) {
rightColEl.textContent = ctx.right.content;
} else {
rightColEl.innerHTML = rightHTML;
}
this.$.leftDiffNumbers.appendChild(leftLineNumEl);
this.$.leftDiffContent.appendChild(leftColEl);
this.$.rightDiffNumbers.appendChild(rightLineNumEl);
this.$.rightDiffContent.appendChild(rightColEl);
ctx.rowNum++;
},
_renderRulerElements: function() {
var contentEls = this.querySelectorAll('.content');
for (var i = 0; i < contentEls.length; i++) {
var rulerEl = this._createElement('i', 'ruler');
rulerEl.style.left = this.rulerWidth + 'ch';
contentEls[i].appendChild(rulerEl);
}
},
_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.className = 'style-scope gr-diff-view ' + className;
return el;
},
});
})();
</script>
</dom-module>