diff options
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.ts | 479 |
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) + ); + } +} |