// 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); } }, }); })();