| /** |
| * @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 {GrAnnotation} from '../gr-diff-highlight/gr-annotation'; |
| import {PolymerElement} from '@polymer/polymer/polymer-element'; |
| import {htmlTemplate} from './gr-ranged-comment-layer_html'; |
| import {GrDiffLine, GrDiffLineType} from '../gr-diff/gr-diff-line'; |
| import {strToClassName} from '../../../utils/dom-util'; |
| import {customElement, property, observe} from '@polymer/decorators'; |
| import {Side} from '../../../constants/constants'; |
| import { |
| PolymerDeepPropertyChange, |
| PolymerSpliceChange, |
| } from '@polymer/polymer/interfaces'; |
| 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; |
| hovering: boolean; |
| rootId: string; |
| } |
| |
| /** |
| * This class breaks down all comment ranges into individual line segment |
| * highlights. |
| */ |
| interface CommentRangeLineLayer { |
| hovering: boolean; |
| longRange: boolean; |
| rootId: string; |
| start: number; |
| end: number; |
| } |
| |
| type LinesMap = { |
| [line in number]: CommentRangeLineLayer[]; |
| }; |
| |
| type RangesMap = { |
| [side in Side]: LinesMap; |
| }; |
| |
| // Polymer 1 adds # before array's key, while Polymer 2 doesn't |
| const HOVER_PATH_PATTERN = /^(commentRanges\.#?\d+)\.hovering$/; |
| |
| const RANGE_BASE_ONLY = 'style-scope gr-diff range'; |
| const RANGE_HIGHLIGHT = 'style-scope gr-diff range rangeHighlight'; |
| const HOVER_HIGHLIGHT = 'style-scope gr-diff range rangeHoverHighlight'; |
| |
| @customElement('gr-ranged-comment-layer') |
| export class GrRangedCommentLayer extends PolymerElement implements DiffLayer { |
| static get template() { |
| return htmlTemplate; |
| } |
| |
| /** |
| * Fired when the range in a range comment was malformed and had to be |
| * normalized. |
| * |
| * It's `detail` has a `lineNum` and `side` parameter. |
| * |
| * @event normalize-range |
| */ |
| |
| @property({type: Array}) |
| commentRanges: CommentRangeLayer[] = []; |
| |
| @property({type: Array}) |
| _listeners: DiffLayerListener[] = []; |
| |
| @property({type: Object}) |
| _rangesMap: RangesMap = {left: {}, right: {}}; |
| |
| get styleModuleName() { |
| return 'gr-ranged-comment-styles'; |
| } |
| |
| /** |
| * 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') !== 'right') |
| ) { |
| ranges = ranges.concat(this._getRangesForLine(line, Side.LEFT)); |
| } |
| if ( |
| line.type === GrDiffLineType.ADD || |
| (line.type === GrDiffLineType.BOTH && |
| el.getAttribute('data-side') !== 'left') |
| ) { |
| ranges = ranges.concat(this._getRangesForLine(line, Side.RIGHT)); |
| } |
| |
| for (const range of ranges) { |
| GrAnnotation.annotateElement( |
| el, |
| range.start, |
| range.end - range.start, |
| (range.hovering |
| ? HOVER_HIGHLIGHT |
| : range.longRange |
| ? RANGE_BASE_ONLY |
| : RANGE_HIGHLIGHT) + ` ${strToClassName(range.rootId)}` |
| ); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| _notifyUpdateRange(start: number, end: number, side: Side) { |
| for (const listener of this._listeners) { |
| listener(start, end, side); |
| } |
| } |
| |
| /** |
| * Handle change in the ranges by updating the ranges maps and by |
| * emitting appropriate update notifications. |
| */ |
| @observe('commentRanges.*') |
| _handleCommentRangesChange( |
| record: PolymerDeepPropertyChange< |
| CommentRangeLayer[], |
| PolymerSpliceChange<CommentRangeLayer[]> |
| > |
| ) { |
| if (!record) return; |
| |
| // If the entire set of comments was changed. |
| if (record.path === 'commentRanges') { |
| const value = record.value as CommentRangeLayer[]; |
| this._rangesMap = {left: {}, right: {}}; |
| for (const {side, range, rootId, hovering} of value) { |
| const longRange = isLongCommentRange(range); |
| this._updateRangesMap({ |
| side, |
| range, |
| hovering, |
| operation: (forLine, start, end, hovering) => { |
| forLine.push({start, end, hovering, rootId, longRange}); |
| }, |
| }); |
| } |
| } |
| |
| // If the change only changed the `hovering` property of a comment. |
| const match = record.path.match(HOVER_PATH_PATTERN); |
| if (match) { |
| // The #number indicates the key of that item in the array |
| // not the index, especially in polymer 1. |
| const {side, range, hovering, rootId} = this.get(match[1]); |
| |
| this._updateRangesMap({ |
| side, |
| range, |
| hovering, |
| skipLayerUpdate: true, |
| operation: (forLine, start, end, hovering) => { |
| const index = forLine.findIndex( |
| lineRange => lineRange.start === start && lineRange.end === end |
| ); |
| forLine[index].hovering = hovering; |
| forLine[index].rootId = rootId; |
| }, |
| }); |
| } |
| |
| // If comments were spliced in or out. |
| if (record.path === 'commentRanges.splices') { |
| const value = record.value as PolymerSpliceChange<CommentRangeLayer[]>; |
| for (const indexSplice of value.indexSplices) { |
| const removed = indexSplice.removed; |
| for (const {side, range, hovering, rootId} of removed) { |
| this._updateRangesMap({ |
| side, |
| range, |
| hovering, |
| operation: (forLine, start, end) => { |
| const index = forLine.findIndex( |
| lineRange => |
| lineRange.start === start && |
| lineRange.end === end && |
| rootId === lineRange.rootId |
| ); |
| forLine.splice(index, 1); |
| }, |
| }); |
| } |
| const added = indexSplice.object.slice( |
| indexSplice.index, |
| indexSplice.index + indexSplice.addedCount |
| ); |
| for (const {side, range, hovering, rootId} of added) { |
| const longRange = isLongCommentRange(range); |
| this._updateRangesMap({ |
| side, |
| range, |
| hovering, |
| operation: (forLine, start, end, hovering) => { |
| forLine.push({start, end, hovering, rootId, longRange}); |
| }, |
| }); |
| } |
| } |
| } |
| } |
| |
| _updateRangesMap(options: { |
| side: Side; |
| range: CommentRange; |
| hovering: boolean; |
| operation: ( |
| forLine: CommentRangeLineLayer[], |
| start: number, |
| end: number, |
| hovering: boolean |
| ) => void; |
| skipLayerUpdate?: boolean; |
| }) { |
| const {side, range, hovering, operation, skipLayerUpdate} = 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, hovering); |
| } |
| if (!skipLayerUpdate) { |
| this._notifyUpdateRange(range.start_line, range.end_line, side); |
| } |
| } |
| |
| _getRangesForLine(line: GrDiffLine, side: Side) { |
| const lineNum = side === Side.LEFT ? line.beforeNumber : line.afterNumber; |
| const ranges: CommentRangeLineLayer[] = |
| this.get(['_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; |
| this.dispatchEvent( |
| new CustomEvent('normalize-range', { |
| bubbles: true, |
| composed: true, |
| detail: {lineNum, side}, |
| }) |
| ); |
| } |
| |
| return range; |
| }) |
| // Sort the ranges so that hovering highlights are on top. |
| .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0)) |
| ); |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-ranged-comment-layer': GrRangedCommentLayer; |
| } |
| } |