blob: d2498019c2626d108c68e48aa298acd8cbd0ca76 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement} from 'lit';
import {property, queryAll, state} from 'lit/decorators.js';
import {
DiffInfo,
DiffLayer,
DiffViewMode,
RenderPreferences,
Side,
LineNumber,
DiffPreferencesInfo,
} from '../../../api/diff';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {diffClasses, getResponsiveMode} from '../gr-diff/gr-diff-utils';
import {GrDiffRow} from './gr-diff-row';
import '../gr-context-controls/gr-context-controls-section';
import '../gr-context-controls/gr-context-controls';
import '../gr-range-header/gr-range-header';
import './gr-diff-row';
import {when} from 'lit/directives/when.js';
import {fire} from '../../../utils/event-util';
import {countLines} from '../../../utils/diff-util';
import {resolve} from '../../../models/dependency';
import {
ColumnsToShow,
diffModelToken,
NO_COLUMNS,
} from '../gr-diff-model/gr-diff-model';
import {subscribe} from '../../../elements/lit/subscription-controller';
export class GrDiffSection extends LitElement {
@queryAll('gr-diff-row')
diffRows?: NodeListOf<GrDiffRow>;
@property({type: Object})
group?: GrDiffGroup;
@state()
diff?: DiffInfo;
@state()
renderPrefs?: RenderPreferences;
@state()
diffPrefs?: DiffPreferencesInfo;
@state()
layers: DiffLayer[] = [];
@state()
lineLength = 100;
@state() columns: ColumnsToShow = NO_COLUMNS;
/**
* Semantic DOM diff testing does not work with just table fragments, so when
* running such tests the render() method has to wrap the DOM in a proper
* <table> element.
*/
@state()
addTableWrapperForTesting = false;
@state() viewMode: DiffViewMode = DiffViewMode.SIDE_BY_SIDE;
private readonly getDiffModel = resolve(this, diffModelToken);
constructor() {
super();
subscribe(
this,
() => this.getDiffModel().lineLength$,
lineLength => (this.lineLength = lineLength)
);
subscribe(
this,
() => this.getDiffModel().viewMode$,
viewMode => (this.viewMode = viewMode)
);
subscribe(
this,
() => this.getDiffModel().diff$,
diff => (this.diff = diff)
);
subscribe(
this,
() => this.getDiffModel().renderPrefs$,
renderPrefs => (this.renderPrefs = renderPrefs)
);
subscribe(
this,
() => this.getDiffModel().diffPrefs$,
diffPrefs => (this.diffPrefs = diffPrefs)
);
subscribe(
this,
() => this.getDiffModel().layers$,
layers => (this.layers = layers)
);
subscribe(
this,
() => this.getDiffModel().columnsToShow$,
columnsToShow => (this.columns = columnsToShow)
);
}
/**
* The browser API for handling selection does not (yet) work for selection
* across multiple shadow DOM elements. So we are rendering gr-diff components
* into the light DOM instead of the shadow DOM by overriding this method,
* which was the recommended workaround by the lit team.
* See also https://github.com/WICG/webcomponents/issues/79.
*/
override createRenderRoot() {
return this;
}
protected override async getUpdateComplete(): Promise<boolean> {
const result = await super.getUpdateComplete();
const rows = [...(this.diffRows ?? [])];
await Promise.all(rows.map(row => row.updateComplete));
return result;
}
override render() {
if (!this.group) return;
const extras: string[] = [];
extras.push('section');
extras.push(this.group.type);
if (this.group.isTotal()) extras.push('total');
if (this.group.dueToRebase) extras.push('dueToRebase');
if (this.group.moveDetails) extras.push('dueToMove');
if (this.group.moveDetails?.changed) extras.push('changed');
if (this.group.ignoredWhitespaceOnly) extras.push('ignoredWhitespaceOnly');
const pairs = this.getLinePairs();
const responsiveMode = getResponsiveMode(this.diffPrefs, this.renderPrefs);
const hideFileCommentButton =
this.diffPrefs?.show_file_comment_button === false ||
this.renderPrefs?.show_file_comment_button === false;
const body = html`
<tbody class=${diffClasses(...extras)}>
${this.renderContextControls()} ${this.renderMoveControls()}
${pairs.map(pair => {
const leftClass = `left-${pair.left.lineNumber(Side.LEFT)}`;
const rightClass = `right-${pair.right.lineNumber(Side.RIGHT)}`;
return html`
<gr-diff-row
class="${leftClass} ${rightClass}"
.left=${pair.left}
.right=${pair.right}
.layers=${this.layers}
.lineLength=${this.diffPrefs?.line_length ?? 80}
.tabSize=${this.diffPrefs?.tab_size ?? 2}
.unifiedDiff=${this.isUnifiedDiff()}
.responsiveMode=${responsiveMode}
.hideFileCommentButton=${hideFileCommentButton}
>
</gr-diff-row>
`;
})}
</tbody>
`;
if (this.addTableWrapperForTesting) {
return html`<table>
${body}
</table>`;
}
return body;
}
private isUnifiedDiff() {
return this.viewMode === DiffViewMode.UNIFIED;
}
getLinePairs() {
if (!this.group) return [];
const isControl = this.group.type === GrDiffGroupType.CONTEXT_CONTROL;
if (isControl) return [];
return this.isUnifiedDiff()
? this.group.getUnifiedPairs()
: this.group.getSideBySidePairs();
}
getDiffRows(): GrDiffRow[] {
return [...this.querySelectorAll<GrDiffRow>('gr-diff-row')];
}
private renderContextControls() {
if (this.group?.type !== GrDiffGroupType.CONTEXT_CONTROL) return;
const leftStart = this.group.lineRange.left.start_line;
const leftEnd = this.group.lineRange.left.end_line;
const firstGroupIsSkipped = !!this.group.contextGroups[0].skip;
const lastGroupIsSkipped =
!!this.group.contextGroups[this.group.contextGroups.length - 1].skip;
const lineCountLeft = countLines(this.diff, Side.LEFT);
const containsWholeFile = lineCountLeft === leftEnd - leftStart + 1;
const showAbove =
(leftStart > 1 && !firstGroupIsSkipped) || containsWholeFile;
const showBelow = leftEnd < lineCountLeft && !lastGroupIsSkipped;
return html`
<gr-context-controls-section
.showAbove=${showAbove}
.showBelow=${showBelow}
.group=${this.group}
.diff=${this.diff}
.renderPrefs=${this.renderPrefs}
>
</gr-context-controls-section>
`;
}
findRow(side: Side, lineNumber: LineNumber): GrDiffRow | undefined {
return (
this.querySelector<GrDiffRow>(`gr-diff-row.${side}-${lineNumber}`) ??
undefined
);
}
private renderMoveControls() {
if (!this.group?.moveDetails) return;
const movedIn = this.group.adds.length > 0;
const plainCell = html`<td class=${diffClasses()}></td>`;
const moveCell = html`
<td class=${diffClasses('moveHeader')}>
<gr-range-header class=${diffClasses()} icon="move_item">
${this.renderMoveDescription(movedIn)}
</gr-range-header>
</td>
`;
return html`
<tr
class=${diffClasses('moveControls', movedIn ? 'movedIn' : 'movedOut')}
>
${when(
this.columns.blame,
() => html`<td class=${diffClasses('blame')}></td>`
)}
${when(
this.columns.leftNumber,
() => html`<td class=${diffClasses('moveControlsLineNumCol')}></td>`
)}
${when(
this.columns.leftSign,
() => html`<td class=${diffClasses('sign')}></td>`
)}
${when(this.columns.leftContent, () =>
movedIn ? plainCell : moveCell
)}
${when(
this.columns.rightNumber,
() => html`<td class=${diffClasses('moveControlsLineNumCol')}></td>`
)}
${when(
this.columns.rightSign,
() => html`<td class=${diffClasses('sign')}></td>`
)}
${when(this.columns.rightContent, () =>
movedIn || this.isUnifiedDiff() ? moveCell : plainCell
)}
</tr>
`;
}
private renderMoveDescription(movedIn: boolean) {
if (this.group?.moveDetails?.range) {
const {changed, range} = this.group.moveDetails;
const otherSide = movedIn ? Side.LEFT : Side.RIGHT;
const andChangedLabel = changed ? 'and changed ' : '';
const direction = movedIn ? 'from' : 'to';
const textLabel = `Moved ${andChangedLabel}${direction} lines `;
return html`
<div class=${diffClasses()}>
<span class=${diffClasses()}>${textLabel}</span>
${this.renderMovedLineAnchor(range.start, otherSide)}
<span class=${diffClasses()}> - </span>
${this.renderMovedLineAnchor(range.end, otherSide)}
</div>
`;
}
return html`
<div class=${diffClasses()}>
<span class=${diffClasses()}
>${movedIn ? 'Moved in' : 'Moved out'}</span
>
</div>
`;
}
private renderMovedLineAnchor(line: number, side: Side) {
const listener = (e: MouseEvent) => {
e.preventDefault();
this.handleMovedLineAnchorClick(e.target, side, line);
};
// `href` is not actually used but important for Screen Readers
return html`
<a class=${diffClasses()} href=${`#${line}`} @click=${listener}
>${line}</a
>
`;
}
private handleMovedLineAnchorClick(
anchor: EventTarget | null,
side: Side,
line: number
) {
if (!anchor) return;
fire(anchor, 'moved-link-clicked', {
lineNum: line,
side,
});
}
}
customElements.define('gr-diff-section', GrDiffSection);
declare global {
interface HTMLElementTagNameMap {
'gr-diff-section': GrDiffSection;
}
}