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:
parent
174a103c18
commit
1aa7b90258
@ -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() {
|
||||
|
@ -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">
|
||||
|
317
polygerrit-ui/app/elements/gr-diff-side.html
Normal file
317
polygerrit-ui/app/elements/gr-diff-side.html
Normal 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
|
||||
// '<span>'. Advancing from index 0 on the prior html string would
|
||||
// return 4, since < 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., <) 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>
|
@ -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, don’t 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();
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
394
polygerrit-ui/app/elements/gr-diff.html
Normal file
394
polygerrit-ui/app/elements/gr-diff.html
Normal 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>
|
144
polygerrit-ui/app/test/gr-diff-side-test.html
Normal file
144
polygerrit-ui/app/test/gr-diff-side-test.html
Normal 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 +
|
||||
' <gr-linked-text content="' +
|
||||
element._highlightEndTag +
|
||||
'[[_computeCurrentRevisionMessage(change)]]' +
|
||||
element._highlightStartTag +
|
||||
'"></gr-linked-text>' +
|
||||
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>
|
268
polygerrit-ui/app/test/gr-diff-test.html
Normal file
268
polygerrit-ui/app/test/gr-diff-test.html
Normal 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 isn’t 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>
|
@ -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 < 5 { // "comments" &= \'fun\' && 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>
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user