Wyatt Allen 6a96c8b248 Normalize some invalid comment ranges
Formerly, nonsensical comment ranges would prevent comments from
appearing in diffs. With this change, ranges are normalized by the
ranged comment layer so that they can be translated into valid
annotations.

See also
* Ibcab6e537abe8b81e764b09982a2581ae81463f8 should reject such ranges.
* I019e00063ab5c45c99379f3f9fb74eda0408d63f should avoid this error when
  constructing comment emails.

Bug: Issue 5744
Change-Id: Ib5834017f81f81a877b32264ee57d72302911a6c
2017-03-10 12:21:03 -08:00

195 lines
6.6 KiB
JavaScript

// 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';
var HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
var SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;
var RANGE_HIGHLIGHT = 'range';
var HOVER_HIGHLIGHT = 'rangeHighlight';
Polymer({
is: 'gr-ranged-comment-layer',
properties: {
comments: Object,
_listeners: {
type: Array,
value: function() { return []; },
},
_commentMap: {
type: Object,
value: function() { 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 {GrDiffLine} line The line object.
*/
annotate: function(el, line) {
var 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'));
}
ranges.forEach(function(range) {
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: function(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: function(start, end, side) {
this._listeners.forEach(function(listener) {
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: function(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.
var match = record.path.match(HOVER_PATH_PATTERN);
if (match) {
var side = match[1];
var index = match[2];
var 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) {
var 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 {Array<Object>} commentList The list of comments.
* @return {Object} The sparse list.
*/
_computeCommentMap: function(commentList) {
var result = {};
commentList.forEach(function(comment) {
if (!comment.range) { return; }
var range = comment.range;
for (var line = range.start_line; line <= range.end_line; line++) {
if (!result[line]) { result[line] = []; }
result[line].push({
comment: 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: function(record, side) {
if (!record || !record.indexSplices) { return; }
record.indexSplices.forEach(function(splice) {
var ranges = splice.removed.length ?
splice.removed.map(function(c) { return c.range; }) :
[splice.object[splice.index].range];
ranges.forEach(function(range) {
if (!range) { return; }
this._notifyUpdateRange(range.start_line, range.end_line, side);
}.bind(this));
}.bind(this));
},
_getRangesForLine: function(line, side) {
var lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
var ranges = this.get(['_commentMap', side, lineNum]) || [];
return ranges
.map(function(range) {
var 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;
}
return range;
})
.sort(function(a, b) {
// Sort the ranges so that hovering highlights are on top.
return a.hovering && !b.hovering ? 1 : 0;
});
},
});
})();