blob: 0d787c19d567d91dd38f9d100d74345ffb6f0dcf [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function() {
'use strict';
var WHOLE_FILE = -1;
var DiffSide = {
LEFT: 'left',
RIGHT: 'right',
};
var DiffGroupType = {
ADDED: 'b',
BOTH: 'ab',
REMOVED: 'a',
};
var DiffHighlights = {
ADDED: 'edit_b',
REMOVED: 'edit_a',
};
Polymer({
is: 'gr-diff-processor',
properties: {
/**
* The amount of context around collapsed groups.
*/
context: Number,
/**
* The array of groups output by the processor.
*/
groups: {
type: Array,
notify: true,
},
/**
* Locations that should not be collapsed, including the locations of
* comments.
*/
keyLocations: {
type: Object,
value: function() { return {left: {}, right: {}}; },
},
_content: Object,
},
process: function(content) {
return new Promise(function(resolve) {
var groups = [];
this._processContent(content, groups, this.context);
this.groups = groups;
resolve(groups);
}.bind(this));
},
_processContent: function(content, groups, context) {
this._appendFileComments(groups);
context = content.length > 1 ? context : WHOLE_FILE;
var lineNums = {
left: 0,
right: 0,
};
content = this._splitCommonGroupsWithComments(content, lineNums);
for (var i = 0; i < content.length; i++) {
var group = content[i];
var lines = [];
if (group[DiffGroupType.BOTH] !== undefined) {
var rows = group[DiffGroupType.BOTH];
this._appendCommonLines(rows, lines, lineNums);
var hiddenRange = [context, rows.length - context];
if (i === 0) {
hiddenRange[0] = 0;
} else if (i === content.length - 1) {
hiddenRange[1] = rows.length;
}
if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
this._insertContextGroups(groups, lines, hiddenRange);
} else {
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
}
continue;
}
if (group[DiffGroupType.REMOVED] !== undefined) {
var highlights = undefined;
if (group[DiffHighlights.REMOVED] !== undefined) {
highlights = this._normalizeIntralineHighlights(
group[DiffGroupType.REMOVED],
group[DiffHighlights.REMOVED]);
}
this._appendRemovedLines(group[DiffGroupType.REMOVED], lines,
lineNums, highlights);
}
if (group[DiffGroupType.ADDED] !== undefined) {
var highlights = undefined;
if (group[DiffHighlights.ADDED] !== undefined) {
highlights = this._normalizeIntralineHighlights(
group[DiffGroupType.ADDED],
group[DiffHighlights.ADDED]);
}
this._appendAddedLines(group[DiffGroupType.ADDED], lines,
lineNums, highlights);
}
groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
}
},
_appendFileComments: function(groups) {
var line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = GrDiffLine.FILE;
line.afterNumber = GrDiffLine.FILE;
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]));
},
/**
* In order to show comments out of the bounds of the selected context,
* treat them as separate chunks within the model so that the content (and
* context surrounding it) renders correctly.
*/
_splitCommonGroupsWithComments: function(content, lineNums) {
var result = [];
var leftLineNum = lineNums.left;
var rightLineNum = lineNums.right;
for (var i = 0; i < content.length; i++) {
if (!content[i].ab) {
result.push(content[i]);
if (content[i].a) {
leftLineNum += content[i].a.length;
}
if (content[i].b) {
rightLineNum += content[i].b.length;
}
continue;
}
var chunk = content[i].ab;
var currentChunk = {ab: []};
for (var j = 0; j < chunk.length; j++) {
leftLineNum++;
rightLineNum++;
if (this.keyLocations[DiffSide.LEFT][leftLineNum] ||
this.keyLocations[DiffSide.RIGHT][rightLineNum]) {
if (currentChunk.ab && currentChunk.ab.length > 0) {
result.push(currentChunk);
currentChunk = {ab: []};
}
result.push({ab: [chunk[j]]});
} else {
currentChunk.ab.push(chunk[j]);
}
}
// != instead of !== because we want to cover both undefined and null.
if (currentChunk.ab != null && currentChunk.ab.length > 0) {
result.push(currentChunk);
}
}
return result;
},
_appendCommonLines: function(rows, lines, lineNums) {
for (var i = 0; i < rows.length; i++) {
var line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.text = rows[i];
line.beforeNumber = ++lineNums.left;
line.afterNumber = ++lineNums.right;
lines.push(line);
}
},
_insertContextGroups: function(groups, lines, hiddenRange) {
var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
var linesAfterCtx = lines.slice(hiddenRange[1]);
if (linesBeforeCtx.length > 0) {
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
}
var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
ctxLine.contextGroup =
new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
[ctxLine]));
if (linesAfterCtx.length > 0) {
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
}
},
/**
* 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;
},
_appendRemovedLines: function(rows, lines, lineNums, opt_highlights) {
for (var i = 0; i < rows.length; i++) {
var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.text = rows[i];
line.beforeNumber = ++lineNums.left;
if (opt_highlights) {
line.highlights = opt_highlights.filter(function(hl) {
return hl.contentIndex === i;
});
}
lines.push(line);
}
},
_appendAddedLines: function(rows, lines, lineNums, opt_highlights) {
for (var i = 0; i < rows.length; i++) {
var line = new GrDiffLine(GrDiffLine.Type.ADD);
line.text = rows[i];
line.afterNumber = ++lineNums.right;
if (opt_highlights) {
line.highlights = opt_highlights.filter(function(hl) {
return hl.contentIndex === i;
});
}
lines.push(line);
}
},
});
})();