Separates diff processing from diff building

Moves the diff-processing functionality of the gr-diff-builder component
into a new gr-diff-processor component which exposes a promise-based
interface. This is step one of creating an asynchronous (non-blocking)
diff rendering system.

As much as possible, this change is a transfer of code (with tests) from
one component to another, making it easier to verify that functionality
has not changed. Cleanup of the code, and refactoring it into a
more-testable form will come with later changes.

Feature: Issue 3916
Change-Id: I875b03b20bf953b128cbe3c5001ba1f8eba12c61
This commit is contained in:
Wyatt Allen
2016-06-27 12:19:21 -07:00
parent f6e5149bbf
commit 7f2bd97901
8 changed files with 807 additions and 599 deletions

View File

@@ -28,9 +28,7 @@
GrDiffBuilderSideBySide.prototype);
GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage;
GrDiffBuilderImage.prototype.emitDiff = function() {
this.emitGroup(this._groups[0]);
GrDiffBuilderImage.prototype.renderDiffImages = function() {
var section = this._createElement('tbody', 'image-diff');
this._emitImagePair(section);

View File

@@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-diff-processor/gr-diff-processor.html">
<dom-module id="gr-diff-builder">
<template>
<div class="contentWrapper">
<content></content>
</div>
<gr-diff-processor id="processor"></gr-diff-processor>
</template>
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
@@ -59,7 +61,11 @@ limitations under the License.
render: function(diff, comments, prefs) {
this._builder = this._getDiffBuilder(diff, comments, prefs);
this._renderDiff();
this.$.processor.context = prefs.context;
this.$.processor.keyLocations = this._getCommentLocations(comments);
this.$.processor.process(diff.content)
.then(this._renderDiff.bind(this));
},
getLineElByChild: function(node) {
@@ -176,7 +182,7 @@ limitations under the License.
},
showContext: function(newGroups, sectionEl) {
var groups = this._builder._groups;
var groups = this._builder.groups;
// TODO(viktard): Polyfill findIndex for IE10.
var contextIndex = groups.findIndex(function(group) {
return group.element == sectionEl;
@@ -207,9 +213,14 @@ limitations under the License.
throw Error('Unsupported diff view mode: ' + this.viewMode);
},
_renderDiff: function() {
_renderDiff: function(groups) {
this._builder.groups = groups;
this._clearDiffContent();
this.emitDiff();
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
this.async(function() {
this.fire('render');
}, 1);
@@ -218,6 +229,23 @@ limitations under the License.
_clearDiffContent: function() {
this.diffElement.innerHTML = null;
},
_getCommentLocations: function(comments) {
var result = {
left: {},
right: {},
};
for (var side in comments) {
if (side !== GrDiffBuilder.Side.LEFT &&
side !== GrDiffBuilder.Side.RIGHT) {
continue;
}
comments[side].forEach(function(c) {
result[side][c.line || GrDiffLine.FILE] = true;
});
}
return result;
},
});
})();
</script>

View File

@@ -22,10 +22,7 @@
this._comments = comments;
this._prefs = prefs;
this._outputEl = outputEl;
this._groups = [];
this._commentLocations = this._getCommentLocations(comments);
this._processContent(diff.content, this._groups, prefs.context);
this.groups = [];
}
GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
@@ -63,8 +60,8 @@
var PARTIAL_CONTEXT_AMOUNT = 10;
GrDiffBuilder.prototype.emitDiff = function() {
for (var i = 0; i < this._groups.length; i++) {
this.emitGroup(this._groups[i]);
for (var i = 0; i < this.groups.length; i++) {
this.emitGroup(this.groups[i]);
}
};
@@ -79,8 +76,8 @@
};
GrDiffBuilder.prototype.renderSection = function(element) {
for (var i = 0; i < this._groups.length; i++) {
var group = this._groups[i];
for (var i = 0; i < this.groups.length; i++) {
var group = this.groups[i];
if (group.element === element) {
var newElement = this.buildSectionElement(group);
group.element.parentElement.replaceChild(newElement, group.element);
@@ -93,8 +90,8 @@
GrDiffBuilder.prototype.getGroupsByLineRange = function(
startLine, endLine, opt_side) {
var groups = [];
for (var i = 0; i < this._groups.length; i++) {
var group = this._groups[i];
for (var i = 0; i < this.groups.length; i++) {
var group = this.groups[i];
if (group.lines.length === 0) {
continue;
}
@@ -139,196 +136,11 @@
function(group) { return group.element; });
};
GrDiffBuilder.prototype._processContent = function(content, groups, context) {
this._appendFileComments(groups);
var WHOLE_FILE = -1;
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[GrDiffBuilder.GroupType.BOTH] !== undefined) {
var rows = group[GrDiffBuilder.GroupType.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[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
var highlights = undefined;
if (group[GrDiffBuilder.Highlights.REMOVED] !== undefined) {
highlights = this._normalizeIntralineHighlights(
group[GrDiffBuilder.GroupType.REMOVED],
group[GrDiffBuilder.Highlights.REMOVED]);
}
this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
lineNums, highlights);
}
if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
var highlights = undefined;
if (group[GrDiffBuilder.Highlights.ADDED] !== undefined) {
highlights = this._normalizeIntralineHighlights(
group[GrDiffBuilder.GroupType.ADDED],
group[GrDiffBuilder.Highlights.ADDED]);
}
this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
lineNums, highlights);
}
groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
}
};
GrDiffBuilder.prototype._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]));
};
GrDiffBuilder.prototype._getCommentLocations = function(comments) {
var result = {
left: {},
right: {},
};
for (var side in comments) {
if (side !== GrDiffBuilder.Side.LEFT &&
side !== GrDiffBuilder.Side.RIGHT) {
continue;
}
comments[side].forEach(function(c) {
result[side][c.line || GrDiffLine.FILE] = true;
});
}
return result;
};
GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
return this._commentLocations[side][lineNum] === true;
};
// 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.
GrDiffBuilder.prototype._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._commentIsAtLineNum(GrDiffBuilder.Side.LEFT, leftLineNum) ||
this._commentIsAtLineNum(GrDiffBuilder.Side.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;
};
// 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.
GrDiffBuilder.prototype._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;
};
// TODO (wyatta): Move this completely into the processor.
GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
hiddenRange) {
var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
@@ -350,46 +162,6 @@
}
};
GrDiffBuilder.prototype._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);
}
};
GrDiffBuilder.prototype._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);
}
};
GrDiffBuilder.prototype._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);
}
};
GrDiffBuilder.prototype._createContextControl = function(section, line) {
if (!line.contextGroup || !line.contextGroup.lines.length) {
return null;

View File

@@ -18,11 +18,23 @@ 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-builder</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
<script src="gr-diff-builder.js"></script>
<link rel="import" href="gr-diff-builder.html">
<test-fixture id="basic">
<template>
<gr-diff-builder>
<table id="diffTable"></table>
</gr-diff-builder>
</template>
</test-fixture>
<script>
suite('gr-diff-builder tests', function() {
var builder;
@@ -36,205 +48,6 @@ limitations under the License.
builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs);
});
test('process loaded content', function() {
var content = [
{
ab: [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
]
},
{
a: [
' Welcome ',
' to the wooorld of tomorrow!',
],
b: [
' Hello, world!',
],
},
{
ab: [
'Leela: This is the only place the ship cant hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
]
},
];
var groups = [];
builder._processContent(content, groups, -1);
assert.equal(groups.length, 4);
var group = groups[0];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 1);
assert.equal(group.lines[0].text, '');
assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
group = groups[1];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 2);
assert.equal(group.lines.length, 2);
function beforeNumberFn(l) { return l.beforeNumber; }
function afterNumberFn(l) { return l.afterNumber; }
function textFn(l) { return l.text; }
assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
assert.deepEqual(group.lines.map(textFn), [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
]);
group = groups[2];
assert.equal(group.type, GrDiffGroup.Type.DELTA);
assert.equal(group.lines.length, 3);
assert.equal(group.adds.length, 1);
assert.equal(group.removes.length, 2);
assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
assert.deepEqual(group.adds.map(afterNumberFn), [3]);
assert.deepEqual(group.removes.map(textFn), [
' Welcome ',
' to the wooorld of tomorrow!',
]);
assert.deepEqual(group.adds.map(textFn), [
' Hello, world!',
]);
group = groups[3];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 3);
assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
assert.deepEqual(group.lines.map(textFn), [
'Leela: This is the only place the ship cant hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
]);
});
test('insert context groups', function() {
var content = [
{ab: []},
{a: ['all work and no play make andybons a dull boy']},
{ab: []},
{b: ['elgoog elgoog elgoog']},
{ab: []},
];
for (var i = 0; i < 100; i++) {
content[0].ab.push('all work and no play make jack a dull boy');
content[4].ab.push('all work and no play make jill a dull girl');
}
for (var i = 0; i < 5; i++) {
content[2].ab.push('no tv and no beer make homer go crazy');
}
var groups = [];
var context = 10;
builder._processContent(content, groups, context);
assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[0].lines.length, 1);
assert.equal(groups[0].lines[0].text, '');
assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
groups[1].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[0].ab[0]);
});
assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[2].lines.length, context);
groups[2].lines.forEach(function(l) {
assert.equal(l.text, content[0].ab[0]);
});
assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[3].lines.length, 1);
assert.equal(groups[3].removes.length, 1);
assert.equal(groups[3].removes[0].text,
'all work and no play make andybons a dull boy');
assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[4].lines.length, 5);
groups[4].lines.forEach(function(l) {
assert.equal(l.text, content[2].ab[0]);
});
assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[5].lines.length, 1);
assert.equal(groups[5].adds.length, 1);
assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[6].lines.length, context);
groups[6].lines.forEach(function(l) {
assert.equal(l.text, content[4].ab[0]);
});
assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
groups[7].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[4].ab[0]);
});
content = [
{a: ['all work and no play make andybons a dull boy']},
{ab: []},
{b: ['elgoog elgoog elgoog']},
];
for (var i = 0; i < 50; i++) {
content[1].ab.push('no tv and no beer make homer go crazy');
}
groups = [];
builder._processContent(content, groups, 10);
assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[0].lines.length, 1);
assert.equal(groups[0].lines[0].text, '');
assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[1].lines.length, 1);
assert.equal(groups[1].removes.length, 1);
assert.equal(groups[1].removes[0].text,
'all work and no play make andybons a dull boy');
assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[2].lines.length, context);
groups[2].lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
groups[3].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[4].lines.length, context);
groups[4].lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[5].lines.length, 1);
assert.equal(groups[5].adds.length, 1);
assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
});
test('context control buttons', function() {
var section = {};
var line = {contextGroup: {lines: []}};
@@ -412,153 +225,11 @@ limitations under the License.
[{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
});
test('break up common diff chunks', function() {
builder._commentLocations = {
left: {1: true},
right: {10: true},
};
var lineNums = {
left: 0,
right: 0,
};
var content = [
{
ab: [
'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.',
]
}
];
var result = builder._splitCommonGroupsWithComments(content, lineNums);
assert.deepEqual(result, [
{
ab: ['Copyright (C) 2015 The Android Open Source Project'],
},
{
ab: [
'',
'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, ',
]
},
{
ab: ['software distributed under the License is distributed on an '],
},
{
ab: [
'"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.',
]
}
]);
});
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 = GrDiffBuilder.prototype._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 = GrDiffBuilder.prototype._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,
}
]);
});
suite('rendering', function() {
var content;
var outputEl;
setup(function() {
setup(function(done) {
var prefs = {
line_length: 10,
show_tabs: true,
@@ -577,32 +248,38 @@ limitations under the License.
]
},
];
outputEl = document.createElement('out');
builder =
new GrDiffBuilder(
{content: content}, {left: [], right: []}, prefs, outputEl);
builder.buildSectionElement = function(group) {
var section = document.createElement('stub');
section.textContent = group.lines.reduce(function(acc, line) {
return acc + line.text;
}, '');
return section;
};
builder.emitDiff();
element = fixture('basic');
outputEl = element.queryEffectiveChildren('#diffTable');
element.addEventListener('render', function() {
done();
});
sinon.stub(element, '_getDiffBuilder', function() {
var builder = new GrDiffBuilder(
{content: content}, {left: [], right: []}, prefs, outputEl);
builder.buildSectionElement = function(group) {
var section = document.createElement('stub');
section.textContent = group.lines.reduce(function(acc, line) {
return acc + line.text;
}, '');
return section;
};
return builder;
});
element.render({ content: content }, {left: [], right: []}, prefs);
});
test('renderSection', function() {
var section = outputEl.querySelector('stub:nth-of-type(2)');
var prevInnerHTML = section.innerHTML;
section.innerHTML = 'wiped';
builder.renderSection(section);
element._builder.renderSection(section);
section = outputEl.querySelector('stub:nth-of-type(2)');
assert.equal(section.innerHTML, prevInnerHTML);
});
test('getSectionsByLineRange one line', function() {
var section = outputEl.querySelector('stub:nth-of-type(2)');
var sections = builder.getSectionsByLineRange(1, 1, 'left');
var sections = element._builder.getSectionsByLineRange(1, 1, 'left');
assert.equal(sections.length, 1);
assert.strictEqual(sections[0], section);
});
@@ -612,7 +289,7 @@ limitations under the License.
outputEl.querySelector('stub:nth-of-type(2)'),
outputEl.querySelector('stub:nth-of-type(3)'),
];
var sections = builder.getSectionsByLineRange(1, 2, 'left');
var sections = element._builder.getSectionsByLineRange(1, 2, 'left');
assert.equal(sections.length, 2);
assert.strictEqual(sections[0], section[0]);
assert.strictEqual(sections[1], section[1]);

View File

@@ -0,0 +1,23 @@
<!--
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.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<dom-module id="gr-diff-processor">
<script src="../gr-diff/gr-diff-line.js"></script>
<script src="../gr-diff/gr-diff-group.js"></script>
<script src="gr-diff-processor.js"></script>
</dom-module>

View File

@@ -0,0 +1,303 @@
// 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);
}
},
});
})();

View File

@@ -0,0 +1,406 @@
<!DOCTYPE html>
<!--
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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-processor test</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-processor.html">
<test-fixture id="basic">
<template>
<gr-diff-processor></gr-diff-processor>
</template>
</test-fixture>
<script>
suite('gr-diff-processor tests', function() {
var element;
suite('not logged in', function() {
setup(function() {
element = fixture('basic');
element.context = 4;
});
test('process loaded content', function(done) {
var content = [
{
ab: [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
]
},
{
a: [
' Welcome ',
' to the wooorld of tomorrow!',
],
b: [
' Hello, world!',
],
},
{
ab: [
'Leela: This is the only place the ship cant hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
]
},
];
element.process(content).then(function() {
var groups = element.groups;
assert.equal(groups.length, 4);
var group = groups[0];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 1);
assert.equal(group.lines[0].text, '');
assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
group = groups[1];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 2);
assert.equal(group.lines.length, 2);
function beforeNumberFn(l) { return l.beforeNumber; }
function afterNumberFn(l) { return l.afterNumber; }
function textFn(l) { return l.text; }
assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
assert.deepEqual(group.lines.map(textFn), [
'<!DOCTYPE html>',
'<meta charset="utf-8">',
]);
group = groups[2];
assert.equal(group.type, GrDiffGroup.Type.DELTA);
assert.equal(group.lines.length, 3);
assert.equal(group.adds.length, 1);
assert.equal(group.removes.length, 2);
assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
assert.deepEqual(group.adds.map(afterNumberFn), [3]);
assert.deepEqual(group.removes.map(textFn), [
' Welcome ',
' to the wooorld of tomorrow!',
]);
assert.deepEqual(group.adds.map(textFn), [
' Hello, world!',
]);
group = groups[3];
assert.equal(group.type, GrDiffGroup.Type.BOTH);
assert.equal(group.lines.length, 3);
assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
assert.deepEqual(group.lines.map(textFn), [
'Leela: This is the only place the ship cant hear us, so ',
'everyone pretend to shower.',
'Fry: Same as every day. Got it.',
]);
done();
});
});
test('insert context groups', function(done) {
var content = [
{ab: []},
{a: ['all work and no play make andybons a dull boy']},
{ab: []},
{b: ['elgoog elgoog elgoog']},
{ab: []},
];
for (var i = 0; i < 100; i++) {
content[0].ab.push('all work and no play make jack a dull boy');
content[4].ab.push('all work and no play make jill a dull girl');
}
for (var i = 0; i < 5; i++) {
content[2].ab.push('no tv and no beer make homer go crazy');
}
var context = 10;
element.context = context;
element.process(content).then(function() {
var groups = element.groups;
assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[0].lines.length, 1);
assert.equal(groups[0].lines[0].text, '');
assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[1].lines[0].contextGroup.lines.length, 90);
groups[1].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[0].ab[0]);
});
assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[2].lines.length, context);
groups[2].lines.forEach(function(l) {
assert.equal(l.text, content[0].ab[0]);
});
assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[3].lines.length, 1);
assert.equal(groups[3].removes.length, 1);
assert.equal(groups[3].removes[0].text,
'all work and no play make andybons a dull boy');
assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[4].lines.length, 5);
groups[4].lines.forEach(function(l) {
assert.equal(l.text, content[2].ab[0]);
});
assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[5].lines.length, 1);
assert.equal(groups[5].adds.length, 1);
assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[6].lines.length, context);
groups[6].lines.forEach(function(l) {
assert.equal(l.text, content[4].ab[0]);
});
assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[7].lines[0].contextGroup.lines.length, 90);
groups[7].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[4].ab[0]);
});
done();
});
});
test('insert context groups', function(done) {
content = [
{a: ['all work and no play make andybons a dull boy']},
{ab: []},
{b: ['elgoog elgoog elgoog']},
];
for (var i = 0; i < 50; i++) {
content[1].ab.push('no tv and no beer make homer go crazy');
}
var context = 10;
element.context = context;
element.process(content).then(function() {
var groups = element.groups;
assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[0].lines.length, 1);
assert.equal(groups[0].lines[0].text, '');
assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[1].lines.length, 1);
assert.equal(groups[1].removes.length, 1);
assert.equal(groups[1].removes[0].text,
'all work and no play make andybons a dull boy');
assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[2].lines.length, context);
groups[2].lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup);
assert.equal(groups[3].lines[0].contextGroup.lines.length, 30);
groups[3].lines[0].contextGroup.lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[4].lines.length, context);
groups[4].lines.forEach(function(l) {
assert.equal(l.text, content[1].ab[0]);
});
assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[5].lines.length, 1);
assert.equal(groups[5].adds.length, 1);
assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
done();
});
});
test('break up common diff chunks', function() {
element.keyLocations = {
left: {1: true},
right: {10: true},
};
var lineNums = {left: 0, right: 0};
var content = [
{
ab: [
'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.',
]
}
];
var result = element._splitCommonGroupsWithComments(content, lineNums);
assert.deepEqual(result, [
{
ab: ['Copyright (C) 2015 The Android Open Source Project'],
},
{
ab: [
'',
'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, ',
]
},
{
ab: [
'software distributed under the License is distributed on an '],
},
{
ab: [
'"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.',
]
}
]);
});
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

@@ -49,6 +49,7 @@ limitations under the License.
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html',
'diff/gr-diff-preferences/gr-diff-preferences_test.html',
'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html',
'diff/gr-diff-view/gr-diff-view_test.html',
'diff/gr-diff/gr-diff-group_test.html',