| /** |
| * @license |
| * Copyright 2016 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import './gr-diff-section'; |
| import '../gr-context-controls/gr-context-controls'; |
| import { |
| ContentLoadNeededEventDetail, |
| DiffContextExpandedExternalDetail, |
| DiffViewMode, |
| RenderPreferences, |
| } from '../../../api/diff'; |
| import {LineNumber} from '../gr-diff/gr-diff-line'; |
| import {GrDiffGroup} from '../gr-diff/gr-diff-group'; |
| import {BlameInfo} from '../../../types/common'; |
| import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff'; |
| import {Side} from '../../../constants/constants'; |
| import {DiffLayer, isDefined} from '../../../types/types'; |
| import {GrDiffRow} from './gr-diff-row'; |
| import {GrDiffSection} from './gr-diff-section'; |
| import {html, render} from 'lit'; |
| import {diffClasses} from '../gr-diff/gr-diff-utils'; |
| import {when} from 'lit/directives/when.js'; |
| import {GrDiffBuilderImage} from './gr-diff-builder-image'; |
| import {GrDiffBuilderBinary} from './gr-diff-builder-binary'; |
| |
| export interface DiffContextExpandedEventDetail |
| extends DiffContextExpandedExternalDetail { |
| /** The context control group that should be replaced by `groups`. */ |
| contextGroup: GrDiffGroup; |
| groups: GrDiffGroup[]; |
| numLines: number; |
| } |
| |
| declare global { |
| interface HTMLElementEventMap { |
| 'diff-context-expanded': CustomEvent<DiffContextExpandedEventDetail>; |
| 'content-load-needed': CustomEvent<ContentLoadNeededEventDetail>; |
| } |
| } |
| |
| export function isImageDiffBuilder<T extends GrDiffBuilder>( |
| x: T | GrDiffBuilderImage | undefined |
| ): x is GrDiffBuilderImage { |
| return !!x && !!(x as GrDiffBuilderImage).renderImageDiff; |
| } |
| |
| export function isBinaryDiffBuilder<T extends GrDiffBuilder>( |
| x: T | GrDiffBuilderBinary | undefined |
| ): x is GrDiffBuilderBinary { |
| return !!x && !!(x as GrDiffBuilderBinary).renderBinaryDiff; |
| } |
| |
| /** |
| * The builder takes GrDiffGroups, and builds the corresponding DOM elements, |
| * called sections. Only the builder should add or remove sections from the |
| * DOM. Callers can use the ...group() methods to modify groups and thus cause |
| * rendering changes. |
| */ |
| export class GrDiffBuilder { |
| private readonly diff: DiffInfo; |
| |
| readonly prefs: DiffPreferencesInfo; |
| |
| renderPrefs?: RenderPreferences; |
| |
| readonly outputEl: HTMLElement; |
| |
| private groups: GrDiffGroup[]; |
| |
| private readonly layerUpdateListener: ( |
| start: LineNumber, |
| end: LineNumber, |
| side: Side |
| ) => void; |
| |
| constructor( |
| diff: DiffInfo, |
| prefs: DiffPreferencesInfo, |
| outputEl: HTMLElement, |
| readonly layers: DiffLayer[] = [], |
| renderPrefs?: RenderPreferences |
| ) { |
| this.diff = diff; |
| this.prefs = prefs; |
| this.renderPrefs = renderPrefs; |
| this.outputEl = outputEl; |
| this.groups = []; |
| |
| if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) { |
| throw Error('Invalid tab size from preferences.'); |
| } |
| |
| if (isNaN(prefs.line_length) || prefs.line_length <= 0) { |
| throw Error('Invalid line length from preferences.'); |
| } |
| |
| this.layerUpdateListener = ( |
| start: LineNumber, |
| end: LineNumber, |
| side: Side |
| ) => this.renderContentByRange(start, end, side); |
| this.init(); |
| } |
| |
| getContentTdByLine( |
| lineNumber: LineNumber, |
| side?: Side |
| ): HTMLTableCellElement | undefined { |
| if (!side) return undefined; |
| const row = this.findRow(lineNumber, side); |
| return row?.getContentCell(side); |
| } |
| |
| getLineElByNumber( |
| lineNumber: LineNumber, |
| side?: Side |
| ): HTMLTableCellElement | undefined { |
| if (!side) return undefined; |
| const row = this.findRow(lineNumber, side); |
| return row?.getLineNumberCell(side); |
| } |
| |
| private findRow(lineNumber?: LineNumber, side?: Side): GrDiffRow | undefined { |
| if (!side || !lineNumber) return undefined; |
| const group = this.findGroup(side, lineNumber); |
| if (!group) return undefined; |
| const section = this.findSection(group); |
| if (!section) return undefined; |
| return section.findRow(side, lineNumber); |
| } |
| |
| private getDiffRows() { |
| const sections = [ |
| ...this.outputEl.querySelectorAll<GrDiffSection>('gr-diff-section'), |
| ]; |
| return sections.map(s => s.getDiffRows()).flat(); |
| } |
| |
| getLineNumberRows(): HTMLTableRowElement[] { |
| const rows = this.getDiffRows(); |
| return rows.map(r => r.getTableRow()).filter(isDefined); |
| } |
| |
| getLineNumEls(side: Side): HTMLTableCellElement[] { |
| const rows = this.getDiffRows(); |
| return rows.map(r => r.getLineNumberCell(side)).filter(isDefined); |
| } |
| |
| /** This is used when layers initiate an update. */ |
| renderContentByRange(start: LineNumber, end: LineNumber, side: Side) { |
| const groups = this.getGroupsByLineRange(start, end, side); |
| for (const group of groups) { |
| const section = this.findSection(group); |
| for (const row of section?.getDiffRows() ?? []) { |
| row.requestUpdate(); |
| } |
| } |
| } |
| |
| private findSection(group: GrDiffGroup): GrDiffSection | undefined { |
| const leftClass = `left-${group.startLine(Side.LEFT)}`; |
| const rightClass = `right-${group.startLine(Side.RIGHT)}`; |
| return ( |
| this.outputEl.querySelector<GrDiffSection>( |
| `gr-diff-section.${leftClass}.${rightClass}` |
| ) ?? undefined |
| ); |
| } |
| |
| buildSectionElement(group: GrDiffGroup): HTMLElement { |
| const leftCl = `left-${group.startLine(Side.LEFT)}`; |
| const rightCl = `right-${group.startLine(Side.RIGHT)}`; |
| const section = html` |
| <gr-diff-section |
| class="${leftCl} ${rightCl}" |
| .group=${group} |
| .diff=${this.diff} |
| .layers=${this.layers} |
| .diffPrefs=${this.prefs} |
| .renderPrefs=${this.renderPrefs} |
| ></gr-diff-section> |
| `; |
| // When using Lit's `render()` method it wants to be in full control of the |
| // element that it renders into, so we let it render into a temp element. |
| // Rendering into the diff table directly would interfere with |
| // `clearDiffContent()`for example. |
| // TODO: Convert <gr-diff> to be fully lit controlled and incorporate this |
| // method into Lit's `render()` cycle. |
| const tempEl = document.createElement('div'); |
| render(section, tempEl); |
| const sectionEl = tempEl.firstElementChild as GrDiffSection; |
| return sectionEl; |
| } |
| |
| addColumns(outputEl: HTMLElement, lineNumberWidth: number): void { |
| const colgroup = html` |
| <colgroup> |
| <col class=${diffClasses('blame')}></col> |
| ${when( |
| this.renderPrefs?.view_mode === DiffViewMode.UNIFIED, |
| () => html` ${this.renderUnifiedColumns(lineNumberWidth)} `, |
| () => html` |
| ${this.renderSideBySideColumns(Side.LEFT, lineNumberWidth)} |
| ${this.renderSideBySideColumns(Side.RIGHT, lineNumberWidth)} |
| ` |
| )} |
| </colgroup> |
| `; |
| // When using Lit's `render()` method it wants to be in full control of the |
| // element that it renders into, so we let it render into a temp element. |
| // Rendering into the diff table directly would interfere with |
| // `clearDiffContent()`for example. |
| // TODO: Convert <gr-diff> to be fully lit controlled and incorporate this |
| // method into Lit's `render()` cycle. |
| const tempEl = document.createElement('div'); |
| render(colgroup, tempEl); |
| const colgroupEl = tempEl.firstElementChild as HTMLElement; |
| outputEl.appendChild(colgroupEl); |
| } |
| |
| private renderUnifiedColumns(lineNumberWidth: number) { |
| return html` |
| <col class=${diffClasses()} width=${lineNumberWidth}></col> |
| <col class=${diffClasses()} width=${lineNumberWidth}></col> |
| <col class=${diffClasses()}></col> |
| `; |
| } |
| |
| private renderSideBySideColumns(side: Side, lineNumberWidth: number) { |
| return html` |
| <col class=${diffClasses(side)} width=${lineNumberWidth}></col> |
| <col class=${diffClasses(side, 'sign')}></col> |
| <col class=${diffClasses(side)}></col> |
| `; |
| } |
| |
| /** |
| * This is meant to be called when the gr-diff component re-connects, or when |
| * the diff is (re-)rendered. |
| * |
| * Make sure that this method is symmetric with cleanup(), which is called |
| * when gr-diff disconnects. |
| */ |
| init() { |
| this.cleanup(); |
| for (const layer of this.layers) { |
| if (layer.addListener) { |
| layer.addListener(this.layerUpdateListener); |
| } |
| } |
| } |
| |
| /** |
| * This is meant to be called when the gr-diff component disconnects, or when |
| * the diff is (re-)rendered. |
| * |
| * Make sure that this method is symmetric with init(), which is called when |
| * gr-diff re-connects. |
| */ |
| cleanup() { |
| for (const layer of this.layers) { |
| if (layer.removeListener) { |
| layer.removeListener(this.layerUpdateListener); |
| } |
| } |
| } |
| |
| addGroups(groups: readonly GrDiffGroup[]) { |
| for (const group of groups) { |
| this.groups.push(group); |
| this.emitGroup(group); |
| } |
| } |
| |
| clearGroups() { |
| for (const deletedGroup of this.groups) { |
| deletedGroup.element?.remove(); |
| } |
| this.groups = []; |
| } |
| |
| replaceGroup(contextControl: GrDiffGroup, groups: readonly GrDiffGroup[]) { |
| const i = this.groups.indexOf(contextControl); |
| if (i === -1) throw new Error('cannot find context control group'); |
| |
| const contextControlSection = this.groups[i].element; |
| if (!contextControlSection) throw new Error('diff group element not set'); |
| |
| this.groups.splice(i, 1, ...groups); |
| for (const group of groups) { |
| this.emitGroup(group, contextControlSection); |
| } |
| if (contextControlSection) contextControlSection.remove(); |
| } |
| |
| findGroup(side: Side, line: LineNumber) { |
| return this.groups.find(group => group.containsLine(side, line)); |
| } |
| |
| private emitGroup(group: GrDiffGroup, beforeSection?: HTMLElement) { |
| const element = this.buildSectionElement(group); |
| this.outputEl.insertBefore(element, beforeSection ?? null); |
| group.element = element; |
| } |
| |
| // visible for testing |
| getGroupsByLineRange( |
| startLine: LineNumber, |
| endLine: LineNumber, |
| side: Side |
| ): GrDiffGroup[] { |
| const startIndex = this.groups.findIndex(group => |
| group.containsLine(side, startLine) |
| ); |
| if (startIndex === -1) return []; |
| let endIndex = this.groups.findIndex(group => |
| group.containsLine(side, endLine) |
| ); |
| // Not all groups may have been processed yet (i.e. this.groups is still |
| // incomplete). In that case let's just return *all* groups until the end |
| // of the array. |
| if (endIndex === -1) endIndex = this.groups.length - 1; |
| // The filter preserves the legacy behavior to only return non-context |
| // groups |
| return this.groups |
| .slice(startIndex, endIndex + 1) |
| .filter(group => group.lines.length > 0); |
| } |
| |
| /** |
| * Set the blame information for the diff. For any already-rendered line, |
| * re-render its blame cell content. |
| */ |
| setBlame(blame: BlameInfo[]) { |
| for (const blameInfo of blame) { |
| for (const range of blameInfo.ranges) { |
| for (let line = range.start; line <= range.end; line++) { |
| const row = this.findRow(line, Side.LEFT); |
| if (row) row.blameInfo = blameInfo; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Only special builders need to implement this. The default is to |
| * just ignore it. |
| */ |
| updateRenderPrefs(_: RenderPreferences) {} |
| } |