Merge "Add gr-focus-layer to gr-diff"
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
index 22a7694..98a2093 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-styles.ts
@@ -367,10 +367,16 @@
gr-diff-row td.sign.add.no-intraline-info,
gr-diff-section tbody.delta.total gr-diff-row td.content.add div.contentText {
background-color: var(--dark-add-highlight-color);
+ &:has(.is-out-of-focus-range) {
+ background-color: transparent;
+ }
}
gr-diff-row td.content.add div.contentText,
gr-diff-row td.sign.add {
background-color: var(--light-add-highlight-color);
+ &:has(.is-out-of-focus-range) {
+ background-color: transparent;
+ }
}
/* If there are no intraline info, consider everything changed */
gr-diff-row td.content.remove div.contentText .intraline,
@@ -382,10 +388,16 @@
div.contentText,
gr-diff-row td.sign.remove.no-intraline-info {
background-color: var(--dark-remove-highlight-color);
+ &:has(.is-out-of-focus-range) {
+ background-color: transparent;
+ }
}
gr-diff-row td.content.remove div.contentText,
gr-diff-row td.sign.remove {
background-color: var(--light-remove-highlight-color);
+ &:has(.is-out-of-focus-range) {
+ background-color: transparent;
+ }
}
gr-diff-element table.responsive gr-diff-row td.content div.contentText {
white-space: break-spaces;
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 f3534c2..59dad46 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff.ts
@@ -75,6 +75,7 @@
grDiffTextStyles,
} from './gr-diff-styles';
import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
+import {GrFocusLayer} from '../gr-focus-layer/gr-focus-layer';
import {
GrAnnotationImpl,
getStringLength,
@@ -275,6 +276,8 @@
private coverageLayerRight = new GrCoverageLayer(Side.RIGHT);
+ private focusLayer = new GrFocusLayer();
+
private rangeLayer = new GrRangedCommentLayer();
@state() groups: GrDiffGroup[] = [];
@@ -440,6 +443,9 @@
if (changedProperties.has('lineOfInterest')) {
this.lineOfInterestChanged();
}
+ if (changedProperties.has('diffRangesToFocus')) {
+ this.updateFocusRanges(this.diffRangesToFocus);
+ }
}
protected override async getUpdateComplete(): Promise<boolean> {
@@ -720,6 +726,10 @@
this.coverageLayerRight.setRanges(rs.filter(r => r?.side === Side.RIGHT));
}
+ private updateFocusRanges(rs?: DiffRangesToFocus) {
+ this.focusLayer.setRanges(rs);
+ }
+
private onDiffContextExpanded = (
e: CustomEvent<DiffContextExpandedEventDetail>
) => {
@@ -746,6 +756,7 @@
this.rangeLayer,
this.coverageLayerLeft,
this.coverageLayerRight,
+ this.focusLayer,
];
this.layersChanged();
}
diff --git a/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer.ts b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer.ts
new file mode 100644
index 0000000..b3d780e
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer.ts
@@ -0,0 +1,151 @@
+/**
+ * @license
+ * Copyright 2024 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {DiffRangesToFocus, GrDiffLine, Side} from '../../../api/diff';
+import {DiffLayer, DiffLayerListener} from '../../../types/types';
+
+// A range of lines in a diff.
+export type Range = {
+ start: number;
+ end: number;
+};
+
+export class GrFocusLayer implements DiffLayer {
+ private diffRangesToFocus?: DiffRangesToFocus;
+
+ /**
+ * Diff Ranges which were unfocused(colors are saturated) in previous call.
+ */
+ private previousUnfocusedRanges?: DiffRangesToFocus;
+
+ /**
+ * Has any line been annotated already in the lifetime of this layer?
+ * If not, then `setRanges()` does not have to call `notify()` and thus
+ * trigger re-rendering of the affected diff rows.
+ */
+ // visible for testing
+ annotated = false;
+
+ private listeners: DiffLayerListener[] = [];
+
+ addListener(listener: DiffLayerListener) {
+ this.listeners.push(listener);
+ }
+
+ removeListener(listener: DiffLayerListener) {
+ this.listeners = this.listeners.filter(f => f !== listener);
+ }
+
+ setRanges(diffRangesToFocus?: DiffRangesToFocus) {
+ if (!this.previousUnfocusedRanges && !diffRangesToFocus) return;
+ this.diffRangesToFocus = diffRangesToFocus;
+
+ // If ranges are set before any diff row was rendered, then great, no need
+ // to notify and re-render.
+ if (this.annotated) {
+ this.notify({
+ left: [
+ ...(this.previousUnfocusedRanges?.left ?? []),
+ ...(diffRangesToFocus?.left ?? []),
+ ],
+ right: [
+ ...(this.previousUnfocusedRanges?.right ?? []),
+ ...(diffRangesToFocus?.right ?? []),
+ ],
+ });
+ }
+ this.previousUnfocusedRanges = undefined;
+ }
+
+ private notify(ranges: DiffRangesToFocus) {
+ for (const r of ranges.left) {
+ for (const l of this.listeners) l(r.start, r.end, Side.LEFT);
+ }
+ for (const r of ranges.right) {
+ for (const l of this.listeners) l(r.start, r.end, Side.RIGHT);
+ }
+ }
+
+ /**
+ * Layer method to add is-out-of-focus-range to a textElement
+ * if line is out of focus.
+ *
+ * @param textEl The gr-text element for this line.
+ * @param lineNumberEl The <td> element with the line number.
+ * @param _line Not used for this layer. (unused parameter)
+ * @param side The side of the diff.
+ */
+ annotate(
+ textEl: HTMLElement,
+ lineNumberEl: HTMLElement,
+ _line: GrDiffLine,
+ side: Side
+ ) {
+ if (!lineNumberEl || !textEl || !this.diffRangesToFocus) {
+ return;
+ }
+ let elementLineNumber = -1;
+ const dataValue = lineNumberEl.getAttribute('data-value');
+ if (dataValue) {
+ elementLineNumber = Number(dataValue);
+ }
+ if (!elementLineNumber || elementLineNumber < 1) return;
+
+ let focusedRanges: Range[] = [];
+ if (side === Side.LEFT) {
+ focusedRanges = this.diffRangesToFocus.left;
+ } else if (side === Side.RIGHT) {
+ focusedRanges = this.diffRangesToFocus.right;
+ }
+ // TODO(anuragpathak): Optimize this using the same approach as gr-coverage-layer.ts
+ if (
+ !focusedRanges.some(
+ range =>
+ elementLineNumber >= range.start && elementLineNumber <= range.end
+ )
+ ) {
+ textEl.classList.add('is-out-of-focus-range');
+ this.updateUnfocusedRanges(elementLineNumber, side);
+ }
+ }
+
+ private updateUnfocusedRanges(lineNumber: number, side: Side) {
+ this.previousUnfocusedRanges = {
+ left:
+ side === Side.LEFT
+ ? this.addToRange(lineNumber, this.previousUnfocusedRanges?.left)
+ : this.previousUnfocusedRanges?.left ?? [],
+ right:
+ side === Side.RIGHT
+ ? this.addToRange(lineNumber, this.previousUnfocusedRanges?.right)
+ : this.previousUnfocusedRanges?.right ?? [],
+ };
+ }
+
+ private addToRange(lineNumber: number, ranges?: Range[]) {
+ const previousRange: Range[] = [];
+ if (ranges) {
+ previousRange.push(...ranges);
+ }
+ let lastEntryInRange = previousRange.pop();
+ if (lastEntryInRange) {
+ if (lastEntryInRange.end + 1 === lineNumber) {
+ lastEntryInRange = {start: lastEntryInRange.start, end: lineNumber};
+ previousRange.push(lastEntryInRange);
+ } else {
+ previousRange.push(lastEntryInRange, {
+ start: lineNumber,
+ end: lineNumber,
+ });
+ }
+ } else {
+ previousRange.push({
+ start: lineNumber,
+ end: lineNumber,
+ });
+ }
+ return previousRange;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts
new file mode 100644
index 0000000..fc61d21
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-focus-layer/gr-focus-layer_test.ts
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Copyright 2024 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+
+import {assert} from '@open-wc/testing';
+import {SinonStub} from 'sinon';
+
+import {DiffRangesToFocus, Side, GrDiffLineType} from '../../../api/diff';
+
+import {GrFocusLayer} from './gr-focus-layer';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+
+const ONE_RANGE: DiffRangesToFocus = {
+ left: [{start: 1, end: 2}],
+ right: [{start: 9, end: 10}],
+};
+
+const RANGES: DiffRangesToFocus = {
+ left: [
+ {start: 1, end: 2},
+ {start: 13, end: 14},
+ {start: 25, end: 76},
+ ],
+ right: [
+ {start: 1, end: 2},
+ {start: 23, end: 24},
+ {start: 55, end: 65},
+ ],
+};
+
+suite('gr-focus-layer', () => {
+ let layer: GrFocusLayer;
+ const line = new GrDiffLine(GrDiffLineType.ADD);
+
+ function createLineElement(lineNumber: number, side: Side) {
+ const lineNumberEl = document.createElement('div');
+ lineNumberEl.setAttribute('data-side', side);
+ lineNumberEl.setAttribute('data-value', lineNumber.toString());
+ lineNumberEl.className = side;
+ return lineNumberEl;
+ }
+
+ function createTextElement() {
+ const textElement = document.createElement('div');
+ textElement.innerText = 'A line of code';
+ return textElement;
+ }
+
+ suite('setRanges and notify', () => {
+ let listener: SinonStub;
+
+ setup(() => {
+ layer = new GrFocusLayer();
+ listener = sinon.stub();
+ layer.addListener(listener);
+ });
+
+ test('empty ranges do not notify', () => {
+ layer.annotated = true;
+ layer.setRanges();
+ assert.isFalse(listener.called);
+ });
+
+ test('do not notify while annotated is false', () => {
+ layer.setRanges(RANGES);
+ assert.isFalse(listener.called);
+ });
+
+ test('initial ranges', () => {
+ layer.annotated = true;
+ layer.setRanges(ONE_RANGE);
+ assert.isTrue(listener.called);
+ assert.equal(listener.callCount, 2);
+ assert.equal(listener.getCall(0).args[0], 1);
+ assert.equal(listener.getCall(0).args[1], 2);
+ assert.equal(listener.getCall(1).args[0], 9);
+ assert.equal(listener.getCall(1).args[1], 10);
+ });
+
+ test('old ranges and new range', () => {
+ layer.annotated = true;
+ layer.setRanges(ONE_RANGE);
+ listener.reset();
+ layer.annotate(
+ createTextElement(),
+ createLineElement(100, Side.RIGHT),
+ line,
+ Side.RIGHT
+ );
+ layer.annotate(
+ createTextElement(),
+ createLineElement(101, Side.RIGHT),
+ line,
+ Side.RIGHT
+ );
+ layer.setRanges(RANGES);
+ assert.isTrue(listener.called);
+ assert.equal(listener.callCount, 7);
+ assert.equal(listener.getCall(3).args[0], 100);
+ assert.equal(listener.getCall(3).args[1], 101);
+ assert.equal(listener.getCall(3).args[2], Side.RIGHT);
+ });
+ });
+
+ suite('annotate', () => {
+ function hasOutOfFocusClass(lineNumber: number, side: Side) {
+ const textEl = createTextElement();
+ layer.annotate(textEl, createLineElement(lineNumber, side), line, side);
+ return textEl.classList.contains('is-out-of-focus-range');
+ }
+
+ setup(() => {
+ layer = new GrFocusLayer();
+ layer.setRanges(RANGES);
+ });
+
+ test('line 1-2 are focussed on both sides', () => {
+ assert.isFalse(hasOutOfFocusClass(1, Side.LEFT));
+ assert.isFalse(hasOutOfFocusClass(2, Side.RIGHT));
+ assert.isFalse(hasOutOfFocusClass(1, Side.LEFT));
+ assert.isFalse(hasOutOfFocusClass(2, Side.RIGHT));
+ });
+
+ test('line 3-12 are not focussed on left side', () => {
+ for (let index = 3; index < 12; index++) {
+ assert.isTrue(hasOutOfFocusClass(index, Side.LEFT));
+ }
+ });
+
+ test('line 3-22 are not focussed on right side', () => {
+ for (let index = 3; index < 22; index++) {
+ assert.isTrue(hasOutOfFocusClass(index, Side.RIGHT));
+ }
+ });
+
+ test('line 13-14 are focussed on left side', () => {
+ assert.isFalse(hasOutOfFocusClass(13, Side.LEFT));
+ assert.isFalse(hasOutOfFocusClass(14, Side.LEFT));
+ });
+ });
+});