blob: 3de18bee687fe67ac2bceb1eaabbd7a97f3f1d2e [file] [log] [blame]
// 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);
},
});
})();