| /** | |
| * @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 { | |
| isNewDiff, | |
| 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 {diffModelToken} 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; | |
| /** | |
| * 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) | |
| ); | |
| } | |
| /** | |
| * 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 signCell = html`<td class=${diffClasses('sign')}></td>`; | |
| const lineNumberCell = html` | |
| <td class=${diffClasses('moveControlsLineNumCol')}></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.isUnifiedDiff(), | |
| () => html`${lineNumberCell} ${lineNumberCell} ${moveCell}`, | |
| () => html`${lineNumberCell} ${signCell} | |
| ${movedIn ? plainCell : moveCell} ${lineNumberCell} ${signCell} | |
| ${movedIn ? 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, | |
| }); | |
| } | |
| } | |
| // TODO(newdiff-cleanup): Remove once newdiff migration is completed. | |
| if (isNewDiff()) { | |
| customElements.define('gr-diff-section', GrDiffSection); | |
| } | |
| declare global { | |
| interface HTMLElementTagNameMap { | |
| // TODO(newdiff-cleanup): Replace once newdiff migration is completed. | |
| 'gr-diff-section': LitElement; | |
| } | |
| } |