Diff view refactor/cleanup

This breaks gr-diff-view into three components:

+ gr-diff-view: manages keyboard shortcuts and fetching
  change information.
+ gr-diff: fetches diff, comment, and draft data. Normalizes
  it for use in rendering via gr-diff-side.
+ gr-diff-side: renders the normalized model constructed in
  gr-diff.

Comments are not implemented using the new model for the
sake of the reviewer's sanity.

Feature: Issue 3648
Feature: Issue 3663

Change-Id: I60b8a61ef4349d0b7e45b105bb704aa1c07cd358
This commit is contained in:
Andrew Bonventre 2015-12-11 12:02:57 -05:00
parent 174a103c18
commit 1aa7b90258
9 changed files with 1182 additions and 869 deletions

View File

@ -161,7 +161,7 @@ limitations under the License.
},
get loggedIn() {
return this.account && Object.keys(this.account).length > 0;
return !!(this.account && Object.keys(this.account).length > 0);
},
_accountChanged: function() {

View File

@ -14,7 +14,6 @@ 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="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="gr-date-formatter.html">

View File

@ -0,0 +1,317 @@
<!--
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">
<dom-module id="gr-diff-side">
<template>
<style>
:host,
.container {
display: flex;
}
.content {
width: 80ch;
}
.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;
}
.canComment .lineNum:hover {
background-color: #ccc;
}
.code {
white-space: pre;
}
.lightHighlight {
background-color: var(--light-highlight-color);
}
hl,
.darkHighlight {
background-color: var(--dark-highlight-color);
}
.br:after {
/* Line feed */
content: '\A';
}
.filler {
background: #eee;
}
</style>
<div class$="[[_computeContainerClass(canComment)]]">
<div class="numbers" id="numbers"></div>
<div class="content" id="content"></div>
</div>
</template>
<script>
(function() {
'use strict';
var CharCode = {
LESS_THAN: '<'.charCodeAt(0),
GREATER_THAN: '>'.charCodeAt(0),
AMPERSAND: '&'.charCodeAt(0),
SEMICOLON: ';'.charCodeAt(0),
};
Polymer({
is: 'gr-diff-side',
properties: {
canComment: {
type: Boolean,
value: false,
},
content: {
type: Array,
notify: true,
observer: '_render',
},
width: {
type: Number,
observer: '_widthChanged',
},
_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,
},
},
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);
},
_widthChanged: function(width) {
this.$.content.style.width = width + 'ch';
},
_computeContainerClass: function(canComment) {
return 'container' + (canComment ? ' canComment' : '');
},
_clearChildren: function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
},
_render: function(diff) {
this._clearChildren(this.$.numbers);
this._clearChildren(this.$.content);
for (var i = 0; i < diff.length; i++) {
switch (diff[i].type) {
case 'CODE':
this._renderCode(diff[i]);
break;
case 'FILLER':
this._renderFiller(diff[i]);
break;
}
}
},
_renderFiller: function(filler) {
var lineFillerEl = this._createElement('div', 'filler');
var fillerEl = this._createElement('div', 'filler');
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.appendChild(lineFillerEl);
this.$.content.appendChild(fillerEl);
},
_renderCode: function(code) {
var lineNumEl = this._createElement('div', 'lineNum');
lineNumEl.setAttribute('data-line-num', code.lineNum);
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);
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);
}
// 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.appendChild(lineNumEl);
this.$.content.appendChild(contentEl);
},
// 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 = [];
for (var i = 0; i < content.length; i++) {
if (i > 0 && i % this.width == 0) {
indices.push(htmlIndex);
}
htmlIndex = this._advanceChar(html, htmlIndex)
}
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;
},
_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', className);
return el;
},
});
})();
</script>
</dom-module>

View File

@ -17,7 +17,7 @@ limitations under the License.
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-diff-comment-thread.html">
<link rel="import" href="gr-diff.html">
<dom-module id="gr-diff-view">
<template>
@ -36,116 +36,27 @@ limitations under the License.
border-top: 1px solid #eee;
width: 100%;
}
.diffNumbers,
.diffContent {
vertical-align: top;
}
.diffContainer {
font-family: 'Source Code Pro', monospace;
white-space: pre;
}
.diffNumbers {
background-color: #eee;
color: #666;
padding: 0 .75em;
text-align: right;
}
.diffContent {
min-width: 80ch;
max-width: 120ch;
overflow: hidden;
}
.diffContainer.leftOnly .diffContent,
.diffContainer.rightOnly .diffContent {
overflow: visible;
}
.diffContainer.leftOnly .right,
.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,
.threadFiller--redLine {
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>
<gr-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
last-response="{{_change}}"></gr-ajax>
<gr-ajax id="diffXHR"
url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]"
on-response="_handleDiffResponse"></gr-ajax>
<gr-ajax id="leftCommentsXHR"
url="[[_computeCommentsPath(_changeNum, _basePatchNum)]]"
json-prefix=")]}'"
on-response="_handleLeftCommentsResponse"></gr-ajax>
<gr-ajax id="rightCommentsXHR"
url="[[_computeCommentsPath(_changeNum, _patchNum)]]"
on-response="_handleRightCommentsResponse"></gr-ajax>
<!-- TODO(andybons): This is populated in gr-change-view. Use that instead
of incurring an extra ajax call. -->
<gr-ajax id="filesXHR"
url="[[_computeFilesPath(_changeNum, _patchNum)]]"
auto
url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
on-response="_handleFilesResponse"></gr-ajax>
<gr-ajax id="leftDraftsXHR"
url="[[_computeDraftsPath(_changeNum, _basePatchNum)]]"
on-response="_handleLeftDraftsResponse"></gr-ajax>
<gr-ajax id="rightDraftsXHR"
url="[[_computeDraftsPath(_changeNum, _patchNum)]]"
on-response="_handleRightDraftsResponse"></gr-ajax>
<h3>
<a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
<span>[[_change.subject]]</span><span>[[params.path]]</span>
</h3>
<table class="mainContainer">
<tr class="diffContainer" id="diffContainer">
<td class="diffNumbers left" id="leftDiffNumbers"></td>
<td class="diffContent left" id="leftDiffContent"></td>
<td class="diffNumbers right" id="rightDiffNumbers"></td>
<td class="diffContent right" id="rightDiffContent"></td>
</tr>
</table>
<gr-diff id="diff"
auto
change-num="[[_changeNum]]"
patch-range="[[_patchRange]]"
path="[[_path]]"
on-render="_handleDiffRender">
</gr-diff>
</template>
<script>
(function() {
@ -172,12 +83,7 @@ limitations under the License.
type: Object,
observer: '_paramsChanged',
},
rulerWidth: {
type: Number,
value: 80,
observer: '_rulerWidthChanged',
},
_basePatchNum: String,
_patchRange: Object,
_change: Object,
_changeNum: String,
_diff: Object,
@ -185,222 +91,13 @@ limitations under the License.
type: Array,
value: function() { return []; },
},
_leftComments: {
type: Array,
value: function() { return []; },
},
_leftDrafts: {
type: Array,
value: function() { return []; },
},
_patchNum: String,
_path: String,
_rendered: Boolean,
_rightComments: {
type: Array,
value: function() { return []; },
},
_rightDrafts: {
type: Array,
value: function() { return []; },
},
},
listeners: {
'diffContainer.tap': '_diffContainerTapHandler',
},
keyBindings: {
'[ ] u': '_handleKey',
},
_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 = [];
this._rightComments = [];
this._leftDrafts = [];
this._rightDrafts = [];
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);
var requestPromises = [];
requestPromises.push(this.$.diffXHR.generateRequest().completes);
if (this._basePatchNum) {
requestPromises.push(
this.$.leftCommentsXHR.generateRequest().completes);
}
requestPromises.push(
this.$.rightCommentsXHR.generateRequest().completes);
requestPromises.push(this.$.filesXHR.generateRequest().completes);
app.accountReady.then(function() {
if (app.loggedIn) {
if (this._basePatchNum) {
requestPromises.push(
this.$.leftDraftsXHR.generateRequest().completes);
}
requestPromises.push(
this.$.rightDraftsXHR.generateRequest().completes);
}
Promise.all(requestPromises).then(function(requests) {
this._renderDiff(this._diff, this._leftComments,
this._rightComments, this._leftDrafts, this._rightDrafts);
}.bind(this), function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
_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 = Polymer.dom(this.root).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';
},
_computeFilesPath: function(changeNum, patchNum) {
return '/changes/' + changeNum + '/revisions/' + patchNum + '/files';
},
_computeDraftsPath: function(changeNum, patchNum) {
return '/changes/' + changeNum + '/revisions/' + patchNum + '/drafts';
},
_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;
// This tap handler only handles line number taps.
if (!el.classList.contains('lineNum')) { return; }
var leftSide = el.parentNode == this.$.leftDiffNumbers;
var rightSide = el.parentNode == this.$.rightDiffNumbers;
if (leftSide == rightSide) {
throw Error('Comment tap event cannot originate from both left and ' +
'right side');
}
// If a draft or comment is already present at that line, dont do
// anything.
var lineNum = el.getAttribute('data-line-num');
var patchNum = el.getAttribute('data-patch-num');
var existingEl = this.$$('gr-diff-comment-thread' +
'[data-patch-num="' + patchNum + '"]' +
'[data-line-num="' + lineNum + '"]');
if (existingEl) {
// A comment or draft is already present at this line.
return;
}
var tempDraftID = Math.floor(Math.random() * Math.pow(10, 10)) + '';
var drafts = [{
__draft: true,
__draftID: tempDraftID,
path: this._path,
line: lineNum,
}];
// If the comment is on the left side of a side-by-side diff with the
// parent on the left and a patch with patchNum on the right, the patch
// number passed to the backend is the right side patchNum when mutating
// a draft. The property `side` is used to determine that it should be
// on the parent patch, which is inconsistent and why this looks weird.
var patchNum = this._patchNum;
if (leftSide && this._basePatchNum == null) {
drafts[0].side = 'PARENT';
patchNum = 'PARENT';
}
this._addThread(drafts, patchNum, lineNum);
},
_handleLeftCommentsResponse: function(e, req) {
this._leftComments = e.detail.response[this._path] || [];
},
_handleRightCommentsResponse: function(e, req) {
this._rightComments = e.detail.response[this._path] || [];
},
_handleLeftDraftsResponse: function(e, req) {
this._leftDrafts = e.detail.response[this._path] || [];
},
_handleRightDraftsResponse: function(e, req) {
this._rightDrafts = e.detail.response[this._path] || [];
},
_handleFilesResponse: function(e, req) {
this._fileList = Object.keys(e.detail.response).sort();
},
_handleDiffResponse: function(e, req) {
this._diff = e.detail.response;
},
_handleKey: function(e) {
if (util.shouldSupressKeyboardShortcut(e)) { return; }
@ -419,6 +116,13 @@ limitations under the License.
}
},
_handleDiffRender: function() {
if (window.location.hash.length > 0) {
this.$.diff.scrollToLine(
parseInt(window.location.hash.substring(1), 10));
}
},
_navToFile: function(fileList, direction) {
if (fileList.length == 0) { return; }
@ -427,328 +131,51 @@ limitations under the License.
page.show(this._computeChangePath(this._changeNum));
return;
}
page.show(
this._diffURL(this._changeNum, this._patchNum, fileList[idx]));
page.show(this._diffURL(this._changeNum,
this._patchRange.patchNum,
fileList[idx]));
},
_diffURL: function(changeNum, patchNum, path) {
return '/c/' + changeNum + '/' + patchNum + '/' + path;
},
_threadID: function(patchNum, lineNum) {
return 'thread-' + patchNum + '-' + lineNum;
},
_renderCommentsAndDrafts: function(comments, drafts, patchNum) {
// Drafts and comments are combined here, with drafts annotated with a
// property.
var annotatedDrafts = drafts.map(function(d) {
d.__draft = true;
return d;
});
comments = comments.concat(annotatedDrafts);
// Group the comments and drafts by line number. Absence of a line
// number indicates a top-level file comment or draft.
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;
el.changeNum = this._changeNum;
// Assign the element's patchNum to the right side patchNum if the
// passed patchNum is 'PARENT' due to the odd behavior of the REST API.
// Don't overwrite patchNum since 'PARENT' is used for other properties.
el.patchNum = patchNum == 'PARENT' ? this._patchNum : patchNum;
var threadID = this._threadID(patchNum, lineNum);
el.setAttribute('data-thread-id', threadID);
el.setAttribute('data-line-num', lineNum);
el.setAttribute('data-patch-num', patchNum);
// 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 = Polymer.dom(this.root).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 = Polymer.dom(this.root).querySelectorAll(
'[data-row-num="' + rowNum + '"]');
for (var i = 0; i < els.length; i++) {
// Is this is the column 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.classList.add('js-threadFiller');
if (els[i].classList.contains('lineNum')) {
fillerEl.classList.add('threadFiller--redLine');
}
fillerEl.style.height = e.detail.height + 'px';
Polymer.dom(els[i].parentNode).insertBefore(
fillerEl, els[i].nextSibling);
}
},
_clearChildren: function(el) {
while (el.firstChild) {
el.removeChild(el.firstChild);
}
},
_renderDiff: function(
diff, leftComments, rightComments, leftDrafts, rightDrafts) {
if (this._rendered) {
this._clearChildren(this.$.leftDiffNumbers);
this._clearChildren(this.$.leftDiffContent);
this._clearChildren(this.$.rightDiffNumbers);
this._clearChildren(this.$.rightDiffContent);
}
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: '',
}
_paramsChanged: function(value) {
this._changeNum = value.changeNum;
this._patchRange = {
patchNum: value.patchNum,
basePatchNum: value.basePatchNum || 'PARENT',
};
for (var i = 0; i < diff.content.length; i++) {
this._addDiffChunk(ctx, diff.content[i]);
}
this._path = value.path;
if (leftComments) {
this._renderCommentsAndDrafts(leftComments, leftDrafts,
this._basePatchNum);
}
if (rightComments) {
this._renderCommentsAndDrafts(rightComments, rightDrafts,
this._patchNum);
}
if (this.rulerWidth) {
this._renderRulerElements();
}
if (window.location.hash.length > 0) {
// Allow for the initial rendering to complete before scrolling to the
// appropriate line.
this.async(function() {
this._jumpToLine(parseInt(window.location.hash.substring(1), 10));
}.bind(this), 1);
}
this._rendered = true;
},
_jumpToLine: function(lineNum) {
if (isNaN(lineNum) || lineNum < 1) { return; }
var el = this.$$(
'.diffNumbers.right .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);
},
_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);
}
// 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) {
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);
});
[leftLineNumEl, leftColEl].forEach(function(el) {
el.setAttribute('data-patch-num', this._basePatchNum || 'PARENT');
}.bind(this));
[rightLineNumEl, rightColEl].forEach(function(el) {
el.setAttribute('data-patch-num', this._patchNum);
}.bind(this));
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++;
_computeChangePath: function(changeNum) {
return '/c/' + changeNum;
},
_renderRulerElements: function() {
var contentEls = Polymer.dom(this.root).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);
}
_computeChangeDetailPath: function(changeNum) {
return '/changes/' + changeNum + '/detail';
},
_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;
_computeChangeDetailQueryParams: function() {
return { O: Changes.listChangesOptionsToHex(
Changes.ListChangesOption.ALL_REVISIONS
)};
},
_computeFilesPath: function(changeNum, patchNum) {
return Changes.baseURL(changeNum, patchNum) + '/files';
},
_handleFilesResponse: function(e, req) {
this._fileList = Object.keys(e.detail.response).sort();
},
});
})();

View File

@ -0,0 +1,394 @@
<!--
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-ajax.html">
<link rel="import" href="gr-diff-side.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-diff">
<template>
<style>
:host {
border-bottom: 1px solid #eee;
border-top: 1px solid #eee;
font-family: 'Source Code Pro', monospace;
display: flex;
white-space: pre;
}
gr-diff-side:first-of-type {
--light-highlight-color: #ffecec;
--dark-highlight-color: #faa;
}
gr-diff-side:last-of-type {
--light-highlight-color: #eaffea;
--dark-highlight-color: #9f9;
border-right: 1px solid #ddd;
}
</style>
<gr-ajax id="diffXHR"
url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
last-response="{{_diffResponse}}"></gr-ajax>
<gr-ajax id="baseCommentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="commentsXHR"
url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<gr-ajax id="baseDraftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
<gr-ajax id="draftsXHR"
url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
<gr-diff-side id="leftDiff"
content="{{_diff.leftSide}}"
width="[[sideWidth]]"
can-comment="[[_loggedIn]]"></gr-diff-side>
<gr-diff-side id="rightDiff"
content="{{_diff.rightSide}}"
width="[[sideWidth]]"
can-comment="[[_loggedIn]]"></gr-diff-side>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
auto: {
type: Boolean,
value: false,
},
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,
sideWidth: {
type: Number,
value: 80,
},
_comments: Array,
_baseComments: Array,
_drafts: Array,
_baseDrafts: Array,
_diffResponse: Object,
_diff: {
type: Object,
value: function() { return {}; },
},
_loggedIn: {
type: Boolean,
value: false,
},
_diffRequestsPromise: Object, // Used for testing.
},
observers: [
'_diffOptionsChanged(changeNum, patchRange, path)'
],
ready: function() {
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
},
scrollToLine: function(lineNum) {
// TODO(andybons): Should this always be the right side?
this.$.rightDiff.scrollToLine(lineNum);
},
_diffOptionsChanged: function(changeNum, patchRange, path) {
if (!this.auto) { return; }
var promises = [this.$.diffXHR.generateRequest().completes];
var basePatchNum = patchRange.basePatchNum;
var patchNum = patchRange.patchNum;
app.accountReady.then(function() {
promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
this._diffRequestsPromise = Promise.all(promises).then(function() {
this._allDataReceived();
}.bind(this)).catch(function(err) {
alert('Oops. Something went wrong. Check the console and bug the ' +
'PolyGerrit team for assistance.');
throw err;
});
}.bind(this));
},
_allDataReceived: function() {
this._processContent();
// Allow for the initial rendering to complete before firing the event.
this.async(function() {
this.fire('render', {bubbles: false});
}.bind(this), 1);
},
_getCommentsAndDrafts: function(basePatchNum, loggedIn) {
var promises = [];
function onlyParent(c) { return c.side == 'PARENT'; }
function withoutParent(c) { return c.side != 'PARENT'; }
var promises = [];
var commentsPromise = this.$.commentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
var comments = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseComments = comments.filter(onlyParent);
}
this._comments = comments.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
promises.push(commentsPromise.then(function(req) {
this._baseComments =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
if (!loggedIn) {
this._baseDrafts = [];
this._drafts = [];
return Promise.all(promises);
}
var draftsPromise = this.$.draftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
var drafts = req.response[this.path] || [];
if (basePatchNum == 'PARENT') {
this._baseDrafts = drafts.filter(onlyParent);
}
this._drafts = drafts.filter(withoutParent);
}.bind(this)));
if (basePatchNum != 'PARENT') {
draftsPromise = this.$baseDraftsXHR.generateRequest().completes;
promises.push(draftsPromise.then(function(req) {
this._baseDrafts =
(req.response[this.path] || []).filter(withoutParent);
}.bind(this)));
}
return Promise.all(promises);
},
_computeDiffPath: function(changeNum, patchNum, path) {
return Changes.baseURL(changeNum, patchNum) + '/files/' +
encodeURIComponent(path) + '/diff';
},
_computeCommentsPath: function(changeNum, patchNum) {
return Changes.baseURL(changeNum, patchNum) + '/comments';
},
_computeDraftsPath: function(changeNum, patchNum) {
return Changes.baseURL(changeNum, patchNum) + '/drafts';
},
_computeDiffQueryParams: function(basePatchNum) {
var params = {
context: 'ALL',
intraline: null
};
if (basePatchNum != 'PARENT') {
params.base = basePatchNum;
}
return params;
},
_processContent: function() {
var leftSide = [];
var rightSide = [];
var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
var ctx = {
left: {
lineNum: initialLineNum,
},
right: {
lineNum: initialLineNum,
}
};
for (var i = 0; i < this._diffResponse.content.length; i++) {
this._addDiffChunk(ctx, this._diffResponse.content[i], leftSide,
rightSide);
}
this._diff = {
leftSide: leftSide,
rightSide: rightSide,
};
},
_addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
if (chunk.ab) {
for (var i = 0; i < chunk.ab.length; i++) {
var numLines = Math.ceil(chunk.ab[i].length / this.sideWidth);
// Blank lines within a diff content array indicate a newline.
leftSide.push({
type: 'CODE',
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.left.lineNum,
});
rightSide.push({
type: 'CODE',
content: chunk.ab[i] || '\n',
numLines: numLines,
lineNum: ++ctx.right.lineNum,
});
}
}
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 maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
if (hasLeftContent) {
leftSide.push({
type: 'CODE',
content: leftContent || '\n',
numLines: maxNumLines,
lineNum: ++ctx.left.lineNum,
highlight: true,
intraline: 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: true,
intraline: rightHighlights.filter(function(hl) {
return hl.contentIndex == i;
}),
});
} else {
rightSide.push({
type: 'FILLER',
numLines: maxNumLines,
});
}
}
},
// 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;
},
_maxLinesSpanned: function(left, right) {
return Math.max(Math.ceil(left.length / this.sideWidth),
Math.ceil(right.length / this.sideWidth));
},
});
})();
</script>
</dom-module>

View File

@ -0,0 +1,144 @@
<!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="../elements/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._render(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.width = 80;
var content = [{
type: 'CODE',
content: 'A'.repeat(50),
numLines: 1,
lineNum: 1,
highlight: false,
intraline: [],
}];
element._render(content);
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));
content = [{
type: 'CODE',
content: 'A'.repeat(100),
numLines: 2,
lineNum: 1,
highlight: false,
intraline: [],
}];
element._render(content);
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);
});
});
</script>

View File

@ -0,0 +1,268 @@
<!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</title>
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/changes.js"></script>
<script src="../scripts/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="../elements/gr-diff.html">
<test-fixture id="basic">
<template>
<gr-diff auto></gr-diff>
</template>
</test-fixture>
<script>
suite('gr-diff tests', function() {
var element;
var server;
setup(function() {
element = fixture('basic');
element.changeNum = 42;
element.path = 'sieve.go';
server = sinon.fakeServer.create();
server.respondWith(
'GET',
/\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/,
[
200,
{ 'Content-Type': 'application/json' },
')]}\'\n' +
JSON.stringify({
change_type: 'MODIFIED',
content: [
{ab: ['doin some codez and stuffs']},
]
}),
]
);
server.respondWith(
'GET',
'/changes/42/revisions/1/comments',
[
200,
{ 'Content-Type': 'application/json' },
')]}\'\n' +
JSON.stringify({
'/COMMIT_MSG': [],
'sieve.go': [
{
author: {
_account_id: 1000000,
name: 'Andrew Bonventre',
email: 'andybons@gmail.com',
},
id: '9af53d3f_5f2b8b82',
line: 1,
message: 'this isnt quite right',
updated: '2015-12-10 02:50:21.627000000',
},
{
author: {
_account_id: 1000000,
name: 'Andrew Bonventre',
email: 'andybons@gmail.com',
},
id: '9af53d3f_bf1cd76b',
line: 1,
side: 'PARENT',
message: 'how did this work in the first place?',
updated: '2015-12-10 00:08:42.255000000',
},
],
}),
]
);
server.respondWith(
'GET',
'/changes/42/revisions/2/comments',
[
200,
{ 'Content-Type': 'application/json' },
')]}\'\n' +
JSON.stringify({
'/COMMIT_MSG': [],
'sieve.go': [
{
author: {
_account_id: 1010008,
name: 'Dave Borowitz',
email: 'dborowitz@google.com',
},
id: '001a2067_f30f3048',
line: 17,
message: 'What on earth are you thinking, here?',
updated: '2015-12-12 02:51:37.973000000',
},
{
author: {
_account_id: 1010008,
name: 'Dave Borowitz',
email: 'dborowitz@google.com',
},
id: '001a2067_f6b1b1c8',
in_reply_to: '9af53d3f_bf1cd76b',
line: 1,
side: 'PARENT',
message: 'Yeah not sure how this worked either?',
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: 17,
message: '¯\\_(ツ)_/¯',
updated: '2015-12-12 18:50:21.627000000',
},
],
}),
]
);
});
teardown(function() {
server.restore();
});
test('comments with parent', function(done) {
element.patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
server.respond();
element._diffRequestsPromise.then(function() {
assert.equal(element._baseComments.length, 1);
assert.equal(element._comments.length, 1);
assert.equal(element._baseDrafts.length, 0);
assert.equal(element._drafts.length, 0);
done();
});
});
test('comments between two patches', function(done) {
element.patchRange = {
basePatchNum: 1,
patchNum: 2,
};
server.respond();
element._diffRequestsPromise.then(function() {
assert.equal(element._baseComments.length, 1);
assert.equal(element._comments.length, 2);
assert.equal(element._baseDrafts.length, 0);
assert.equal(element._drafts.length, 0);
done();
});
});
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,
}
]);
});
});
</script>

View File

@ -22,273 +22,34 @@ limitations under the License.
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/page/page.js"></script>
<script src="../scripts/changes.js"></script>
<script src="../scripts/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="../elements/gr-diff-view.html">
<test-fixture id="basic">
<template>
<gr-diff-view ruler-width="0"></gr-diff-view>
</template>
</test-fixture>
<test-fixture id="comments">
<template>
<gr-diff-view></gr-diff-view>
</template>
</test-fixture>
<test-fixture id="manylines">
<template>
<gr-diff-view></gr-diff-view>
</template>
</test-fixture>
<script>
// Original diff:
// Left side (side A):
// 1: if i < 5 { // "comments" &= \'fun\' && true
// 2: println("i is less than five")
// 3: }
// 4:
// 5:
// 6: // Comment
// 7: foo
// 8: bar
// 9: Bad news: combustible lemons failed.
//
// Right side (side B):
// 1: if i < 5 { // "comments" &= \'fun\' && true
// 2: println("i is less than five")
// 3: }
// 4:
// 5:
// 6: // Comment
// 7: baz
// 8: Bad news: combustible lemons failed.
//
var diffContent = [
{
ab: [
'if i < 5 { // "comments" &= \'fun\' && true',
' println("i is less than five")',
'}',
'',
'',
'// Comment'
]
},
{
a: [
'foo',
'bar'
],
b: [
'baz',
]
},
{
ab: [
'Bad news: combustible lemons failed.'
]
}
];
suite('<gr-diff-view>', function() {
suite('gr-diff-view tests', function() {
var element;
setup(function() {
element = fixture('basic');
element._renderDiff({content: diffContent}, [], [], [], []);
flushAsynchronousOperations();
});
test('ab content is the same for left and right sides', function() {
for (var i = 0; i < diffContent[0].ab.length; i++) {
var leftEls = Polymer.dom(element.root).querySelectorAll(
'#leftDiffContent .content[data-line-num="' + (i + 1) + '"]');
assert.equal(leftEls.length, 1);
var rightEls = Polymer.dom(element.root).querySelectorAll(
'#rightDiffContent .content[data-line-num="' + (i + 1) + '"]');
assert.equal(rightEls.length, 1);
assert.equal(leftEls[0].textContent, rightEls[0].textContent);
}
});
test('all line number and content elements have same (non-zero) height',
function() {
var els = Polymer.dom(element.root).querySelectorAll('.lineNum, .content');
assert.isAbove(els.length, 0);
var offsetHeight = els.length > 0 && els[0].offsetHeight;
assert.isAbove(offsetHeight, 0);
for (var i = 0; i < els.length; i++) {
assert.equal(offsetHeight, els[i].offsetHeight);
}
});
test('content is properly escaped', function() {
var firstLineEls = Polymer.dom(element.root).querySelectorAll(
'#leftDiffContent .content[data-line-num="1"], ' +
'#rightDiffContent .content[data-line-num="1"]');
assert.equal(2, firstLineEls.length);
for (var i = 0; i < firstLineEls.length; i++) {
assert.equal(firstLineEls[i].innerHTML,
'if i &lt; 5 { // "comments" &amp;= \'fun\' &amp;&amp; true');
}
});
test('content and line numbers are correct for a/b edit', function() {
assert.equal(element.$$(
'#leftDiffContent .content[data-line-num="7"]').textContent, 'foo');
assert.equal(element.$$(
'#leftDiffContent .content[data-line-num="8"]').textContent, 'bar');
assert.equal(element.$$(
'#rightDiffContent .content[data-line-num="7"]').textContent, 'baz');
assert.equal(element.$$(
'#leftDiffContent .content[data-line-num="9"]').textContent,
element.$$(
'#rightDiffContent .content[data-line-num="8"]').textContent);
});
test('ruler width changes are applied correctly', function() {
assert.equal(element.rulerWidth, 0);
assert.equal(Polymer.dom(element.root).querySelectorAll('.ruler').length,
0);
element.rulerWidth = 80;
flushAsynchronousOperations();
var els = Polymer.dom(element.root).querySelectorAll('.ruler');
assert.isAbove(els.length, 0);
for (var i = 0; i < els.length; i++) {
assert.equal(els[i].style.left, '80ch');
}
element.rulerWidth = 0;
flushAsynchronousOperations();
assert.equal(Polymer.dom(element.root).querySelectorAll('.ruler').length,
0);
element.rulerWidth = 100;
flushAsynchronousOperations();
els = Polymer.dom(element.root).querySelectorAll('.ruler');
assert.isAbove(els.length, 0);
for (var i = 0; i < els.length; i++) {
assert.equal(els[i].style.left, '100ch');
}
});
});
suite('comments and drafts', function() {
var element;
setup(function(done) {
element = fixture('comments');
element._patchNum = 1;
element._renderDiff({content: diffContent}, [], [
{
id: 'file_comment',
message: 'this is a file comment about the meaninglessness of life',
author: {
name: 'GLaDOS'
}
},
{
id: 'all_the_lemons',
line: 8,
message: 'MAKE LIFE TAKE THE LEMONS BACK',
author: {
name: 'Cave Johnson',
}
}
], [], []);
// On WebKit and Gecko, flushAsynchronousOperations isn't enough to allow
// the thread filler elements to properly render. Spin the runloop.
element.async(function() {
done();
}, 1);
});
test('comment threads are rendered correctly', function() {
var threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-thread-id="thread-1-8"]');
assert.equal(threadEls.length, 1);
var fillerEls = Polymer.dom(element.root).querySelectorAll(
'.js-threadFiller[data-thread-id="thread-1-8"]');
assert.equal(fillerEls.length, 3);
threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-thread-id="thread-1-FILE"]');
assert.equal(threadEls.length, 1);
fillerEls = Polymer.dom(element.root).querySelectorAll(
'.js-threadFiller[data-thread-id="thread-1-FILE"]');
assert.equal(fillerEls.length, 3);
});
test('tapping a line with an existing thread', function() {
var threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-line-num="8"][data-patch-num="1"]');
assert.equal(threadEls.length, 1);
var lineEl = element.$$(
'.lineNum[data-line-num="8"][data-patch-num="1"]');
assert.ok(lineEl);
MockInteractions.tap(lineEl);
threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-line-num="8"][data-patch-num="1"]');
assert.equal(threadEls.length, 1);
});
test('creating a draft', function() {
var threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-line-num="5"][data-patch-num="1"]');
assert.equal(threadEls.length, 0);
var lineEl = element.$$(
'.lineNum[data-line-num="5"][data-patch-num="1"]');
assert.ok(lineEl);
MockInteractions.tap(lineEl);
threadEls = Polymer.dom(element.root).querySelectorAll(
'gr-diff-comment-thread[data-line-num="5"][data-patch-num="1"]');
assert.equal(threadEls.length, 1);
});
});
suite('long diffs', function() {
var element;
setup(function() {
element = fixture('manylines');
var longDiffContent = [{ ab: [] }];
for (var i = 0; i < 300; i++) {
longDiffContent[0].ab.push('');
}
element._renderDiff({content: longDiffContent}, [], [], [], []);
});
function isVisibleInWindow(el) {
var rect = el.getBoundingClientRect();
return rect.top >= 0 && rect.left >= 0 &&
rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
}
test('jump to line', function() {
window.scrollTo(0, 0);
element._jumpToLine(-12849);
assert.equal(window.scrollY, 0);
element._jumpToLine('sup');
assert.equal(window.scrollY, 0);
// Use the left line numbers in this case because the viewport may be too
// thin for the right line number element to be visible. Since the content
// is the same for both sides, it should not make a difference.
var lineEl =
element.$$('.diffNumbers.left .lineNum[data-line-num="150"]');
assert.isFalse(isVisibleInWindow(lineEl),
'element should not be visible');
element._jumpToLine(150);
assert.isAbove(window.scrollY, 0);
assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
element.$.changeDetailXHR.auto = false;
element.$.filesXHR.auto = false;
element.$.diff.auto = false;
});
test('keyboard shortcuts', function() {
element._changeNum = '42';
element._patchNum = '10';
element._patchRange = {
patchNum: '10',
};
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
element._path = 'glados.txt';
@ -349,5 +110,6 @@ limitations under the License.
}, 1);
}
});
});
</script>

View File

@ -30,6 +30,8 @@ limitations under the License.
'gr-date-formatter-test.html',
'gr-diff-comment-test.html',
'gr-diff-comment-thread-test.html',
'gr-diff-side-test.html',
'gr-diff-test.html',
'gr-diff-view-test.html',
'gr-reply-dropdown-test.html',
'gr-search-bar-test.html',