
Even before the refactorings, the comment and thread elements were also used from views outside of diff/. Now they are now completely independent from gr-diff and descendants, and can move into their own folder and drop the -diff from their name. The dependency left from diff/gr-diff-host onto comment[-thread], makes sense since gr-diff-host is the Gerrit wrapper for gr-diff with gr-comment[-thread]. Change-Id: I5076428da980198989edc605e5edc0e4d66529dd
848 lines
25 KiB
JavaScript
848 lines
25 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright (C) 2018 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 MSG_EMPTY_BLAME = 'No blame information for this diff.';
|
|
|
|
const EVENT_AGAINST_PARENT = 'diff-against-parent';
|
|
const EVENT_ZERO_REBASE = 'rebase-percent-zero';
|
|
const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero';
|
|
|
|
const DiffViewMode = {
|
|
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
|
|
UNIFIED: 'UNIFIED_DIFF',
|
|
};
|
|
|
|
const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE';
|
|
|
|
/**
|
|
* @param {Object} diff
|
|
* @return {boolean}
|
|
*/
|
|
function isImageDiff(diff) {
|
|
if (!diff) { return false; }
|
|
|
|
const isA = diff.meta_a &&
|
|
diff.meta_a.content_type.startsWith('image/');
|
|
const isB = diff.meta_b &&
|
|
diff.meta_b.content_type.startsWith('image/');
|
|
|
|
return !!(diff.binary && (isA || isB));
|
|
}
|
|
|
|
/**
|
|
* Compare two ranges. Either argument may be falsy, but will only return
|
|
* true if both are falsy or if neither are falsy and have the same position
|
|
* values.
|
|
*
|
|
* @param {Gerrit.Range=} a range 1
|
|
* @param {Gerrit.Range=} b range 2
|
|
* @return {boolean}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/** @enum {string} */
|
|
Gerrit.DiffSide = {
|
|
LEFT: 'left',
|
|
RIGHT: 'right',
|
|
};
|
|
|
|
/**
|
|
* Wrapper around gr-diff.
|
|
*
|
|
* Webcomponent fetching diffs and related data from restAPI and passing them
|
|
* to the presentational gr-diff for rendering.
|
|
*/
|
|
Polymer({
|
|
is: 'gr-diff-host',
|
|
|
|
/**
|
|
* Fired when the user selects a line.
|
|
* @event line-selected
|
|
*/
|
|
|
|
/**
|
|
* Fired if being logged in is required.
|
|
*
|
|
* @event show-auth-required
|
|
*/
|
|
|
|
/**
|
|
* Fired when a comment is saved or discarded
|
|
*
|
|
* @event diff-comments-modified
|
|
*/
|
|
|
|
properties: {
|
|
changeNum: String,
|
|
noAutoRender: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
/** @type {?} */
|
|
patchRange: Object,
|
|
path: String,
|
|
prefs: {
|
|
type: Object,
|
|
},
|
|
projectName: String,
|
|
displayLine: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
isImageDiff: {
|
|
type: Boolean,
|
|
computed: '_computeIsImageDiff(_diff)',
|
|
notify: true,
|
|
},
|
|
commitRange: Object,
|
|
filesWeblinks: {
|
|
type: Object,
|
|
value() { return {}; },
|
|
notify: true,
|
|
},
|
|
hidden: {
|
|
type: Boolean,
|
|
reflectToAttribute: true,
|
|
},
|
|
noRenderOnPrefsChange: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
comments: {
|
|
type: Object,
|
|
observer: '_commentsChanged',
|
|
},
|
|
lineWrapping: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
viewMode: {
|
|
type: String,
|
|
value: DiffViewMode.SIDE_BY_SIDE,
|
|
},
|
|
|
|
/**
|
|
* Special line number which should not be collapsed into a shared region.
|
|
* @type {{
|
|
* number: number,
|
|
* leftSide: {boolean}
|
|
* }|null}
|
|
*/
|
|
lineOfInterest: Object,
|
|
|
|
/**
|
|
* If the diff fails to load, show the failure message in the diff rather
|
|
* than bubbling the error up to the whole page. This is useful for when
|
|
* loading inline diffs because one diff failing need not mark the whole
|
|
* page with a failure.
|
|
*/
|
|
showLoadFailure: Boolean,
|
|
|
|
isBlameLoaded: {
|
|
type: Boolean,
|
|
notify: true,
|
|
computed: '_computeIsBlameLoaded(_blame)',
|
|
},
|
|
|
|
_loggedIn: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
_loading: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/** @type {?string} */
|
|
_errorMessage: {
|
|
type: String,
|
|
value: null,
|
|
},
|
|
|
|
/** @type {?Object} */
|
|
_baseImage: Object,
|
|
/** @type {?Object} */
|
|
_revisionImage: Object,
|
|
|
|
_diff: Object,
|
|
|
|
/** @type {?Object} */
|
|
_blame: {
|
|
type: Object,
|
|
value: null,
|
|
},
|
|
|
|
_loadedWhitespaceLevel: String,
|
|
|
|
_parentIndex: {
|
|
type: Number,
|
|
computed: '_computeParentIndex(patchRange.*)',
|
|
},
|
|
},
|
|
|
|
behaviors: [
|
|
Gerrit.PatchSetBehavior,
|
|
],
|
|
|
|
listeners: {
|
|
// These are named inconsistently for a reason:
|
|
// The create-comment event is fired to indicate that we should
|
|
// create a comment.
|
|
// The comment-* events are just notifying that the comments did already
|
|
// change in some way, and that we should update any models we may want
|
|
// to keep in sync.
|
|
'create-comment': '_handleCreateComment',
|
|
'comment-discard': '_handleCommentDiscard',
|
|
'comment-update': '_handleCommentUpdate',
|
|
'comment-save': '_handleCommentSave',
|
|
},
|
|
|
|
observers: [
|
|
'_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' +
|
|
' noRenderOnPrefsChange)',
|
|
],
|
|
|
|
ready() {
|
|
if (this._canReload()) {
|
|
this.reload();
|
|
}
|
|
},
|
|
|
|
attached() {
|
|
this._getLoggedIn().then(loggedIn => {
|
|
this._loggedIn = loggedIn;
|
|
});
|
|
},
|
|
|
|
/** @return {!Promise} */
|
|
reload() {
|
|
this._loading = true;
|
|
this._errorMessage = null;
|
|
const whitespaceLevel = this._getIgnoreWhitespace();
|
|
|
|
const diffRequest = this._getDiff()
|
|
.then(diff => {
|
|
this._loadedWhitespaceLevel = whitespaceLevel;
|
|
this._reportDiff(diff);
|
|
if (this._getIgnoreWhitespace() !== WHITESPACE_IGNORE_NONE) {
|
|
return this._translateChunksToIgnore(diff);
|
|
}
|
|
return diff;
|
|
})
|
|
.catch(e => {
|
|
this._handleGetDiffError(e);
|
|
return null;
|
|
});
|
|
|
|
const assetRequest = diffRequest.then(diff => {
|
|
// If the diff is null, then it's failed to load.
|
|
if (!diff) { return null; }
|
|
|
|
return this._loadDiffAssets(diff);
|
|
});
|
|
|
|
return Promise.all([diffRequest, assetRequest])
|
|
.then(results => {
|
|
const diff = results[0];
|
|
if (!diff) {
|
|
return Promise.resolve();
|
|
}
|
|
this.filesWeblinks = this._getFilesWeblinks(diff);
|
|
return new Promise(resolve => {
|
|
const callback = () => {
|
|
resolve();
|
|
this.removeEventListener('render', callback);
|
|
};
|
|
this.addEventListener('render', callback);
|
|
this._diff = diff;
|
|
});
|
|
})
|
|
.catch(err => {
|
|
console.warn('Error encountered loading diff:', err);
|
|
})
|
|
.then(() => { this._loading = false; });
|
|
},
|
|
|
|
_getFilesWeblinks(diff) {
|
|
if (!this.commitRange) { return {}; }
|
|
return {
|
|
meta_a: Gerrit.Nav.getFileWebLinks(
|
|
this.projectName, this.commitRange.baseCommit, this.path,
|
|
{weblinks: diff && diff.meta_a && diff.meta_a.web_links}),
|
|
meta_b: Gerrit.Nav.getFileWebLinks(
|
|
this.projectName, this.commitRange.commit, this.path,
|
|
{weblinks: diff && diff.meta_b && diff.meta_b.web_links}),
|
|
};
|
|
},
|
|
|
|
/** Cancel any remaining diff builder rendering work. */
|
|
cancel() {
|
|
this.$.diff.cancel();
|
|
},
|
|
|
|
/** @return {!Array<!HTMLElement>} */
|
|
getCursorStops() {
|
|
return this.$.diff.getCursorStops();
|
|
},
|
|
|
|
/** @return {boolean} */
|
|
isRangeSelected() {
|
|
return this.$.diff.isRangeSelected();
|
|
},
|
|
|
|
toggleLeftDiff() {
|
|
this.$.diff.toggleLeftDiff();
|
|
},
|
|
|
|
/**
|
|
* Load and display blame information for the base of the diff.
|
|
* @return {Promise} A promise that resolves when blame finishes rendering.
|
|
*/
|
|
loadBlame() {
|
|
return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum,
|
|
this.path, true)
|
|
.then(blame => {
|
|
if (!blame.length) {
|
|
this.fire('show-alert', {message: MSG_EMPTY_BLAME});
|
|
return Promise.reject(MSG_EMPTY_BLAME);
|
|
}
|
|
|
|
this._blame = blame;
|
|
});
|
|
},
|
|
|
|
/** Unload blame information for the diff. */
|
|
clearBlame() {
|
|
this._blame = null;
|
|
},
|
|
|
|
/**
|
|
* The thread elements in this diff, in no particular order.
|
|
* @return {!Array<!HTMLElement>}
|
|
*/
|
|
getThreadEls() {
|
|
return Polymer.dom(this.$.diff).querySelectorAll('.comment-thread');
|
|
},
|
|
|
|
/** @param {HTMLElement} el */
|
|
addDraftAtLine(el) {
|
|
this.$.diff.addDraftAtLine(el);
|
|
},
|
|
|
|
clearDiffContent() {
|
|
this.$.diff.clearDiffContent();
|
|
},
|
|
|
|
expandAllContext() {
|
|
this.$.diff.expandAllContext();
|
|
},
|
|
|
|
/** @return {!Promise} */
|
|
_getLoggedIn() {
|
|
return this.$.restAPI.getLoggedIn();
|
|
},
|
|
|
|
/** @return {boolean}} */
|
|
_canReload() {
|
|
return !!this.changeNum && !!this.patchRange && !!this.path &&
|
|
!this.noAutoRender;
|
|
},
|
|
|
|
/** @return {!Promise<!Object>} */
|
|
_getDiff() {
|
|
// Wrap the diff request in a new promise so that the error handler
|
|
// rejects the promise, allowing the error to be handled in the .catch.
|
|
return new Promise((resolve, reject) => {
|
|
this.$.restAPI.getDiff(
|
|
this.changeNum,
|
|
this.patchRange.basePatchNum,
|
|
this.patchRange.patchNum,
|
|
this.path,
|
|
this._getIgnoreWhitespace(),
|
|
reject)
|
|
.then(resolve);
|
|
});
|
|
},
|
|
|
|
_handleGetDiffError(response) {
|
|
// Loading the diff may respond with 409 if the file is too large. In this
|
|
// case, use a toast error..
|
|
if (response.status === 409) {
|
|
this.fire('server-error', {response});
|
|
return;
|
|
}
|
|
|
|
if (this.showLoadFailure) {
|
|
this._errorMessage = [
|
|
'Encountered error when loading the diff:',
|
|
response.status,
|
|
response.statusText,
|
|
].join(' ');
|
|
return;
|
|
}
|
|
|
|
this.fire('page-error', {response});
|
|
},
|
|
|
|
/**
|
|
* Report info about the diff response.
|
|
*/
|
|
_reportDiff(diff) {
|
|
if (!diff || !diff.content) { return; }
|
|
|
|
// Count the delta lines stemming from normal deltas, and from
|
|
// due_to_rebase deltas.
|
|
let nonRebaseDelta = 0;
|
|
let rebaseDelta = 0;
|
|
diff.content.forEach(chunk => {
|
|
if (chunk.ab) { return; }
|
|
const deltaSize = Math.max(
|
|
chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
|
|
if (chunk.due_to_rebase) {
|
|
rebaseDelta += deltaSize;
|
|
} else {
|
|
nonRebaseDelta += deltaSize;
|
|
}
|
|
});
|
|
|
|
// Find the percent of the delta from due_to_rebase chunks rounded to two
|
|
// digits. Diffs with no delta are considered 0%.
|
|
const totalDelta = rebaseDelta + nonRebaseDelta;
|
|
const percentRebaseDelta = !totalDelta ? 0 :
|
|
Math.round(100 * rebaseDelta / totalDelta);
|
|
|
|
// Report the due_to_rebase percentage in the "diff" category when
|
|
// applicable.
|
|
if (this.patchRange.basePatchNum === 'PARENT') {
|
|
this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
|
|
} else if (percentRebaseDelta === 0) {
|
|
this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
|
|
} else {
|
|
this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
|
|
percentRebaseDelta);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object} diff
|
|
* @return {!Promise}
|
|
*/
|
|
_loadDiffAssets(diff) {
|
|
if (isImageDiff(diff)) {
|
|
return this._getImages(diff).then(images => {
|
|
this._baseImage = images.baseImage;
|
|
this._revisionImage = images.revisionImage;
|
|
});
|
|
} else {
|
|
this._baseImage = null;
|
|
this._revisionImage = null;
|
|
return Promise.resolve();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object} diff
|
|
* @return {boolean}
|
|
*/
|
|
_computeIsImageDiff(diff) {
|
|
return isImageDiff(diff);
|
|
},
|
|
|
|
_commentsChanged(newComments) {
|
|
const allComments = [];
|
|
for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) {
|
|
// This is needed by the threading.
|
|
for (const comment of newComments[side]) {
|
|
comment.__commentSide = side;
|
|
}
|
|
allComments.push(...newComments[side]);
|
|
}
|
|
// Currently, the only way this is ever changed here is when the initial
|
|
// comments are loaded, so it's okay performance wise to clear the threads
|
|
// and recreate them. If this changes in future, we might want to reuse
|
|
// some DOM nodes here.
|
|
this._clearThreads();
|
|
const threads = this._createThreads(allComments);
|
|
for (const thread of threads) {
|
|
const threadEl = this._createThreadElement(thread);
|
|
this._attachThreadElement(threadEl);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {!Array<!Object>} comments
|
|
* @return {!Array<!Object>} Threads for the given comments.
|
|
*/
|
|
_createThreads(comments) {
|
|
const sortedComments = comments.slice(0).sort((a, b) => {
|
|
if (b.__draft && !a.__draft ) { return 0; }
|
|
if (a.__draft && !b.__draft ) { return 1; }
|
|
return util.parseDate(a.updated) - util.parseDate(b.updated);
|
|
});
|
|
|
|
const threads = [];
|
|
for (const comment of sortedComments) {
|
|
// If the comment is in reply to another comment, find that comment's
|
|
// thread and append to it.
|
|
if (comment.in_reply_to) {
|
|
const thread = threads.find(thread =>
|
|
thread.comments.some(c => c.id === comment.in_reply_to));
|
|
if (thread) {
|
|
thread.comments.push(comment);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Otherwise, this comment starts its own thread.
|
|
const newThread = {
|
|
start_datetime: comment.updated,
|
|
comments: [comment],
|
|
commentSide: comment.__commentSide,
|
|
patchNum: comment.patch_set,
|
|
rootId: comment.id || comment.__draftID,
|
|
lineNum: comment.line,
|
|
isOnParent: comment.side === 'PARENT',
|
|
};
|
|
if (comment.range) {
|
|
newThread.range = Object.assign({}, comment.range);
|
|
}
|
|
threads.push(newThread);
|
|
}
|
|
return threads;
|
|
},
|
|
|
|
/**
|
|
* @param {Object} blame
|
|
* @return {boolean}
|
|
*/
|
|
_computeIsBlameLoaded(blame) {
|
|
return !!blame;
|
|
},
|
|
|
|
/**
|
|
* @param {Object} diff
|
|
* @return {!Promise}
|
|
*/
|
|
_getImages(diff) {
|
|
return this.$.restAPI.getImagesForDiff(this.changeNum, diff,
|
|
this.patchRange);
|
|
},
|
|
|
|
/** @param {CustomEvent} e */
|
|
_handleCreateComment(e) {
|
|
const {lineNum, side, patchNum, isOnParent, range} = e.detail;
|
|
const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range,
|
|
isOnParent);
|
|
threadEl.addOrEditDraft(lineNum, range);
|
|
|
|
this.$.reporting.recordDraftInteraction();
|
|
},
|
|
|
|
/**
|
|
* Gets or creates a comment thread at a given location.
|
|
* May provide a range, to get/create a range comment.
|
|
*
|
|
* @param {string} patchNum
|
|
* @param {?number} lineNum
|
|
* @param {string} commentSide
|
|
* @param {Gerrit.Range|undefined} range
|
|
* @param {boolean} isOnParent
|
|
* @return {!Object}
|
|
*/
|
|
_getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) {
|
|
let threadEl = this._getThreadEl(lineNum, commentSide, range);
|
|
if (!threadEl) {
|
|
threadEl = this._createThreadElement({
|
|
comments: [],
|
|
commentSide,
|
|
patchNum,
|
|
lineNum,
|
|
range,
|
|
isOnParent,
|
|
});
|
|
this._attachThreadElement(threadEl);
|
|
}
|
|
return threadEl;
|
|
},
|
|
|
|
_attachThreadElement(threadEl) {
|
|
Polymer.dom(this.$.diff).appendChild(threadEl);
|
|
},
|
|
|
|
_clearThreads() {
|
|
for (const threadEl of this.getThreadEls()) {
|
|
const parent = Polymer.dom(threadEl).parentNode;
|
|
Polymer.dom(parent).removeChild(threadEl);
|
|
}
|
|
},
|
|
|
|
_createThreadElement(thread) {
|
|
const threadEl = document.createElement('gr-comment-thread');
|
|
threadEl.className = 'comment-thread';
|
|
threadEl.slot = `${thread.commentSide}-${thread.lineNum}`;
|
|
threadEl.comments = thread.comments;
|
|
threadEl.commentSide = thread.commentSide;
|
|
threadEl.isOnParent = !!thread.isOnParent;
|
|
threadEl.parentIndex = this._parentIndex;
|
|
threadEl.changeNum = this.changeNum;
|
|
threadEl.patchNum = thread.patchNum;
|
|
threadEl.lineNum = thread.lineNum;
|
|
const rootIdChangedListener = changeEvent => {
|
|
thread.rootId = changeEvent.detail.value;
|
|
};
|
|
threadEl.addEventListener('root-id-changed', rootIdChangedListener);
|
|
threadEl.path = this.path;
|
|
threadEl.projectName = this.projectName;
|
|
threadEl.range = thread.range;
|
|
const threadDiscardListener = e => {
|
|
const threadEl = /** @type {!Node} */ (e.currentTarget);
|
|
|
|
const parent = Polymer.dom(threadEl).parentNode;
|
|
Polymer.dom(parent).removeChild(threadEl);
|
|
|
|
threadEl.removeEventListener('root-id-changed', rootIdChangedListener);
|
|
threadEl.removeEventListener('thread-discard', threadDiscardListener);
|
|
};
|
|
threadEl.addEventListener('thread-discard', threadDiscardListener);
|
|
return threadEl;
|
|
},
|
|
|
|
/**
|
|
* Gets a comment thread element at a given location.
|
|
* May provide a range, to get a range comment.
|
|
*
|
|
* @param {?number} lineNum
|
|
* @param {string} commentSide
|
|
* @param {!Gerrit.Range=} range
|
|
* @return {?Node}
|
|
*/
|
|
_getThreadEl(lineNum, commentSide, range=undefined) {
|
|
let line;
|
|
if (commentSide === GrDiffBuilder.Side.LEFT) {
|
|
line = {beforeNumber: lineNum};
|
|
} else if (commentSide === GrDiffBuilder.Side.RIGHT) {
|
|
line = {afterNumber: lineNum};
|
|
} else {
|
|
throw new Error(`Unknown side: ${commentSide}`);
|
|
}
|
|
function matchesRange(threadEl) {
|
|
const threadRange = /** @type {!Gerrit.Range} */(
|
|
JSON.parse(threadEl.getAttribute('range')));
|
|
return rangesEqual(threadRange, range);
|
|
}
|
|
|
|
const filteredThreadEls = this._filterThreadElsForLocation(
|
|
this.getThreadEls(), line, commentSide).filter(matchesRange);
|
|
return filteredThreadEls.length ? filteredThreadEls[0] : null;
|
|
},
|
|
|
|
/**
|
|
* @param {!Array<!HTMLElement>} threadEls
|
|
* @param {!{beforeNumber: (number|string|undefined|null),
|
|
* afterNumber: (number|string|undefined|null)}}
|
|
* lineInfo
|
|
* @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for
|
|
* which to return the threads.
|
|
* @return {!Array<!HTMLElement>} The thread elements matching the given
|
|
* location.
|
|
*/
|
|
_filterThreadElsForLocation(threadEls, lineInfo, side) {
|
|
function matchesLeftLine(threadEl) {
|
|
return threadEl.getAttribute('comment-side') ==
|
|
Gerrit.DiffSide.LEFT &&
|
|
threadEl.getAttribute('line-num') == lineInfo.beforeNumber;
|
|
}
|
|
function matchesRightLine(threadEl) {
|
|
return threadEl.getAttribute('comment-side') ==
|
|
Gerrit.DiffSide.RIGHT &&
|
|
threadEl.getAttribute('line-num') == lineInfo.afterNumber;
|
|
}
|
|
function matchesFileComment(threadEl) {
|
|
return threadEl.getAttribute('comment-side') == side &&
|
|
// line/range comments have 1-based line set, if line is falsy it's
|
|
// a file comment
|
|
!threadEl.getAttribute('line-num');
|
|
}
|
|
|
|
// Select the appropriate matchers for the desired side and line
|
|
// If side is BOTH, we want both the left and right matcher.
|
|
const matchers = [];
|
|
if (side !== Gerrit.DiffSide.RIGHT) {
|
|
matchers.push(matchesLeftLine);
|
|
}
|
|
if (side !== Gerrit.DiffSide.LEFT) {
|
|
matchers.push(matchesRightLine);
|
|
}
|
|
if (lineInfo.afterNumber === 'FILE' ||
|
|
lineInfo.beforeNumber === 'FILE') {
|
|
matchers.push(matchesFileComment);
|
|
}
|
|
return threadEls.filter(threadEl =>
|
|
matchers.some(matcher => matcher(threadEl)));
|
|
},
|
|
|
|
/**
|
|
* Take a diff that was loaded with a ignore-whitespace other than
|
|
* IGNORE_NONE, and convert delta chunks labeled as common into shared
|
|
* chunks.
|
|
* @param {!Object} diff
|
|
* @returns {!Object}
|
|
*/
|
|
_translateChunksToIgnore(diff) {
|
|
const newDiff = Object.assign({}, diff);
|
|
const mergedContent = [];
|
|
|
|
// Was the last chunk visited a shared chunk?
|
|
let lastWasShared = false;
|
|
|
|
for (const chunk of diff.content) {
|
|
if (lastWasShared && chunk.common && chunk.b) {
|
|
// The last chunk was shared and this chunk should be ignored, so
|
|
// add its revision content to the previous chunk.
|
|
mergedContent[mergedContent.length - 1].ab.push(...chunk.b);
|
|
} else if (chunk.common && !chunk.b) {
|
|
// If the chunk should be ignored, but it doesn't have revision
|
|
// content, then drop it and continue without updating lastWasShared.
|
|
continue;
|
|
} else if (lastWasShared && chunk.ab) {
|
|
// Both the last chunk and the current chunk are shared. Merge this
|
|
// chunk's shared content into the previous shared content.
|
|
mergedContent[mergedContent.length - 1].ab.push(...chunk.ab);
|
|
} else if (!lastWasShared && chunk.common && chunk.b) {
|
|
// If the previous chunk was not shared, but this one should be
|
|
// ignored, then add it as a shared chunk.
|
|
mergedContent.push({ab: chunk.b});
|
|
} else {
|
|
// Otherwise add the chunk as is.
|
|
mergedContent.push(chunk);
|
|
}
|
|
|
|
lastWasShared = !!mergedContent[mergedContent.length - 1].ab;
|
|
}
|
|
|
|
newDiff.content = mergedContent;
|
|
return newDiff;
|
|
},
|
|
|
|
_getIgnoreWhitespace() {
|
|
if (!this.prefs || !this.prefs.ignore_whitespace) {
|
|
return WHITESPACE_IGNORE_NONE;
|
|
}
|
|
return this.prefs.ignore_whitespace;
|
|
},
|
|
|
|
_whitespaceChanged(preferredWhitespaceLevel, loadedWhitespaceLevel,
|
|
noRenderOnPrefsChange) {
|
|
if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
|
|
!noRenderOnPrefsChange) {
|
|
this.reload();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {Object} patchRangeRecord
|
|
* @return {number|null}
|
|
*/
|
|
_computeParentIndex(patchRangeRecord) {
|
|
return this.isMergeParent(patchRangeRecord.base.basePatchNum) ?
|
|
this.getParentIndex(patchRangeRecord.base.basePatchNum) : null;
|
|
},
|
|
|
|
_handleCommentSave(e) {
|
|
const comment = e.detail.comment;
|
|
const side = e.detail.comment.__commentSide;
|
|
const idx = this._findDraftIndex(comment, side);
|
|
this.set(['comments', side, idx], comment);
|
|
this._handleCommentSaveOrDiscard();
|
|
},
|
|
|
|
_handleCommentDiscard(e) {
|
|
const comment = e.detail.comment;
|
|
this._removeComment(comment);
|
|
this._handleCommentSaveOrDiscard();
|
|
},
|
|
|
|
/**
|
|
* Closure annotation for Polymer.prototype.push is off. Submitted PR:
|
|
* https://github.com/Polymer/polymer/pull/4776
|
|
* but for not supressing annotations.
|
|
*
|
|
* @suppress {checkTypes}
|
|
*/
|
|
_handleCommentUpdate(e) {
|
|
const comment = e.detail.comment;
|
|
const side = e.detail.comment.__commentSide;
|
|
let idx = this._findCommentIndex(comment, side);
|
|
if (idx === -1) {
|
|
idx = this._findDraftIndex(comment, side);
|
|
}
|
|
if (idx !== -1) { // Update draft or comment.
|
|
this.set(['comments', side, idx], comment);
|
|
} else { // Create new draft.
|
|
this.push(['comments', side], comment);
|
|
}
|
|
},
|
|
|
|
_handleCommentSaveOrDiscard() {
|
|
this.dispatchEvent(new CustomEvent('diff-comments-modified',
|
|
{bubbles: true}));
|
|
},
|
|
|
|
_removeComment(comment) {
|
|
const side = comment.__commentSide;
|
|
this._removeCommentFromSide(comment, side);
|
|
},
|
|
|
|
_removeCommentFromSide(comment, side) {
|
|
let idx = this._findCommentIndex(comment, side);
|
|
if (idx === -1) {
|
|
idx = this._findDraftIndex(comment, side);
|
|
}
|
|
if (idx !== -1) {
|
|
this.splice('comments.' + side, idx, 1);
|
|
}
|
|
},
|
|
|
|
/** @return {number} */
|
|
_findCommentIndex(comment, side) {
|
|
if (!comment.id || !this.comments[side]) {
|
|
return -1;
|
|
}
|
|
return this.comments[side].findIndex(item => item.id === comment.id);
|
|
},
|
|
|
|
/** @return {number} */
|
|
_findDraftIndex(comment, side) {
|
|
if (!comment.__draftID || !this.comments[side]) {
|
|
return -1;
|
|
}
|
|
return this.comments[side].findIndex(
|
|
item => item.__draftID === comment.__draftID);
|
|
},
|
|
});
|
|
})();
|