| /** |
| * @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; |
| } |
| } |