Merge "Cut dep from highlight onto comment model"

This commit is contained in:
Ole Rehmsen
2018-11-13 22:22:51 +00:00
committed by Gerrit Code Review
8 changed files with 235 additions and 219 deletions

View File

@@ -29,7 +29,7 @@ limitations under the License.
</div>
<gr-ranged-comment-layer
id="rangeLayer"
comments="[[comments]]"></gr-ranged-comment-layer>
comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer>
<gr-syntax-layer
id="syntaxLayer"
diff="[[diff]]"></gr-syntax-layer>
@@ -109,7 +109,6 @@ limitations under the License.
changeNum: String,
patchNum: String,
viewMode: String,
comments: Object,
isImageDiff: Boolean,
baseImage: Object,
revisionImage: Object,
@@ -125,6 +124,10 @@ limitations under the License.
_groups: Array,
_layers: Array,
_showTabs: Boolean,
/** @type {!Array<!Gerrit.HoveredRange>} */
commentRanges: {
type: Array,
},
},
get diffElement() {

View File

@@ -389,7 +389,7 @@ limitations under the License.
test('_handlePreferenceError called with invalid preference', () => {
sandbox.stub(element, '_handlePreferenceError');
const prefs = {tab_size: 0};
element._getDiffBuilder(element.diff, element.comments, prefs);
element._getDiffBuilder(element.diff, undefined, prefs);
assert.isTrue(element._handlePreferenceError.lastCall
.calledWithExactly('tab size'));
});

View File

@@ -21,7 +21,11 @@
is: 'gr-diff-highlight',
properties: {
comments: Object,
/** @type {!Array<!Gerrit.HoveredRange>} */
commentRanges: {
type: Array,
notify: true,
},
loggedIn: Boolean,
/**
* querySelector can return null, so needs to be nullable.
@@ -71,35 +75,44 @@
},
_handleCommentMouseOver(e) {
const comment = e.detail.comment;
if (!comment.range) { return; }
const lineEl = this.diffBuilder.getLineElByChild(e.target);
const side = this.diffBuilder.getSideByLineEl(lineEl);
const index = this._indexOfComment(side, comment);
const threadEl = Polymer.dom(e).localTarget;
const index = this._indexForThreadEl(threadEl);
if (index !== undefined) {
this.set(['comments', side, index, '__hovering'], true);
this.set(['commentRanges', index, 'hovering'], true);
}
},
_handleCommentMouseOut(e) {
const comment = e.detail.comment;
if (!comment.range) { return; }
const lineEl = this.diffBuilder.getLineElByChild(e.target);
const side = this.diffBuilder.getSideByLineEl(lineEl);
const index = this._indexOfComment(side, comment);
const threadEl = Polymer.dom(e).localTarget;
const index = this._indexForThreadEl(threadEl);
if (index !== undefined) {
this.set(['comments', side, index, '__hovering'], false);
this.set(['commentRanges', index, 'hovering'], false);
}
},
_indexOfComment(side, comment) {
const idProp = comment.id ? 'id' : '__draftID';
for (let i = 0; i < this.comments[side].length; i++) {
if (comment[idProp] &&
this.comments[side][i][idProp] === comment[idProp]) {
return i;
}
_indexForThreadEl(threadEl) {
const side = threadEl.getAttribute('comment-side');
const range = JSON.parse(threadEl.getAttribute('range'));
if (!range) return undefined;
return this._indexOfCommentRange(side, range);
},
_indexOfCommentRange(side, range) {
function rangesEqual(a, b) {
if (!a && !b) { return true; }
if (!a || !b) { return false; }
return a.start_line === b.start_line &&
a.start_character === b.start_character &&
a.end_line === b.end_line &&
a.end_character === b.end_character;
}
return this.commentRanges.findIndex(commentRange =>
commentRange.side === side && rangesEqual(commentRange.range, range));
},
/**

View File

@@ -205,7 +205,7 @@ limitations under the License.
test('comment-mouse-over from ranged comment causes set', () => {
sandbox.stub(element, 'set');
sandbox.stub(element, '_indexOfComment').returns(0);
sandbox.stub(element, '_indexForThreadEl').returns(0);
element.fire('comment-mouse-over', {comment: {range: {}}});
assert.isTrue(element.set.called);
});

View File

@@ -280,10 +280,10 @@ limitations under the License.
<gr-diff-highlight
id="highlights"
logged-in="[[loggedIn]]"
comments="{{comments}}">
comment-ranges="{{_commentRanges}}">
<gr-diff-builder
id="diffBuilder"
comments="[[comments]]"
comment-ranges="[[_commentRanges]]"
project-name="[[projectName]]"
diff="[[diff]]"
diff-path="[[path]]"

View File

@@ -43,6 +43,11 @@
* end_line: number, end_character: number}} */
Gerrit.Range;
function isThreadEl(node) {
return node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains('comment-thread');
}
Polymer({
is: 'gr-diff',
@@ -100,6 +105,11 @@
type: Object,
value: {left: [], right: []},
},
/** @type {!Array<!Gerrit.HoveredRange>} */
_commentRanges: {
type: Array,
value: [],
},
lineWrapping: {
type: Boolean,
value: false,
@@ -185,7 +195,19 @@
_diffLength: Number,
/** @type {?PolymerDomApi.ObserveHandle} */
/**
* Observes comment nodes added or removed after the initial render.
* Can be used to unregister when the entire diff is (re-)rendered or upon
* detachment.
* @type {?PolymerDomApi.ObserveHandle}
*/
_incrementalNodeObserver: Object,
/**
* Observes comment nodes added or removed at any point.
* Can be used to unregister upon detachment.
* @type {?PolymerDomApi.ObserveHandle}
*/
_nodeObserver: Object,
},
@@ -201,10 +223,36 @@
'render-content': '_handleRenderContent',
},
attached() {
this._updateRangesWhenNodesChange();
},
detached() {
this._unobserveIncrementalNodes();
this._unobserveNodes();
},
_updateRangesWhenNodesChange() {
function commentRangeFromThreadEl(threadEl) {
const side = threadEl.getAttribute('comment-side');
const range = JSON.parse(threadEl.getAttribute('range'));
return {side, range, hovering: false};
}
this._nodeObserver = Polymer.dom(this).observeNodes(info => {
const addedThreadEls = info.addedNodes.filter(isThreadEl);
const addedCommentRanges = addedThreadEls
.map(commentRangeFromThreadEl)
.filter(({range}) => range);
this.push('_commentRanges', ...addedCommentRanges);
// In principal we should also handle removed nodes, but I have not
// figured out how to do that yet without also catching all the removals
// caused by further redistribution. Right now, comments are never
// removed by no longer slotting them in, so I decided to not handle
// this situation until it occurs.
});
},
/** Cancel any remaining diff builder rendering work. */
cancel() {
this.$.diffBuilder.cancel();
@@ -577,7 +625,7 @@
},
_renderDiffTable() {
this._unobserveNodes();
this._unobserveIncrementalNodes();
if (!this.prefs) {
this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
return;
@@ -595,9 +643,8 @@
},
_handleRenderContent() {
this._nodeObserver = Polymer.dom(this).observeNodes(info => {
const addedThreadEls = info.addedNodes.filter(
node => node.nodeType === Node.ELEMENT_NODE);
this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => {
const addedThreadEls = info.addedNodes.filter(isThreadEl);
// In principal we should also handle removed nodes, but I have not
// figured out how to do that yet without also catching all the removals
// caused by further redistribution. Right now, comments are never
@@ -616,6 +663,12 @@
});
},
_unobserveIncrementalNodes() {
if (this._incrementalNodeObserver) {
Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver);
}
},
_unobserveNodes() {
if (this._nodeObserver) {
Polymer.dom(this).unobserveNodes(this._nodeObserver);
@@ -633,7 +686,7 @@
},
clearDiffContent() {
this._unobserveNodes();
this._unobserveIncrementalNodes();
this.$.diffTable.innerHTML = null;
},

View File

@@ -17,31 +17,34 @@
(function() {
'use strict';
const HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
const SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;
const HOVER_PATH_PATTERN = /^commentRanges\.\#(\d+)\.hovering$/;
const RANGE_HIGHLIGHT = 'range';
const HOVER_HIGHLIGHT = 'rangeHighlight';
const NORMALIZE_RANGE_EVENT = 'normalize-range';
/** @typedef {{side: string, range: Gerrit.Range, hovering: boolean}} */
Gerrit.HoveredRange;
Polymer({
is: 'gr-ranged-comment-layer',
properties: {
comments: Object,
/** @type {!Array<!Gerrit.HoveredRange>} */
commentRanges: Array,
_listeners: {
type: Array,
value() { return []; },
},
_commentMap: {
_rangesMap: {
type: Object,
value() { return {left: [], right: []}; },
value() { return {left: {}, right: {}}; },
},
},
observers: [
'_handleCommentChange(comments.*)',
'_handleCommentRangesChange(commentRanges.*)',
],
/**
@@ -93,97 +96,78 @@
},
/**
* Handle change in the comments by updating the comment maps and by
* Handle change in the ranges by updating the ranges maps and by
* emitting appropriate update notifications.
* @param {Object} record The change record.
*/
_handleCommentChange(record) {
if (!record.path) { return; }
_handleCommentRangesChange(record) {
if (!record) 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 (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.
let match = record.path.match(HOVER_PATH_PATTERN);
let side;
const match = record.path.match(HOVER_PATH_PATTERN);
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;
const commentRangesIndex = match[1];
const {side, range, hovering} = this.commentRanges[commentRangesIndex];
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.
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);
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});
});
}
}
}
},
/**
* 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);
}
_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(['_commentMap', side, lineNum]) || [];
const ranges = this.get(['_rangesMap', side, lineNum]) || [];
return ranges
.map(range => {
range = {
start: range.start,
end: range.end === -1 ? line.text.length : range.end,
hovering: !!range.comment.__hovering,
};
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.

View File

@@ -40,62 +40,48 @@ limitations under the License.
let sandbox;
setup(() => {
const initialComments = {
left: [
{
id: '12345',
line: 39,
message: 'range comment',
range: {
end_character: 9,
end_line: 39,
start_character: 6,
start_line: 36,
},
}, {
id: '23456',
line: 100,
message: 'non range comment',
const initialCommentRanges = [
{
side: 'left',
range: {
end_character: 9,
end_line: 39,
start_character: 6,
start_line: 36,
},
],
right: [
{
id: '34567',
line: 10,
message: 'range comment',
range: {
end_character: 22,
end_line: 12,
start_character: 10,
start_line: 10,
},
}, {
id: '45678',
line: 100,
message: 'single line range comment',
range: {
end_character: 15,
end_line: 100,
start_character: 5,
start_line: 100,
},
}, {
id: '8675309',
line: 55,
message: 'nonsense range',
range: {
end_character: 2,
end_line: 55,
start_character: 32,
start_line: 55,
},
},
{
side: 'right',
range: {
end_character: 22,
end_line: 12,
start_character: 10,
start_line: 10,
},
],
};
},
{
side: 'right',
range: {
end_character: 15,
end_line: 100,
start_character: 5,
start_line: 100,
},
},
{
side: 'right',
range: {
end_character: 2,
end_line: 55,
start_character: 32,
start_line: 55,
},
},
];
sandbox = sinon.sandbox.create();
element = fixture('basic');
element.comments = initialComments;
element.commentRanges = initialCommentRanges;
});
teardown(() => {
@@ -149,7 +135,7 @@ limitations under the License.
test('type=Remove has-comment hovering', () => {
line.type = GrDiffLine.Type.REMOVE;
line.beforeNumber = 36;
element.set(['comments', 'left', 0, '__hovering'], true);
element.set(['commentRanges', 0, 'hovering'], true);
const expectedStart = 6;
const expectedLength = line.text.length - expectedStart;
@@ -210,29 +196,18 @@ limitations under the License.
});
});
test('_handleCommentChange overwrite', () => {
const handlerSpy = sandbox.spy(element, '_handleCommentChange');
const mapSpy = sandbox.spy(element, '_computeCommentMap');
test('_handleCommentRangesChange overwrite', () => {
element.set('commentRanges', []);
element.set('comments', {left: [], right: []});
assert.isTrue(handlerSpy.called);
assert.equal(mapSpy.callCount, 2);
assert.equal(Object.keys(element._commentMap.left).length, 0);
assert.equal(Object.keys(element._commentMap.right).length, 0);
assert.equal(Object.keys(element._rangesMap.left).length, 0);
assert.equal(Object.keys(element._rangesMap.right).length, 0);
});
test('_handleCommentChange hovering', () => {
const handlerSpy = sandbox.spy(element, '_handleCommentChange');
const mapSpy = sandbox.spy(element, '_computeCommentMap');
test('_handleCommentRangesChange hovering', () => {
const notifyStub = sinon.stub();
element.addListener(notifyStub);
element.set(['comments', 'right', 0, '__hovering'], true);
assert.isTrue(handlerSpy.called);
assert.isTrue(mapSpy.called);
element.set(['commentRanges', 1, 'hovering'], true);
assert.isTrue(notifyStub.called);
const lastCall = notifyStub.lastCall;
@@ -241,16 +216,11 @@ limitations under the License.
assert.equal(lastCall.args[2], 'right');
});
test('_handleCommentChange splice out', () => {
const handlerSpy = sandbox.spy(element, '_handleCommentChange');
const mapSpy = sandbox.spy(element, '_computeCommentMap');
test('_handleCommentRangesChange splice out', () => {
const notifyStub = sinon.stub();
element.addListener(notifyStub);
element.splice('comments.right', 0, 1);
assert.isTrue(handlerSpy.called);
assert.isTrue(mapSpy.called);
element.splice('commentRanges', 1, 1);
assert.isTrue(notifyStub.called);
const lastCall = notifyStub.lastCall;
@@ -259,16 +229,12 @@ limitations under the License.
assert.equal(lastCall.args[2], 'right');
});
test('_handleCommentChange splice in', () => {
const handlerSpy = sandbox.spy(element, '_handleCommentChange');
const mapSpy = sandbox.spy(element, '_computeCommentMap');
test('_handleCommentRangesChange splice in', () => {
const notifyStub = sinon.stub();
element.addListener(notifyStub);
element.splice('comments.left', element.comments.left.length, 0, {
id: '56123',
line: 250,
message: 'new range comment',
element.splice('commentRanges', 1, 0, {
side: 'left',
range: {
end_character: 15,
end_line: 275,
@@ -277,9 +243,6 @@ limitations under the License.
},
});
assert.isTrue(handlerSpy.called);
assert.isTrue(mapSpy.called);
assert.isTrue(notifyStub.called);
const lastCall = notifyStub.lastCall;
assert.equal(lastCall.args[0], 250);
@@ -291,48 +254,48 @@ limitations under the License.
// There is only one ranged comment on the left, but it spans ll.36-39.
const leftKeys = [];
for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); }
assert.deepEqual(Object.keys(element._commentMap.left).sort(),
assert.deepEqual(Object.keys(element._rangesMap.left).sort(),
leftKeys.sort());
assert.equal(element._commentMap.left[36].length, 1);
assert.equal(element._commentMap.left[36][0].start, 6);
assert.equal(element._commentMap.left[36][0].end, -1);
assert.equal(element._rangesMap.left[36].length, 1);
assert.equal(element._rangesMap.left[36][0].start, 6);
assert.equal(element._rangesMap.left[36][0].end, -1);
assert.equal(element._commentMap.left[37].length, 1);
assert.equal(element._commentMap.left[37][0].start, 0);
assert.equal(element._commentMap.left[37][0].end, -1);
assert.equal(element._rangesMap.left[37].length, 1);
assert.equal(element._rangesMap.left[37][0].start, 0);
assert.equal(element._rangesMap.left[37][0].end, -1);
assert.equal(element._commentMap.left[38].length, 1);
assert.equal(element._commentMap.left[38][0].start, 0);
assert.equal(element._commentMap.left[38][0].end, -1);
assert.equal(element._rangesMap.left[38].length, 1);
assert.equal(element._rangesMap.left[38][0].start, 0);
assert.equal(element._rangesMap.left[38][0].end, -1);
assert.equal(element._commentMap.left[39].length, 1);
assert.equal(element._commentMap.left[39][0].start, 0);
assert.equal(element._commentMap.left[39][0].end, 9);
assert.equal(element._rangesMap.left[39].length, 1);
assert.equal(element._rangesMap.left[39][0].start, 0);
assert.equal(element._rangesMap.left[39][0].end, 9);
// The right has two ranged comments, one spanning ll.10-12 and the other
// on line 100.
const rightKeys = [];
for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); }
rightKeys.push('55', '100');
assert.deepEqual(Object.keys(element._commentMap.right).sort(),
assert.deepEqual(Object.keys(element._rangesMap.right).sort(),
rightKeys.sort());
assert.equal(element._commentMap.right[10].length, 1);
assert.equal(element._commentMap.right[10][0].start, 10);
assert.equal(element._commentMap.right[10][0].end, -1);
assert.equal(element._rangesMap.right[10].length, 1);
assert.equal(element._rangesMap.right[10][0].start, 10);
assert.equal(element._rangesMap.right[10][0].end, -1);
assert.equal(element._commentMap.right[11].length, 1);
assert.equal(element._commentMap.right[11][0].start, 0);
assert.equal(element._commentMap.right[11][0].end, -1);
assert.equal(element._rangesMap.right[11].length, 1);
assert.equal(element._rangesMap.right[11][0].start, 0);
assert.equal(element._rangesMap.right[11][0].end, -1);
assert.equal(element._commentMap.right[12].length, 1);
assert.equal(element._commentMap.right[12][0].start, 0);
assert.equal(element._commentMap.right[12][0].end, 22);
assert.equal(element._rangesMap.right[12].length, 1);
assert.equal(element._rangesMap.right[12][0].start, 0);
assert.equal(element._rangesMap.right[12][0].end, 22);
assert.equal(element._commentMap.right[100].length, 1);
assert.equal(element._commentMap.right[100][0].start, 5);
assert.equal(element._commentMap.right[100][0].end, 15);
assert.equal(element._rangesMap.right[100].length, 1);
assert.equal(element._rangesMap.right[100][0].start, 5);
assert.equal(element._rangesMap.right[100][0].end, 15);
});
test('_getRangesForLine normalizes invalid ranges', () => {