| /** |
| * @license |
| * Copyright 2016 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import {GrAnnotation} from '../gr-diff-highlight/gr-annotation'; |
| import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line'; |
| import {strToClassName} from '../../../utils/dom-util'; |
| import {Side} from '../../../constants/constants'; |
| import {CommentRange} from '../../../types/common'; |
| import {DiffLayer, DiffLayerListener} from '../../../types/types'; |
| import {isLongCommentRange} from '../gr-diff/gr-diff-utils'; |
| |
| /** |
| * Enhanced CommentRange by UI state. Interface for incoming ranges set from the |
| * outside. |
| * |
| * TODO(TS): Unify with what is used in gr-diff when these objects are created. |
| */ |
| export interface CommentRangeLayer { |
| side: Side; |
| range: CommentRange; |
| // New drafts don't have a rootId. |
| rootId?: string; |
| } |
| |
| /** Can be used for array functions like `some()`. */ |
| function equals(a: CommentRangeLayer) { |
| return (b: CommentRangeLayer) => id(a) === id(b); |
| } |
| |
| function id(r: CommentRangeLayer): string { |
| if (r.rootId) return r.rootId; |
| return `${r.side}-${r.range.start_line}-${r.range.start_character}-${r.range.end_line}-${r.range.end_character}`; |
| } |
| |
| /** |
| * This class breaks down all comment ranges into individual line segment |
| * highlights. |
| */ |
| interface CommentRangeLineLayer { |
| longRange: boolean; |
| id: string; |
| // start char (0-based) |
| start: number; |
| // end char (0-based) |
| end: number; |
| } |
| |
| type LinesMap = { |
| [line in number]: CommentRangeLineLayer[]; |
| }; |
| |
| type RangesMap = { |
| [side in Side]: LinesMap; |
| }; |
| |
| const RANGE_BASE_ONLY = 'style-scope gr-diff range'; |
| const RANGE_HIGHLIGHT = 'style-scope gr-diff range rangeHighlight'; |
| // Note that there is also `rangeHoverHighlight` being set by GrDiffHighlight. |
| |
| export class GrRangedCommentLayer implements DiffLayer { |
| private knownRanges: CommentRangeLayer[] = []; |
| |
| private listeners: DiffLayerListener[] = []; |
| |
| private rangesMap: RangesMap = {left: {}, right: {}}; |
| |
| /** |
| * Layer method to add annotations to a line. |
| * |
| * @param el The DIV.contentText element to apply the annotation to. |
| */ |
| annotate(el: HTMLElement, _: HTMLElement, line: GrDiffLine) { |
| let ranges: CommentRangeLineLayer[] = []; |
| if ( |
| line.type === GrDiffLineType.REMOVE || |
| (line.type === GrDiffLineType.BOTH && |
| el.getAttribute('data-side') !== Side.RIGHT) |
| ) { |
| ranges = this.getRangesForLine(line, Side.LEFT); |
| } |
| if ( |
| line.type === GrDiffLineType.ADD || |
| (line.type === GrDiffLineType.BOTH && |
| el.getAttribute('data-side') !== Side.LEFT) |
| ) { |
| ranges = this.getRangesForLine(line, Side.RIGHT); |
| } |
| |
| for (const range of ranges) { |
| GrAnnotation.annotateElement( |
| el, |
| range.start, |
| range.end - range.start, |
| (range.longRange ? RANGE_BASE_ONLY : RANGE_HIGHLIGHT) + |
| ` ${strToClassName(range.id)}` |
| ); |
| } |
| } |
| |
| /** |
| * Register a listener for layer updates. |
| */ |
| addListener(listener: DiffLayerListener) { |
| this.listeners.push(listener); |
| } |
| |
| removeListener(listener: DiffLayerListener) { |
| this.listeners = this.listeners.filter(f => f !== listener); |
| } |
| |
| /** |
| * Notify Layer listeners of changes to annotations. |
| */ |
| private notifyUpdateRange(start: number, end: number, side: Side) { |
| for (const listener of this.listeners) { |
| listener(start, end, side); |
| } |
| } |
| |
| updateRanges(newRanges: CommentRangeLayer[]) { |
| for (const newRange of newRanges) { |
| if (this.knownRanges.some(equals(newRange))) continue; |
| this.addRange(newRange); |
| } |
| |
| for (const knownRange of this.knownRanges) { |
| if (newRanges.some(equals(knownRange))) continue; |
| this.removeRange(knownRange); |
| } |
| |
| this.knownRanges = [...newRanges]; |
| } |
| |
| private addRange(commentRange: CommentRangeLayer) { |
| const {side, range} = commentRange; |
| const longRange = isLongCommentRange(range); |
| this.updateRangesMap({ |
| side, |
| range, |
| operation: (forLine, startChar, endChar) => { |
| forLine.push({ |
| start: startChar, |
| end: endChar, |
| id: id(commentRange), |
| longRange, |
| }); |
| }, |
| }); |
| } |
| |
| private removeRange(commentRange: CommentRangeLayer) { |
| const {side, range} = commentRange; |
| this.updateRangesMap({ |
| side, |
| range, |
| operation: forLine => { |
| const index = forLine.findIndex( |
| lineRange => id(commentRange) === lineRange.id |
| ); |
| if (index > -1) forLine.splice(index, 1); |
| }, |
| }); |
| } |
| |
| private updateRangesMap(options: { |
| side: Side; |
| range: CommentRange; |
| operation: ( |
| forLine: CommentRangeLineLayer[], |
| start: number, |
| end: number |
| ) => void; |
| }) { |
| const {side, range, operation} = options; |
| const forSide = this.rangesMap[side] || (this.rangesMap[side] = {}); |
| for (let line = range.start_line; line <= range.end_line; line++) { |
| const forLine = forSide[line] || (forSide[line] = []); |
| const start = line === range.start_line ? range.start_character : 0; |
| const end = line === range.end_line ? range.end_character : -1; |
| operation(forLine, start, end); |
| } |
| this.notifyUpdateRange(range.start_line, range.end_line, side); |
| } |
| |
| // visible for testing |
| getRangesForLine(line: GrDiffLine, side: Side): CommentRangeLineLayer[] { |
| const lineNum = side === Side.LEFT ? line.beforeNumber : line.afterNumber; |
| if (lineNum === 'FILE' || lineNum === 'LOST') return []; |
| const ranges: CommentRangeLineLayer[] = this.rangesMap[side][lineNum] || []; |
| return ranges.map(range => { |
| // Make a copy, so that the normalization below does not mess with |
| // our map. |
| range = {...range}; |
| range.end = range.end === -1 ? line.text.length : range.end; |
| |
| // Normalize invalid ranges where the start is after the end but the |
| // start still makes sense. Set the end to the end of the line. |
| // @see Issue 5744 |
| if (range.start >= range.end && range.start < line.text.length) { |
| range.end = line.text.length; |
| } |
| |
| return range; |
| }); |
| } |
| } |