Use the same comment data for the ranged comment layer as diff model
And further reduce the node observer complexity.
We are removing the `commentRanges` property from gr-diff.ts and the
rather complicated way of computing it in `updateRanges()`. Instead
in `processNodes()` we are calling `this.rangeLayer.updateRanges()`
with the same comment objects that are fed into the diff model.
We are also adding the comment metadata as event detail to
`comment-thread-mouseenter` and `comment-thread-mouseleave` events.
That allows us to get rid of `getThreadEl()` in gr-diff-highlight.
Release-Notes: skip
Change-Id: I3314a7a691f7500ffa5023c6b7279acc9e6f338d
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
index cfb64b9..c41dc91 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils.ts
@@ -163,8 +163,9 @@
}
/**
- * This is all the data that gr-diff extracts from comment thread elements.
- * Otherwise gr-diff treats such elements as a black box.
+ * This is all the data that gr-diff extracts from comment thread elements,
+ * see `GrDiffThreadElement`. Otherwise gr-diff treats such elements as a black
+ * box.
*/
export interface GrDiffCommentThread {
side: Side;
@@ -173,8 +174,12 @@
rootId?: string;
}
-export function toCommentThreadModel(
- threadEl: HTMLElement
+/**
+ * Retrieves all the data from a comment thread element that the gr-diff API
+ * contract defines for such elements.
+ */
+export function getDataFromCommentThreadEl(
+ threadEl?: EventTarget | null
): GrDiffCommentThread | undefined {
if (!isThreadEl(threadEl)) return undefined;
const side = getSide(threadEl);
@@ -300,14 +305,19 @@
// For Gerrit these are instances of GrCommentThread, but other gr-diff users
// have different HTML elements in use for comment threads.
// TODO: Also document the required HTML attributes that thread elements must
-// have, e.g. 'diff-side', 'range', 'line-num'.
+// have, e.g. 'diff-side', 'range' (optional), 'line-num'.
+// Comment widgets are also required to have `comment-thread` in their css
+// class list.
export interface GrDiffThreadElement extends HTMLElement {
rootId: string;
}
-export function isThreadEl(node: Node): node is GrDiffThreadElement {
+export function isThreadEl(
+ node?: Node | EventTarget | null
+): node is GrDiffThreadElement {
return (
- node.nodeType === Node.ELEMENT_NODE &&
+ !!node &&
+ (node as Node).nodeType === Node.ELEMENT_NODE &&
(node as Element).classList.contains('comment-thread')
);
}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
index 639b1ac..44f4f60 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-utils_test.ts
@@ -12,7 +12,7 @@
getRange,
computeKeyLocations,
GrDiffCommentThread,
- toCommentThreadModel,
+ getDataFromCommentThreadEl,
compareComments,
GrDiffThreadElement,
computeContext,
@@ -290,7 +290,7 @@
el.setAttribute('line-num', '3');
el.rootId = 'ab12';
- assert.deepEqual(toCommentThreadModel(el), {
+ assert.deepEqual(getDataFromCommentThreadEl(el), {
line: 3,
side: Side.LEFT,
range: undefined,
@@ -306,7 +306,7 @@
el.setAttribute('diff-side', 'left');
el.rootId = 'ab12';
- assert.deepEqual(toCommentThreadModel(el), {
+ assert.deepEqual(getDataFromCommentThreadEl(el), {
line: FILE,
side: Side.LEFT,
range: undefined,
@@ -318,11 +318,11 @@
const el = document.createElement(
'div'
) as unknown as GrDiffThreadElement;
- assert.isUndefined(toCommentThreadModel(el));
+ assert.isUndefined(getDataFromCommentThreadEl(el));
el.className = 'comment-thread';
- assert.isUndefined(toCommentThreadModel(el));
+ assert.isUndefined(getDataFromCommentThreadEl(el));
el.setAttribute('line-num', '3');
- assert.isUndefined(toCommentThreadModel(el));
+ assert.isUndefined(getDataFromCommentThreadEl(el));
});
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
index 0b37bd4..09dab26 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -17,19 +17,16 @@
import '../gr-diff-builder/gr-diff-row';
import {
getLineNumber,
- getRange,
- getSide,
- GrDiffThreadElement,
isThreadEl,
- rangesEqual,
getResponsiveMode,
isResponsive,
isNewDiff,
getSideByLineEl,
compareComments,
- toCommentThreadModel,
+ getDataFromCommentThreadEl,
FullContext,
DiffContextExpandedEventDetail,
+ GrDiffCommentThread,
} from '../gr-diff/gr-diff-utils';
import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common';
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
@@ -38,10 +35,7 @@
GrDiffHighlight,
} from '../gr-diff-highlight/gr-diff-highlight';
import {CoverageRange, DiffLayer, isDefined} from '../../../types/types';
-import {
- CommentRangeLayer,
- GrRangedCommentLayer,
-} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
+import {GrRangedCommentLayer} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
import {DiffViewMode, Side} from '../../../constants/constants';
import {fire, fireAlert} from '../../../utils/event-util';
import {MovedLinkClickedEvent, ValueChangedEvent} from '../../../types/events';
@@ -54,7 +48,7 @@
LineNumber,
ContentLoadNeededEventDetail,
} from '../../../api/diff';
-import {isHtmlElement, isSafari, toggleClass} from '../../../utils/dom-util';
+import {isSafari, toggleClass} from '../../../utils/dom-util';
import {assertIsDefined} from '../../../utils/common-util';
import {GrDiffSelection} from '../gr-diff-selection/gr-diff-selection';
import {property, query, state} from 'lit/decorators.js';
@@ -161,10 +155,6 @@
@property({type: Boolean})
noRenderOnPrefsChange?: boolean;
- // Private but used in tests.
- @state()
- commentRanges: CommentRangeLayer[] = [];
-
// explicitly highlight a range if it is not associated with any comment
@property({type: Object})
highlightRange?: CommentRange;
@@ -468,63 +458,22 @@
: document.getSelection();
}
- private updateRanges(
- addedThreadEls: GrDiffThreadElement[],
- removedThreadEls: GrDiffThreadElement[]
- ) {
- function commentRangeFromThreadEl(
- threadEl: GrDiffThreadElement
- ): CommentRangeLayer | undefined {
- const side = getSide(threadEl);
- if (!side) return undefined;
- const range = getRange(threadEl);
- if (!range) return undefined;
+ private commentThreadRedispatcher = (
+ target: EventTarget | null,
+ eventName: 'comment-thread-mouseenter' | 'comment-thread-mouseleave'
+ ) => {
+ if (!isThreadEl(target)) return;
+ const data = getDataFromCommentThreadEl(target);
+ if (data) fire(target, eventName, data);
+ };
- return {side, range, rootId: threadEl.rootId};
- }
+ private commentThreadEnterRedispatcher = (e: Event) => {
+ this.commentThreadRedispatcher(e.target, 'comment-thread-mouseenter');
+ };
- // TODO(brohlfs): Rewrite `.map().filter() as ...` with `.reduce()` instead.
- const addedCommentRanges = addedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(range => !!range) as CommentRangeLayer[];
- const removedCommentRanges = removedThreadEls
- .map(commentRangeFromThreadEl)
- .filter(range => !!range) as CommentRangeLayer[];
- for (const removedCommentRange of removedCommentRanges) {
- const i = this.commentRanges.findIndex(
- cr =>
- cr.side === removedCommentRange.side &&
- rangesEqual(cr.range, removedCommentRange.range)
- );
- this.commentRanges.splice(i, 1);
- }
-
- if (addedCommentRanges?.length) {
- this.commentRanges.push(...addedCommentRanges);
- }
- if (this.highlightRange) {
- this.commentRanges.push({
- side: Side.RIGHT,
- range: this.highlightRange,
- rootId: '',
- });
- }
-
- this.rangeLayer?.updateRanges(this.commentRanges);
- }
-
- // Dispatch events that are handled by the gr-diff-highlight.
- private redispatchHoverEvents(
- hoverEl: HTMLElement,
- threadEl: GrDiffThreadElement
- ) {
- hoverEl.addEventListener('mouseenter', () => {
- fire(threadEl, 'comment-thread-mouseenter', {});
- });
- hoverEl.addEventListener('mouseleave', () => {
- fire(threadEl, 'comment-thread-mouseleave', {});
- });
- }
+ private commentThreadLeaveRedispatcher = (e: Event) => {
+ this.commentThreadRedispatcher(e.target, 'comment-thread-mouseleave');
+ };
/** TODO: Can be removed when diff-old is gone. */
cancel() {}
@@ -739,39 +688,33 @@
* This must be called once, but only after diff lines are rendered. Otherwise
* `processNodes()` will fail to lookup the HTML elements that it wants to
* manipulate.
+ *
+ * TODO: Validate whether the above comment is still true. We don't look up
+ * elements anymore, and processing the nodes earlier might be beneficial
+ * performance wise.
*/
private observeNodes() {
if (this.nodeObserver) return;
- // First stop observing old nodes.
- // Then introduce a Mutation observer that watches for children being added
- // to gr-diff. If those children are `isThreadEl`, namely then they are
- // processed.
- this.nodeObserver = new MutationObserver(mutations => {
- const addedThreadEls = extractAddedNodes(mutations).filter(isThreadEl);
- const removedThreadEls =
- extractRemovedNodes(mutations).filter(isThreadEl);
- this.processNodes(addedThreadEls, removedThreadEls);
- });
+ // Watches children being added to gr-diff. We are expecting only comment
+ // widgets to be direct children.
+ this.nodeObserver = new MutationObserver(() => this.processNodes());
this.nodeObserver.observe(this, {childList: true});
- // Make sure to process existing gr-comment-threads that already exist.
- this.processNodes([...this.childNodes].filter(isThreadEl), []);
+ // Process existing comment widgets before the first observed change.
+ this.processNodes();
}
- private processNodes(
- addedThreadEls: GrDiffThreadElement[],
- removedThreadEls: GrDiffThreadElement[]
- ) {
- this.diffModel.updateState({
- comments: [...this.childNodes]
- .filter(isHtmlElement)
- .map(toCommentThreadModel)
- .filter(isDefined)
- .sort(compareComments),
- });
- this.updateRanges(addedThreadEls, removedThreadEls);
- addedThreadEls.forEach(threadEl =>
- this.redispatchHoverEvents(threadEl, threadEl)
- );
+ private processNodes() {
+ const threadEls = [...this.childNodes].filter(isThreadEl);
+ const comments = threadEls
+ .map(getDataFromCommentThreadEl)
+ .filter(isDefined)
+ .sort(compareComments);
+ this.diffModel.updateState({comments});
+ this.rangeLayer.updateRanges(comments);
+ for (const el of threadEls) {
+ el.addEventListener('mouseenter', this.commentThreadEnterRedispatcher);
+ el.addEventListener('mouseleave', this.commentThreadLeaveRedispatcher);
+ }
}
/** TODO: Can be removed when diff-old is gone. */
@@ -1070,14 +1013,6 @@
}
}
-function extractAddedNodes(mutations: MutationRecord[]) {
- return mutations.flatMap(mutation => [...mutation.addedNodes]);
-}
-
-function extractRemovedNodes(mutations: MutationRecord[]) {
- return mutations.flatMap(mutation => [...mutation.removedNodes]);
-}
-
function getLineNumberCellWidth(prefs: DiffPreferencesInfo) {
return prefs.font_size * 4;
}
@@ -1113,8 +1048,8 @@
'gr-diff': LitElement;
}
interface HTMLElementEventMap {
- 'comment-thread-mouseenter': CustomEvent<{}>;
- 'comment-thread-mouseleave': CustomEvent<{}>;
+ 'comment-thread-mouseenter': CustomEvent<GrDiffCommentThread>;
+ 'comment-thread-mouseleave': CustomEvent<GrDiffCommentThread>;
'loading-changed': ValueChangedEvent<boolean>;
'render-required': CustomEvent<{}>;
/**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
index d65aba8..917decc 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff_test.ts
@@ -319,35 +319,6 @@
1
);
});
-
- test('removes long range comment hint when comment is discarded', async () => {
- const range = {
- start_line: 1,
- end_line: 7,
- start_character: 0,
- end_character: 0,
- };
- const threadEl = document.createElement('div');
- threadEl.className = 'comment-thread';
- threadEl.setAttribute('diff-side', 'right');
- threadEl.setAttribute('line-num', '1');
- threadEl.setAttribute('range', JSON.stringify(range));
- threadEl.setAttribute('slot', 'right-1');
- const content = [
- {
- ab: Array(8).fill('text'),
- },
- ];
- await setupSampleDiff({content});
-
- element.appendChild(threadEl);
- await waitUntil(() => element.commentRanges.length === 1);
-
- threadEl.remove();
- await waitUntil(() => element.commentRanges.length === 0);
-
- assert.isEmpty(element.querySelectorAll('gr-ranged-comment-hint'));
- });
});
suite('blame', () => {