/** * @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() { 'use strict'; // Polymer 1 adds # before array's key, while Polymer 2 doesn't const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/; const RANGE_HIGHLIGHT = 'style-scope gr-diff range'; const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight'; class GrRangedCommentLayer extends Polymer.GestureEventListeners( Polymer.LegacyElementMixin( Polymer.Element)) { static get is() { return 'gr-ranged-comment-layer'; } /** * 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 */ static get properties() { return { /** @type {!Array} */ commentRanges: Array, _listeners: { type: Array, value() { return []; }, }, _rangesMap: { type: Object, value() { return {left: {}, right: {}}; }, }, }; } static get observers() { return [ '_handleCommentRangesChange(commentRanges.*)', ]; } get styleModuleName() { return 'gr-ranged-comment-styles'; } /** * Layer method to add annotations to a line. * @param {!HTMLElement} el The DIV.contentText element to apply the * annotation to. * @param {!HTMLElement} lineNumberEl * @param {!Object} line The line object. (GrDiffLine) */ annotate(el, lineNumberEl, line) { let ranges = []; if (line.type === GrDiffLine.Type.REMOVE || ( line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'right')) { ranges = ranges.concat(this._getRangesForLine(line, 'left')); } if (line.type === GrDiffLine.Type.ADD || ( line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'left')) { ranges = ranges.concat(this._getRangesForLine(line, 'right')); } for (const range of ranges) { GrAnnotation.annotateElement(el, range.start, range.end - range.start, range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); } } /** * Register a listener for layer updates. * @param {function(number, number, string)} fn The update handler function. * Should accept as arguments the line numbers for the start and end of * the update and the side as a string. */ addListener(fn) { this._listeners.push(fn); } /** * Notify Layer listeners of changes to annotations. * @param {number} start The line where the update starts. * @param {number} end The line where the update ends. * @param {string} side The side of the update. ('left' or 'right') */ _notifyUpdateRange(start, end, 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. * @param {Object} record The change record. */ _handleCommentRangesChange(record) { if (!record) return; // If the entire set of comments was changed. if (record.path === 'commentRanges') { this._rangesMap = {left: {}, right: {}}; for (const {side, range, hovering} of record.value) { this._updateRangesMap( side, range, hovering, (forLine, start, end, hovering) => { forLine.push({start, end, hovering}); }); } } // 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} = this.get(match[1]); this._updateRangesMap( side, range, hovering, (forLine, start, end, hovering) => { const index = forLine.findIndex(lineRange => lineRange.start === start && lineRange.end === end); forLine[index].hovering = hovering; }); } // If comments were spliced in or out. if (record.path === 'commentRanges.splices') { for (const indexSplice of record.value.indexSplices) { const removed = indexSplice.removed; for (const {side, range, hovering} of removed) { this._updateRangesMap( side, range, hovering, (forLine, start, end) => { const index = forLine.findIndex(lineRange => lineRange.start === start && lineRange.end === end); forLine.splice(index, 1); }); } const added = indexSplice.object.slice( indexSplice.index, indexSplice.index + indexSplice.addedCount); for (const {side, range, hovering} of added) { this._updateRangesMap( side, range, hovering, (forLine, start, end, hovering) => { forLine.push({start, end, hovering}); }); } } } } _updateRangesMap(side, range, hovering, operation) { 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); } this._notifyUpdateRange(range.start_line, range.end_line, side); } _getRangesForLine(line, side) { const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; const ranges = this.get(['_rangesMap', side, lineNum]) || []; return ranges .map(range => { // Make a copy, so that the normalization below does not mess with // our map. range = Object.assign({}, 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); } } customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer); })();