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-old/gr-diff/gr-diff.ts b/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff.ts index 0da3522..45a8784 100644 --- a/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff.ts +++ b/polygerrit-ui/app/embed/diff-old/gr-diff/gr-diff.ts
@@ -25,6 +25,8 @@ getResponsiveMode, isResponsive, isNewDiff, + getDataFromCommentThreadEl, + GrDiffCommentThread, } from '../../diff/gr-diff/gr-diff-utils'; import {BlameInfo, CommentRange, ImageInfo} from '../../../types/common'; import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff'; @@ -564,10 +566,12 @@ threadEl: GrDiffThreadElement ) { hoverEl.addEventListener('mouseenter', () => { - fire(threadEl, 'comment-thread-mouseenter', {}); + const data = getDataFromCommentThreadEl(threadEl); + if (data) fire(threadEl, 'comment-thread-mouseenter', data); }); hoverEl.addEventListener('mouseleave', () => { - fire(threadEl, 'comment-thread-mouseleave', {}); + const data = getDataFromCommentThreadEl(threadEl); + if (data) fire(threadEl, 'comment-thread-mouseleave', data); }); } @@ -1119,8 +1123,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-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts index b332b56..266157a 100644 --- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts +++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -15,7 +15,7 @@ getLineElByChild, getLineNumberByChild, getSideByLineEl, - GrDiffThreadElement, + GrDiffCommentThread, } from '../gr-diff/gr-diff-utils'; import {debounce, DelayedTask} from '../../../utils/async-util'; import {assertIsDefined, queryAndAssert} from '../../../utils/common-util'; @@ -134,48 +134,31 @@ ); } - private getThreadEl(e: Event): GrDiffThreadElement | null { - for (const pathEl of e.composedPath()) { - if ( - pathEl instanceof HTMLElement && - pathEl.classList.contains('comment-thread') - ) { - return pathEl as GrDiffThreadElement; - } - } - return null; - } - private toggleRangeElHighlight( - threadEl: GrDiffThreadElement | null, + thread: GrDiffCommentThread, highlightRange = false ) { - const rootId = threadEl?.rootId; + const rootId = thread?.rootId; if (!rootId) return; if (!this.diffTable) return; - if (highlightRange) { - const selector = `.range.${strToClassName(rootId)}`; - const rangeNodes = this.diffTable.querySelectorAll(selector); - rangeNodes.forEach(rangeNode => { - rangeNode.classList.add('rangeHoverHighlight'); - }); - } else { - const selector = `.rangeHoverHighlight.${strToClassName(rootId)}`; - const rangeNodes = this.diffTable.querySelectorAll(selector); - rangeNodes.forEach(rangeNode => { - rangeNode.classList.remove('rangeHoverHighlight'); - }); + const highlightClass = highlightRange ? 'range' : 'rangeHoverHighlight'; + const selector = `.${highlightClass}.${strToClassName(rootId)}`; + const rangeNodes = this.diffTable.querySelectorAll(selector); + for (const rangeNode of rangeNodes) { + rangeNode.classList.toggle('rangeHoverHighlight', highlightRange); } } - private handleCommentThreadMouseenter = (e: Event) => { - const threadEl = this.getThreadEl(e); - this.toggleRangeElHighlight(threadEl, /* highlightRange= */ true); + private handleCommentThreadMouseenter = ( + e: CustomEvent<GrDiffCommentThread> + ) => { + this.toggleRangeElHighlight(e.detail, /* highlightRange= */ true); }; - private handleCommentThreadMouseleave = (e: Event) => { - const threadEl = this.getThreadEl(e); - this.toggleRangeElHighlight(threadEl, /* highlightRange= */ false); + private handleCommentThreadMouseleave = ( + e: CustomEvent<GrDiffCommentThread> + ) => { + this.toggleRangeElHighlight(e.detail, /* highlightRange= */ false); }; /**
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts index f04e6a2..3edd825 100644 --- a/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts +++ b/polygerrit-ui/app/embed/diff/gr-diff-highlight/gr-diff-highlight_test.ts
@@ -15,7 +15,10 @@ import {Side} from '../../../api/diff'; import {SinonStubbedMember} from 'sinon'; import {queryAndAssert} from '../../../utils/common-util'; -import {GrDiffThreadElement} from '../gr-diff/gr-diff-utils'; +import { + GrDiffThreadElement, + getDataFromCommentThreadEl, +} from '../gr-diff/gr-diff-utils'; import { stubElement, waitQueryAndAssert, @@ -152,6 +155,8 @@ ) as unknown as GrDiffThreadElement; threadEl.className = 'comment-thread'; threadEl.rootId = 'id314'; + threadEl.setAttribute('line-num', '12'); + threadEl.setAttribute('diff-side', 'right'); diff.appendChild(threadEl); }); @@ -164,6 +169,7 @@ assert.isFalse(hlRange.classList.contains('rangeHoverHighlight')); threadEl.dispatchEvent( new CustomEvent('comment-thread-mouseenter', { + detail: getDataFromCommentThreadEl(threadEl), bubbles: true, composed: true, }) @@ -176,6 +182,7 @@ hlRange.classList.add('rangeHoverHighlight'); threadEl.dispatchEvent( new CustomEvent('comment-thread-mouseleave', { + detail: getDataFromCommentThreadEl(threadEl), bubbles: true, composed: true, })
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', () => {
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts index 24729ff9..98acc5a 100644 --- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts +++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.ts
@@ -9,20 +9,22 @@ import {Side} from '../../../constants/constants'; import {CommentRange} from '../../../types/common'; import {DiffLayer, DiffLayerListener} from '../../../types/types'; -import {isLongCommentRange} from '../gr-diff/gr-diff-utils'; +import { + GrDiffCommentThread, + isLongCommentRange, +} from '../gr-diff/gr-diff-utils'; import {GrDiffLineType} from '../../../api/diff'; -/** - * Enhanced CommentRange by UI state. Interface for incoming ranges set from the - * outside. - * - * TODO(TS): Unify with what is used in gr-diff when these objects are created. - */ -export interface CommentRangeLayer { +/** Force `range` to be set for the objects that we are working with. */ +export interface CommentRangeLayer extends Partial<GrDiffCommentThread> { side: Side; range: CommentRange; - // New drafts don't have a rootId. - rootId?: string; +} + +function isCommentRangeLayer<T extends Partial<GrDiffCommentThread>>( + x: T | CommentRangeLayer | undefined +): x is CommentRangeLayer { + return !!x && !!(x as CommentRangeLayer).range; } /** Can be used for array functions like `some()`. */ @@ -124,7 +126,9 @@ } } - updateRanges(newRanges: CommentRangeLayer[]) { + updateRanges(rawRanges: Partial<GrDiffCommentThread>[]) { + const newRanges: CommentRangeLayer[] = + rawRanges.filter(isCommentRangeLayer); for (const newRange of newRanges) { if (this.knownRanges.some(equals(newRange))) continue; this.addRange(newRange);
diff --git a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts index 5bfd94d..4de6d01 100644 --- a/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts +++ b/polygerrit-ui/app/embed/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.ts
@@ -18,6 +18,7 @@ const rangeA: CommentRangeLayer = { side: Side.LEFT, + line: 39, range: { end_character: 9, end_line: 39, @@ -29,6 +30,7 @@ const rangeB: CommentRangeLayer = { side: Side.RIGHT, + line: 12, range: { end_character: 22, end_line: 12, @@ -40,6 +42,7 @@ const rangeC: CommentRangeLayer = { side: Side.RIGHT, + line: 100, range: { end_character: 15, end_line: 100, @@ -50,6 +53,7 @@ const rangeD: CommentRangeLayer = { side: Side.RIGHT, + line: 55, range: { end_character: 2, end_line: 55, @@ -61,6 +65,7 @@ const rangeE: CommentRangeLayer = { side: Side.RIGHT, + line: 71, range: { end_character: 1, end_line: 71, @@ -71,6 +76,7 @@ const rangeF: CommentRangeLayer = { side: Side.RIGHT, + line: 24, range: { end_character: 0, end_line: 24,