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
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
new file mode 100644
index 0000000..0d787c1
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -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);
+ }
+ },
+ });
+})();