summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
diff options
context:
space:
mode:
Diffstat (limited to 'polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts')
-rw-r--r--polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts479
1 files changed, 479 insertions, 0 deletions
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
new file mode 100644
index 0000000000..b8262e0578
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -0,0 +1,479 @@
+/**
+ * @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.
+ */
+import {BLANK_LINE, GrDiffLine, GrDiffLineType} from './gr-diff-line';
+import {LineRange, Side} from '../../../api/diff';
+import {LineNumber} from './gr-diff-line';
+
+export enum GrDiffGroupType {
+ /** Unchanged context. */
+ BOTH = 'both',
+
+ /** A widget used to show more context. */
+ CONTEXT_CONTROL = 'contextControl',
+
+ /** Added, removed or modified chunk. */
+ DELTA = 'delta',
+}
+
+export interface GrDiffLinePair {
+ left: GrDiffLine;
+ right: GrDiffLine;
+}
+
+/**
+ * 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 3 lines or less, nothing is hidden and no context
+ * control group is created.
+ *
+ * @param groups Common groups, ordered by their line ranges.
+ * @param 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 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.
+ */
+export function hideInContextControl(
+ groups: readonly GrDiffGroup[],
+ hiddenStart: number,
+ hiddenEnd: number
+): GrDiffGroup[] {
+ 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: GrDiffGroup[] = [];
+ let hidden = groups;
+ let after: readonly GrDiffGroup[] = [];
+
+ const numHidden = hiddenEnd - hiddenStart;
+
+ // Showing a context control row for less than 4 lines does not make much,
+ // because then that row would consume as much space as the collapsed code.
+ if (numHidden > 3) {
+ if (hiddenStart) {
+ [before, hidden] = _splitCommonGroups(hidden, hiddenStart);
+ }
+ if (hiddenEnd) {
+ let beforeLength = 0;
+ if (before.length > 0) {
+ const beforeStart = before[0].lineRange.left.start_line;
+ const beforeEnd = before[before.length - 1].lineRange.left.end_line;
+ beforeLength = beforeEnd - beforeStart + 1;
+ }
+ [hidden, after] = _splitCommonGroups(hidden, hiddenEnd - beforeLength);
+ }
+ } else {
+ [hidden, after] = [[], hidden];
+ }
+
+ const result = [...before];
+ if (hidden.length) {
+ result.push(
+ new GrDiffGroup({
+ type: GrDiffGroupType.CONTEXT_CONTROL,
+ contextGroups: [...hidden],
+ })
+ );
+ }
+ result.push(...after);
+ return result;
+}
+
+/**
+ * Splits a group in two, defined by leftSplit and rightSplit. Primarily to be
+ * used in function _splitCommonGroups
+ * 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 group The group to be split in two
+ * @param leftSplit The line number relative to the split on the left side
+ * @param rightSplit The line number relative to the split on the right side
+ * @return two new groups, one before the split and another after it
+ */
+function _splitGroupInTwo(
+ group: GrDiffGroup,
+ leftSplit: number,
+ rightSplit: number
+) {
+ let beforeSplit: GrDiffGroup | undefined;
+ let afterSplit: GrDiffGroup | undefined;
+ // split line is in the middle of a group, we need to break the group
+ // in lines before and after the split.
+ if (group.skip) {
+ // Currently we assume skip chunks "refuse" to be split. Expanding this
+ // group will in the future mean load more data - and therefore we want to
+ // fire an event when user wants to do it.
+ const closerToStartThanEnd =
+ leftSplit - group.lineRange.left.start_line <
+ group.lineRange.right.end_line - leftSplit;
+ if (closerToStartThanEnd) {
+ afterSplit = group;
+ } else {
+ beforeSplit = group;
+ }
+ } else {
+ 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) {
+ beforeSplit =
+ before.length === group.lines.length
+ ? group
+ : group.cloneWithLines(before);
+ }
+ if (after.length) {
+ afterSplit =
+ after.length === group.lines.length
+ ? group
+ : group.cloneWithLines(after);
+ }
+ }
+ return {beforeSplit, afterSplit};
+}
+
+/**
+ * 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 split A line number offset relative to the first group's
+ * start line at which the groups should be split.
+ * @return The outer array has 2 elements, the
+ * list of groups before and the list of groups after the split.
+ */
+function _splitCommonGroups(
+ groups: readonly GrDiffGroup[],
+ split: number
+): GrDiffGroup[][] {
+ if (groups.length === 0) return [[], []];
+ const leftSplit = groups[0].lineRange.left.start_line + split;
+ const rightSplit = groups[0].lineRange.right.start_line + split;
+
+ const beforeGroups = [];
+ const afterGroups = [];
+ for (const group of groups) {
+ const isCompletelyBefore =
+ group.lineRange.left.end_line < leftSplit ||
+ group.lineRange.right.end_line < rightSplit;
+ const isCompletelyAfter =
+ leftSplit <= group.lineRange.left.start_line ||
+ rightSplit <= group.lineRange.right.start_line;
+ if (isCompletelyBefore) {
+ beforeGroups.push(group);
+ } else if (isCompletelyAfter) {
+ afterGroups.push(group);
+ } else {
+ const {beforeSplit, afterSplit} = _splitGroupInTwo(
+ group,
+ leftSplit,
+ rightSplit
+ );
+ if (beforeSplit) {
+ beforeGroups.push(beforeSplit);
+ }
+ if (afterSplit) {
+ afterGroups.push(afterSplit);
+ }
+ }
+ }
+ return [beforeGroups, afterGroups];
+}
+
+export interface GrMoveDetails {
+ changed: boolean;
+ range?: {
+ start: number;
+ end: number;
+ };
+}
+
+/** A chunk of the diff that should be rendered together. */
+export class GrDiffGroup {
+ constructor(
+ options:
+ | {
+ type: GrDiffGroupType.BOTH | GrDiffGroupType.DELTA;
+ lines?: GrDiffLine[];
+ skip?: undefined;
+ moveDetails?: GrMoveDetails;
+ dueToRebase?: boolean;
+ ignoredWhitespaceOnly?: boolean;
+ keyLocation?: boolean;
+ }
+ | {
+ type: GrDiffGroupType.BOTH | GrDiffGroupType.DELTA;
+ lines?: undefined;
+ skip: number;
+ offsetLeft: number;
+ offsetRight: number;
+ moveDetails?: GrMoveDetails;
+ dueToRebase?: boolean;
+ ignoredWhitespaceOnly?: boolean;
+ keyLocation?: boolean;
+ }
+ | {
+ type: GrDiffGroupType.CONTEXT_CONTROL;
+ contextGroups: GrDiffGroup[];
+ }
+ ) {
+ this.type = options.type;
+ switch (options.type) {
+ case GrDiffGroupType.BOTH:
+ case GrDiffGroupType.DELTA: {
+ this.moveDetails = options.moveDetails;
+ this.dueToRebase = options.dueToRebase ?? false;
+ this.ignoredWhitespaceOnly = options.ignoredWhitespaceOnly ?? false;
+ this.keyLocation = options.keyLocation ?? false;
+ if (options.skip && options.lines) {
+ throw new Error('Cannot set skip and lines');
+ }
+ this.skip = options.skip;
+ if (options.skip) {
+ this.lineRange = {
+ left: {
+ start_line: options.offsetLeft,
+ end_line: options.offsetLeft + options.skip - 1,
+ },
+ right: {
+ start_line: options.offsetRight,
+ end_line: options.offsetRight + options.skip - 1,
+ },
+ };
+ } else {
+ for (const line of options.lines ?? []) {
+ this.addLine(line);
+ }
+ }
+ break;
+ }
+ case GrDiffGroupType.CONTEXT_CONTROL: {
+ this.contextGroups = options.contextGroups;
+ if (this.contextGroups.length > 0) {
+ const firstGroup = this.contextGroups[0];
+ const lastGroup = this.contextGroups[this.contextGroups.length - 1];
+ this.lineRange = {
+ left: {
+ start_line: firstGroup.lineRange.left.start_line,
+ end_line: lastGroup.lineRange.left.end_line,
+ },
+ right: {
+ start_line: firstGroup.lineRange.right.start_line,
+ end_line: lastGroup.lineRange.right.end_line,
+ },
+ };
+ }
+ break;
+ }
+ default:
+ throw new Error(`Unknown group type: ${this.type}`);
+ }
+ }
+
+ readonly type: GrDiffGroupType;
+
+ readonly dueToRebase: boolean = false;
+
+ /**
+ * True means all changes in this line are whitespace changes that should
+ * not be highlighted as changed as per the user settings.
+ */
+ readonly ignoredWhitespaceOnly: boolean = false;
+
+ /**
+ * True means it should not be collapsed (because it was in the URL, or
+ * there is a comment on that line)
+ */
+ readonly keyLocation: boolean = false;
+
+ /**
+ * Once rendered the diff builder sets this to the diff section element.
+ */
+ element?: HTMLElement;
+
+ readonly lines: GrDiffLine[] = [];
+
+ readonly adds: GrDiffLine[] = [];
+
+ readonly removes: GrDiffLine[] = [];
+
+ readonly contextGroups: GrDiffGroup[] = [];
+
+ readonly skip?: number;
+
+ /** Both start and end line are inclusive. */
+ readonly lineRange: {[side in Side]: LineRange} = {
+ [Side.LEFT]: {start_line: 0, end_line: 0},
+ [Side.RIGHT]: {start_line: 0, end_line: 0},
+ };
+
+ readonly moveDetails?: GrMoveDetails;
+
+ /**
+ * 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.
+ */
+ cloneWithLines(lines: GrDiffLine[]): GrDiffGroup {
+ if (
+ this.type !== GrDiffGroupType.BOTH &&
+ this.type !== GrDiffGroupType.DELTA
+ ) {
+ throw new Error('Cannot clone context group with lines');
+ }
+ const group = new GrDiffGroup({
+ type: this.type,
+ lines,
+ dueToRebase: this.dueToRebase,
+ ignoredWhitespaceOnly: this.ignoredWhitespaceOnly,
+ });
+ return group;
+ }
+
+ private addLine(line: GrDiffLine) {
+ this.lines.push(line);
+
+ const notDelta =
+ this.type === GrDiffGroupType.BOTH ||
+ this.type === GrDiffGroupType.CONTEXT_CONTROL;
+ if (
+ notDelta &&
+ (line.type === GrDiffLineType.ADD || line.type === GrDiffLineType.REMOVE)
+ ) {
+ throw Error('Cannot add delta line to a non-delta group.');
+ }
+
+ if (line.type === GrDiffLineType.ADD) {
+ this.adds.push(line);
+ } else if (line.type === GrDiffLineType.REMOVE) {
+ this.removes.push(line);
+ }
+ this._updateRangeWithNewLine(line);
+ }
+
+ getSideBySidePairs(): GrDiffLinePair[] {
+ if (
+ this.type === GrDiffGroupType.BOTH ||
+ this.type === GrDiffGroupType.CONTEXT_CONTROL
+ ) {
+ return this.lines.map(line => {
+ return {
+ left: line,
+ right: line,
+ };
+ });
+ }
+
+ const pairs: GrDiffLinePair[] = [];
+ let i = 0;
+ let j = 0;
+ while (i < this.removes.length || j < this.adds.length) {
+ pairs.push({
+ left: this.removes[i] || BLANK_LINE,
+ right: this.adds[j] || BLANK_LINE,
+ });
+ i++;
+ j++;
+ }
+ return pairs;
+ }
+
+ /** Returns true if it is, or contains, a skip group. */
+ hasSkipGroup() {
+ return !!this.skip || this.contextGroups?.some(g => !!g.skip);
+ }
+
+ containsLine(side: Side, line: LineNumber) {
+ if (line === 'FILE' || line === 'LOST') {
+ // For FILE and LOST, beforeNumber and afterNumber are the same
+ return this.lines[0]?.beforeNumber === line;
+ }
+ const lineRange = this.lineRange[side];
+ return lineRange.start_line <= line && line <= lineRange.end_line;
+ }
+
+ private _updateRangeWithNewLine(line: GrDiffLine) {
+ if (
+ line.beforeNumber === 'FILE' ||
+ line.afterNumber === 'FILE' ||
+ line.beforeNumber === 'LOST' ||
+ line.afterNumber === 'LOST'
+ ) {
+ return;
+ }
+
+ if (line.type === GrDiffLineType.ADD || line.type === GrDiffLineType.BOTH) {
+ if (
+ this.lineRange.right.start_line === 0 ||
+ line.afterNumber < this.lineRange.right.start_line
+ ) {
+ this.lineRange.right.start_line = line.afterNumber;
+ }
+ if (line.afterNumber > this.lineRange.right.end_line) {
+ this.lineRange.right.end_line = line.afterNumber;
+ }
+ }
+
+ if (
+ line.type === GrDiffLineType.REMOVE ||
+ line.type === GrDiffLineType.BOTH
+ ) {
+ if (
+ this.lineRange.left.start_line === 0 ||
+ line.beforeNumber < this.lineRange.left.start_line
+ ) {
+ this.lineRange.left.start_line = line.beforeNumber;
+ }
+ if (line.beforeNumber > this.lineRange.left.end_line) {
+ this.lineRange.left.end_line = line.beforeNumber;
+ }
+ }
+ }
+
+ /**
+ * Determines whether the group is either totally an addition or totally
+ * a removal.
+ */
+ isTotal(): boolean {
+ return (
+ this.type === GrDiffGroupType.DELTA &&
+ (!this.adds.length || !this.removes.length) &&
+ !(!this.adds.length && !this.removes.length)
+ );
+ }
+}