Add outline information to gr-diff
Implement preliminary block expansion behavior based on the
right side of the syntax tree.
Refator: Split the creation of different button groups (different types)
in different methods.
Change-Id: Iabdc469748b86a1333e57bf2c0ebe5c88f802871
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index a10cb14..396fd8e 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -53,6 +53,35 @@
}
/**
+ * Represents a syntax block in a code (e.g. method, function, class, if-else).
+ */
+export interface SyntaxBlock {
+ /** Name of the block (e.g. name of the method/class)*/
+ name: string;
+ /** Where does this block syntatically starts and ends (line number and column).*/
+ range: {
+ /** first line of the block (1-based inclusive). */
+ start_line: number;
+ /**
+ * column of the range start inside the first line (e.g. "{" character ending a function/method)
+ * (1-based inclusive).
+ */
+ start_column: number;
+ /**
+ * last line of the block (1-based inclusive).
+ */
+ end_line: number;
+ /**
+ * column of the block end inside the end line (e.g. "}" character ending a function/method)
+ * (1-based inclusive).
+ */
+ end_column: number;
+ };
+ /** Sub-blocks of the current syntax block (e.g. methods of a class) */
+ children: SyntaxBlock[];
+}
+
+/**
* The DiffFileMetaInfo entity contains meta information about a file diff.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#diff-file-meta-info
*/
@@ -65,6 +94,12 @@
lines: number;
// TODO: Not documented.
language?: string;
+ /**
+ * The first level of syntax blocks tree (outline) within the current file.
+ * It contains an hierarchical structure where each block contains its
+ * sub-blocks (children).
+ */
+ syntax_tree?: SyntaxBlock[];
}
export declare type ChangeType =
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index 7c01a95..5ba3606 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -97,8 +97,13 @@
return section;
}
+ let diffInfo;
+ let renderPrefs;
+
setup(() => {
- builder = new GrDiffBuilder({content: []}, prefs, null, []);
+ diffInfo = {content: []};
+ renderPrefs = {};
+ builder = new GrDiffBuilder(diffInfo, prefs, null, [], renderPrefs);
});
test('no +10 buttons for 10 or less lines', () => {
@@ -149,6 +154,70 @@
assert.include([...buttons[0].classList.values()], 'aboveButton');
assert.include([...buttons[1].classList.values()], 'aboveButton');
});
+
+ suite('with block expansion', () => {
+ setup(() => {
+ builder._numLinesLeft = 50;
+ renderPrefs.use_block_expansion = true;
+ diffInfo.meta_b = {
+ syntax_tree: [],
+ };
+ });
+
+ test('context control with block expansion at the top', () => {
+ const section = createContextSectionForGroups({offset: 0, count: 20});
+
+ const fullExpansionButtons = section
+ .querySelectorAll('.fullExpansion gr-button');
+ const partialExpansionButtons = section
+ .querySelectorAll('.partialExpansion gr-button');
+ const blockExpansionButtons = section
+ .querySelectorAll('.blockExpansion gr-button');
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons[0].textContent, '+Block');
+ assert.include([...blockExpansionButtons[0].classList.values()],
+ 'belowButton');
+ });
+
+ test('context control in the middle', () => {
+ const section = createContextSectionForGroups({offset: 10, count: 20});
+
+ const fullExpansionButtons = section
+ .querySelectorAll('.fullExpansion gr-button');
+ const partialExpansionButtons = section
+ .querySelectorAll('.partialExpansion gr-button');
+ const blockExpansionButtons = section
+ .querySelectorAll('.blockExpansion gr-button');
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 2);
+ assert.equal(blockExpansionButtons.length, 2);
+ assert.equal(blockExpansionButtons[0].textContent, '+Block');
+ assert.equal(blockExpansionButtons[1].textContent, '+Block');
+ assert.include([...blockExpansionButtons[0].classList.values()],
+ 'aboveButton');
+ assert.include([...blockExpansionButtons[1].classList.values()],
+ 'belowButton');
+ });
+
+ test('context control at the bottom', () => {
+ const section = createContextSectionForGroups({offset: 30, count: 20});
+
+ const fullExpansionButtons = section
+ .querySelectorAll('.fullExpansion gr-button');
+ const partialExpansionButtons = section
+ .querySelectorAll('.partialExpansion gr-button');
+ const blockExpansionButtons = section
+ .querySelectorAll('.blockExpansion gr-button');
+ assert.equal(fullExpansionButtons.length, 1);
+ assert.equal(partialExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons.length, 1);
+ assert.equal(blockExpansionButtons[0].textContent, '+Block');
+ assert.include([...blockExpansionButtons[0].classList.values()],
+ 'aboveButton');
+ });
+ });
});
test('newlines 1', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
index c8b69f0..d5e6ecd 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.ts
@@ -20,6 +20,7 @@
DiffContextExpandedExternalDetail,
MovedLinkClickedEventDetail,
RenderPreferences,
+ SyntaxBlock,
} from '../../../api/diff';
import {getBaseUrl} from '../../../utils/url-util';
import {GrDiffLine, GrDiffLineType, LineNumber} from '../gr-diff/gr-diff-line';
@@ -73,6 +74,19 @@
}
}
+function findMostNestedContainingBlock(
+ lineNum: number,
+ blocks?: SyntaxBlock[]
+): SyntaxBlock | undefined {
+ const containingBlock = blocks?.find(
+ ({range}) => range.start_line < lineNum && range.end_line > lineNum
+ );
+ const containingChildBlock = containingBlock
+ ? findMostNestedContainingBlock(lineNum, containingBlock?.children)
+ : undefined;
+ return containingChildBlock || containingBlock;
+}
+
export abstract class GrDiffBuilder {
private readonly _diff: DiffInfo;
@@ -306,6 +320,7 @@
);
}
+ // TODO(renanoliveira): Move context controls to polymer component (or at least a separate class).
_createContextControls(
section: HTMLElement,
contextGroups: GrDiffGroup[],
@@ -314,9 +329,9 @@
const leftStart = contextGroups[0].lineRange.left.start_line;
const leftEnd =
contextGroups[contextGroups.length - 1].lineRange.left.end_line;
- const numLines = leftEnd - leftStart + 1;
-
- if (numLines === 0) console.error('context group without lines');
+ const rightStart = contextGroups[0].lineRange.right.start_line;
+ const rightEnd =
+ contextGroups[contextGroups.length - 1].lineRange.right.end_line;
const firstGroupIsSkipped = !!contextGroups[0].skip;
const lastGroupIsSkipped = !!contextGroups[contextGroups.length - 1].skip;
@@ -335,7 +350,8 @@
contextGroups,
showAbove,
showBelow,
- numLines
+ rightStart,
+ rightEnd
)
);
if (showBelow) {
@@ -354,8 +370,12 @@
contextGroups: GrDiffGroup[],
showAbove: boolean,
showBelow: boolean,
- numLines: number
+ rightStart: number,
+ rightEnd: number
): HTMLElement {
+ const numLines = rightEnd - rightStart + 1;
+ if (numLines === 0) console.error('context group without lines');
+
const row = this._createElement('tr', 'contextDivider');
if (!(showAbove && showBelow)) {
row.classList.add('collapsed');
@@ -364,13 +384,55 @@
const element = this._createElement('td', 'dividerCell');
row.appendChild(element);
- const showAllContainer = this._createElement('div', 'aboveBelowButtons');
+ const showAllContainer = this._createExpandAllButtonContainer(
+ section,
+ contextGroups,
+ showAbove,
+ showBelow,
+ numLines
+ );
element.appendChild(showAllContainer);
+ const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
+ if (showPartialLinks) {
+ const partialExpansionContainer = this._createPartialExpansionButtons(
+ section,
+ contextGroups,
+ showAbove,
+ showBelow,
+ numLines
+ );
+ if (partialExpansionContainer) {
+ element.appendChild(partialExpansionContainer);
+ }
+ const blockExpansionContainer = this._createBlockExpansionButtons(
+ section,
+ contextGroups,
+ showAbove,
+ showBelow,
+ rightStart,
+ rightEnd,
+ numLines
+ );
+ if (blockExpansionContainer) {
+ element.appendChild(blockExpansionContainer);
+ }
+ }
+ return row;
+ }
+
+ private _createExpandAllButtonContainer(
+ section: HTMLElement,
+ contextGroups: GrDiffGroup[],
+ showAbove: boolean,
+ showBelow: boolean,
+ numLines: number
+ ) {
const showAllButton = this._createContextButton(
ContextButtonType.ALL,
section,
contextGroups,
+ numLines,
numLines
);
showAllButton.classList.add(
@@ -380,61 +442,131 @@
? 'aboveButton'
: 'belowButton'
);
+ const showAllContainer = this._createElement(
+ 'div',
+ 'aboveBelowButtons fullExpansion'
+ );
showAllContainer.appendChild(showAllButton);
+ return showAllContainer;
+ }
- const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
- if (showPartialLinks) {
- const container = this._createElement('div', 'aboveBelowButtons');
- if (showAbove) {
- container.appendChild(
- this._createContextButton(
- ContextButtonType.ABOVE,
- section,
- contextGroups,
- numLines
- )
- );
- }
- if (showBelow) {
- container.appendChild(
- this._createContextButton(
- ContextButtonType.BELOW,
- section,
- contextGroups,
- numLines
- )
- );
- }
- element.appendChild(container);
- if (this._renderPrefs?.use_block_expansion) {
- const blockExpansionContainer = this._createElement(
- 'div',
- 'aboveBelowButtons'
- );
- if (showAbove) {
- blockExpansionContainer.appendChild(
- this._createContextButton(
- ContextButtonType.BLOCK_ABOVE,
- section,
- contextGroups,
- numLines
- )
- );
- }
- if (showBelow) {
- blockExpansionContainer.appendChild(
- this._createContextButton(
- ContextButtonType.BLOCK_BELOW,
- section,
- contextGroups,
- numLines
- )
- );
- }
- element.appendChild(blockExpansionContainer);
+ private _createPartialExpansionButtons(
+ section: HTMLElement,
+ contextGroups: GrDiffGroup[],
+ showAbove: boolean,
+ showBelow: boolean,
+ numLines: number
+ ) {
+ let aboveButton;
+ let belowButton;
+ if (showAbove) {
+ aboveButton = this._createContextButton(
+ ContextButtonType.ABOVE,
+ section,
+ contextGroups,
+ numLines,
+ PARTIAL_CONTEXT_AMOUNT
+ );
+ }
+ if (showBelow) {
+ belowButton = this._createContextButton(
+ ContextButtonType.BELOW,
+ section,
+ contextGroups,
+ numLines,
+ PARTIAL_CONTEXT_AMOUNT
+ );
+ }
+ if (aboveButton || belowButton) {
+ const partialExpansionContainer = this._createElement(
+ 'div',
+ 'aboveBelowButtons partialExpansion'
+ );
+ aboveButton && partialExpansionContainer.appendChild(aboveButton);
+ belowButton && partialExpansionContainer.appendChild(belowButton);
+ return partialExpansionContainer;
+ }
+ return undefined;
+ }
+
+ private _createBlockExpansionButtons(
+ section: HTMLElement,
+ contextGroups: GrDiffGroup[],
+ showAbove: boolean,
+ showBelow: boolean,
+ rightStart: number,
+ rightEnd: number,
+ numLines: number
+ ) {
+ if (!this._renderPrefs?.use_block_expansion) {
+ return undefined;
+ }
+ let aboveBlockButton;
+ let belowBlockButton;
+ const rightSyntaxTree = this._diff.meta_b.syntax_tree;
+ if (showAbove) {
+ aboveBlockButton = this._createBlockButton(
+ section,
+ contextGroups,
+ ContextButtonType.BLOCK_ABOVE,
+ numLines,
+ rightStart - 1,
+ rightSyntaxTree
+ );
+ }
+ if (showBelow) {
+ belowBlockButton = this._createBlockButton(
+ section,
+ contextGroups,
+ ContextButtonType.BLOCK_BELOW,
+ numLines,
+ rightEnd + 1,
+ rightSyntaxTree
+ );
+ }
+ if (aboveBlockButton || belowBlockButton) {
+ const blockExpansionContainer = this._createElement(
+ 'div',
+ 'blockExpansion aboveBelowButtons'
+ );
+ aboveBlockButton && blockExpansionContainer.appendChild(aboveBlockButton);
+ belowBlockButton && blockExpansionContainer.appendChild(belowBlockButton);
+ return blockExpansionContainer;
+ }
+ return undefined;
+ }
+
+ private _createBlockButton(
+ section: HTMLElement,
+ contextGroups: GrDiffGroup[],
+ buttonType: ContextButtonType,
+ numLines: number,
+ referenceLine: number,
+ syntaxTree?: SyntaxBlock[]
+ ) {
+ const containingBlock = findMostNestedContainingBlock(
+ referenceLine,
+ syntaxTree
+ );
+ let linesToExpand = numLines;
+ if (containingBlock) {
+ const {range} = containingBlock;
+ const targetLine =
+ buttonType === ContextButtonType.BLOCK_ABOVE
+ ? range.end_line
+ : range.start_line;
+ const distanceToTargetLine = Math.abs(targetLine - referenceLine);
+ if (distanceToTargetLine < numLines) {
+ linesToExpand = distanceToTargetLine;
}
}
- return row;
+ return this._createContextButton(
+ buttonType,
+ section,
+ contextGroups,
+ numLines,
+ linesToExpand
+ );
}
/**
@@ -469,10 +601,9 @@
type: ContextButtonType,
section: HTMLElement,
contextGroups: GrDiffGroup[],
- numLines: number
+ numLines: number,
+ linesToExpand: number
) {
- const linesToExpand =
- type === ContextButtonType.ALL ? numLines : PARTIAL_CONTEXT_AMOUNT;
const button = this._createElement('gr-button', 'showContext');
button.classList.add('contextControlButton');
button.setAttribute('link', 'true');