/** * @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'; const HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/; const SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/; const RANGE_HIGHLIGHT = 'range'; const HOVER_HIGHLIGHT = 'rangeHighlight'; const NORMALIZE_RANGE_EVENT = 'normalize-range'; Polymer({ is: 'gr-ranged-comment-layer', properties: { comments: Object, _listeners: { type: Array, value() { return []; }, }, _commentMap: { type: Object, value() { return {left: [], right: []}; }, }, }, observers: [ '_handleCommentChange(comments.*)', ], /** * Layer method to add annotations to a line. * @param {!HTMLElement} el The DIV.contentText element to apply the * annotation to. * @param {!Object} line The line object. (GrDiffLine) */ annotate(el, 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 comments by updating the comment maps and by * emitting appropriate update notifications. * @param {Object} record The change record. */ _handleCommentChange(record) { if (!record.path) { return; } // If the entire set of comments was changed. if (record.path === 'comments') { this._commentMap.left = this._computeCommentMap(this.comments.left); this._commentMap.right = this._computeCommentMap(this.comments.right); return; } // If the change only changed the `hovering` property of a comment. let match = record.path.match(HOVER_PATH_PATTERN); let side; if (match) { side = match[1]; const index = match[2]; const comment = this.comments[side][index]; if (comment && comment.range) { this._commentMap[side] = this._computeCommentMap(this.comments[side]); this._notifyUpdateRange( comment.range.start_line, comment.range.end_line, side); } return; } // If comments were spliced in or out. match = record.path.match(SPLICE_PATH_PATTERN); if (match) { side = match[1]; this._commentMap[side] = this._computeCommentMap(this.comments[side]); this._handleCommentSplice(record.value, side); } }, /** * Take a list of comments and return a sparse list mapping line numbers to * partial ranges. Uses an end-character-index of -1 to indicate the end of * the line. * @param {?} commentList The list of comments. * Getting this param to match closure requirements caused problems. * @return {!Object} The sparse list. */ _computeCommentMap(commentList) { const result = {}; for (const comment of commentList) { if (!comment.range) { continue; } const range = comment.range; for (let line = range.start_line; line <= range.end_line; line++) { if (!result[line]) { result[line] = []; } result[line].push({ comment, start: line === range.start_line ? range.start_character : 0, end: line === range.end_line ? range.end_character : -1, }); } } return result; }, /** * Translate a splice record into range update notifications. */ _handleCommentSplice(record, side) { if (!record || !record.indexSplices) { return; } for (const splice of record.indexSplices) { const ranges = splice.removed.length ? splice.removed.map(c => { return c.range; }) : [splice.object[splice.index].range]; for (const range of ranges) { if (!range) { continue; } this._notifyUpdateRange(range.start_line, range.end_line, side); } } }, _getRangesForLine(line, side) { const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; const ranges = this.get(['_commentMap', side, lineNum]) || []; return ranges .map(range => { range = { start: range.start, end: range.end === -1 ? line.text.length : range.end, hovering: !!range.comment.__hovering, }; // 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.$.reporting.reportInteraction(NORMALIZE_RANGE_EVENT, 'Modified invalid comment range on l.' + lineNum + ' of the ' + side + ' side'); } return range; }) // Sort the ranges so that hovering highlights are on top. .sort((a, b) => a.hovering && !b.hovering ? 1 : 0); }, }); })();