blob: c62c603fbe74f967e44356b34551566c3bd027c9 [file] [log] [blame]
/**
* @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<!GrDiffLine>=} 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<!GrDiffLine>} */
this.lines = [];
/** @type {!Array<!GrDiffLine>} */
this.adds = [];
/** @type {!Array<!GrDiffLine>} */
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<!GrDiffGroup>} 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>}
*/
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<!GrDiffGroup>} 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<!Array<!GrDiffGroup>>} 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<!GrDiffLine>} 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);