gerrit/polygerrit-ui/app/elements/gr-diff.html
Andrew Bonventre 8061a65720 Implement selection of patch set in diff view
Feature: Issue 3645
Change-Id: Ic7f408069abce6005392a4a222cc2a1fd4f7573b
2015-12-17 18:54:38 +00:00

408 lines
13 KiB
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.
-->
<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-patch-range-select.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-diff">
<template>
<style>
gr-patch-range-select {
margin: 0 var(--default-horizontal-margin) .75em;
}
.diffContainer {
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-patch-range-select
path="[[path]]"
change-num="[[changeNum]]"
patch-range="[[patchRange]]"
available-patches="[[availablePatches]]"></gr-patch-range-select>
<div class="diffContainer">
<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>
</div>
</template>
<script>
(function() {
'use strict';
Polymer({
is: 'gr-diff',
/**
* Fired when the diff is rendered.
*
* @event render
*/
properties: {
auto: {
type: Boolean,
value: false,
},
availablePatches: Array,
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>