/** * @license * 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(window) { 'use strict'; // Prevent redefinition. if (window.GrDiffGroup) { return; } /** * A chunk of the diff that should be rendered together. * * @constructor * @param {!GrDiffGroup.Type} type * @param {!Array=} opt_lines */ function GrDiffGroup(type, opt_lines) { /** @type {!GrDiffGroup.Type} */ this.type = type; /** @type {boolean} */ this.dueToRebase = false; /** * True means all changes in this line are whitespace changes that should * not be highlighted as changed as per the user settings. * * @type{boolean} */ this.ignoredWhitespaceOnly = false; /** * True means it should not be collapsed (because it was in the URL, or * there is a comment on that line) */ this.keyLocation = false; /** @type {?HTMLElement} */ this.element = null; /** @type {!Array} */ this.lines = []; /** @type {!Array} */ this.adds = []; /** @type {!Array} */ this.removes = []; /** Both start and end line are inclusive. */ this.lineRange = { left: {start: null, end: null}, right: {start: null, end: null}, }; if (opt_lines) { opt_lines.forEach(this.addLine, this); } } /** @enum {string} */ GrDiffGroup.Type = { /** Unchanged context. */ BOTH: 'both', /** A widget used to show more context. */ CONTEXT_CONTROL: 'contextControl', /** Added, removed or modified chunk. */ DELTA: 'delta', }; /** * Hides lines in the given range behind a context control group. * * Groups that would be partially visible are split into their visible and * hidden parts, respectively. * The groups need to be "common groups", meaning they have to have either * originated from an `ab` chunk, or from an `a`+`b` chunk with * `common: true`. * * If the hidden range is 1 line or less, nothing is hidden and no context * control group is created. * * @param {!Array} groups Common groups, ordered by their line * ranges. * @param {number} hiddenStart The first element to be hidden, as a * non-negative line number offset relative to the first group's start * line, left and right respectively. * @param {number} hiddenEnd The first visible element after the hidden range, * as a non-negative line number offset relative to the first group's * start line, left and right respectively. * @return {!Array} */ GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) { if (groups.length === 0) return []; // Clamp hiddenStart and hiddenEnd - inspired by e.g. substring hiddenStart = Math.max(hiddenStart, 0); hiddenEnd = Math.max(hiddenEnd, hiddenStart); let before = []; let hidden = groups; let after = []; const numHidden = hiddenEnd - hiddenStart; // Only collapse if there is more than 1 line to be hidden. if (numHidden > 1) { if (hiddenStart) { [before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart); } if (hiddenEnd) { [hidden, after] = GrDiffGroup._splitCommonGroups( hidden, hiddenEnd - hiddenStart); } } else { [hidden, after] = [[], hidden]; } const result = [...before]; if (hidden.length) { const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); ctxLine.contextGroups = hidden; const ctxGroup = new GrDiffGroup( GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]); result.push(ctxGroup); } result.push(...after); return result; }; /** * Splits a list of common groups into two lists of groups. * * Groups where all lines are before or all lines are after the split will be * retained as is and put into the first or second list respectively. Groups * with some lines before and some lines after the split will be split into * two groups, which will be put into the first and second list. * * @param {!Array} groups * @param {number} split A line number offset relative to the first group's * start line at which the groups should be split. * @return {!Array>} The outer array has 2 elements, the * list of groups before and the list of groups after the split. */ GrDiffGroup._splitCommonGroups = function(groups, split) { if (groups.length === 0) return [[], []]; const leftSplit = groups[0].lineRange.left.start + split; const rightSplit = groups[0].lineRange.right.start + split; const beforeGroups = []; const afterGroups = []; for (const group of groups) { if (group.lineRange.left.end < leftSplit || group.lineRange.right.end < rightSplit) { beforeGroups.push(group); continue; } if (leftSplit <= group.lineRange.left.start || rightSplit <= group.lineRange.right.start) { afterGroups.push(group); continue; } const before = []; const after = []; for (const line of group.lines) { if ((line.beforeNumber && line.beforeNumber < leftSplit) || (line.afterNumber && line.afterNumber < rightSplit)) { before.push(line); } else { after.push(line); } } if (before.length) { beforeGroups.push(before.length === group.lines.length ? group : group.cloneWithLines(before)); } if (after.length) { afterGroups.push(after.length === group.lines.length ? group : group.cloneWithLines(after)); } } return [beforeGroups, afterGroups]; }; /** * Creates a new group with the same properties but different lines. * * The element property is not copied, because the original element is still a * rendering of the old lines, so that would not make sense. * * @param {!Array} lines * @return {!GrDiffGroup} */ GrDiffGroup.prototype.cloneWithLines = function(lines) { const group = new GrDiffGroup(this.type, lines); group.dueToRebase = this.dueToRebase; group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly; return group; }; /** @param {!GrDiffLine} line */ GrDiffGroup.prototype.addLine = function(line) { this.lines.push(line); const notDelta = (this.type === GrDiffGroup.Type.BOTH || this.type === GrDiffGroup.Type.CONTEXT_CONTROL); if (notDelta && (line.type === GrDiffLine.Type.ADD || line.type === GrDiffLine.Type.REMOVE)) { throw Error('Cannot add delta line to a non-delta group.'); } if (line.type === GrDiffLine.Type.ADD) { this.adds.push(line); } else if (line.type === GrDiffLine.Type.REMOVE) { this.removes.push(line); } this._updateRange(line); }; /** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */ GrDiffGroup.prototype.getSideBySidePairs = function() { if (this.type === GrDiffGroup.Type.BOTH || this.type === GrDiffGroup.Type.CONTEXT_CONTROL) { return this.lines.map(line => { return { left: line, right: line, }; }); } const pairs = []; let i = 0; let j = 0; while (i < this.removes.length || j < this.adds.length) { pairs.push({ left: this.removes[i] || GrDiffLine.BLANK_LINE, right: this.adds[j] || GrDiffLine.BLANK_LINE, }); i++; j++; } return pairs; }; GrDiffGroup.prototype._updateRange = function(line) { if (line.beforeNumber === 'FILE' || line.afterNumber === 'FILE') { return; } if (line.type === GrDiffLine.Type.ADD || line.type === GrDiffLine.Type.BOTH) { if (this.lineRange.right.start === null || line.afterNumber < this.lineRange.right.start) { this.lineRange.right.start = line.afterNumber; } if (this.lineRange.right.end === null || line.afterNumber > this.lineRange.right.end) { this.lineRange.right.end = line.afterNumber; } } if (line.type === GrDiffLine.Type.REMOVE || line.type === GrDiffLine.Type.BOTH) { if (this.lineRange.left.start === null || line.beforeNumber < this.lineRange.left.start) { this.lineRange.left.start = line.beforeNumber; } if (this.lineRange.left.end === null || line.beforeNumber > this.lineRange.left.end) { this.lineRange.left.end = line.beforeNumber; } } }; window.GrDiffGroup = GrDiffGroup; })(window);