| /** |
| * @license |
| * Copyright 2023 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../styles/shared-styles'; |
| import '../../../elements/shared/gr-button/gr-button'; |
| import '../../../elements/shared/gr-icon/gr-icon'; |
| import '../gr-diff-highlight/gr-diff-highlight'; |
| import '../gr-diff-selection/gr-diff-selection'; |
| import '../gr-syntax-themes/gr-syntax-theme'; |
| import '../gr-ranged-comment-themes/gr-ranged-comment-theme'; |
| import '../gr-ranged-comment-hint/gr-ranged-comment-hint'; |
| import '../gr-diff-builder/gr-diff-builder-image'; |
| import '../gr-diff-builder/gr-diff-section'; |
| import '../gr-diff-builder/gr-diff-row'; |
| import {isResponsive, FullContext, FULL_CONTEXT} from './gr-diff-utils'; |
| import {ImageInfo} from '../../../types/common'; |
| import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff'; |
| import { |
| DiffViewMode, |
| Side, |
| createDefaultDiffPrefs, |
| } from '../../../constants/constants'; |
| import {fire} from '../../../utils/event-util'; |
| import {RenderPreferences, LOST, DiffResponsiveMode} from '../../../api/diff'; |
| import {query, queryAll, state} from 'lit/decorators.js'; |
| import {html, LitElement, nothing} from 'lit'; |
| import {when} from 'lit/directives/when.js'; |
| import {classMap} from 'lit/directives/class-map.js'; |
| import {expandFileMode} from '../../../utils/file-util'; |
| import { |
| ColumnsToShow, |
| NO_COLUMNS, |
| diffModelToken, |
| } from '../gr-diff-model/gr-diff-model'; |
| import {resolve} from '../../../models/dependency'; |
| import {getDiffLength, isImageDiff} from '../../../utils/diff-util'; |
| import {GrDiffGroup} from './gr-diff-group'; |
| import {subscribe} from '../../../elements/lit/subscription-controller'; |
| import {GrDiffSection} from '../gr-diff-builder/gr-diff-section'; |
| import {repeat} from 'lit/directives/repeat.js'; |
| |
| const LARGE_DIFF_THRESHOLD_LINES = 10000; |
| |
| export class GrDiffElement extends LitElement { |
| @query('#diffTable') |
| diffTable?: HTMLTableElement; |
| |
| @queryAll('gr-diff-section') |
| diffSections?: NodeListOf<GrDiffSection>; |
| |
| @state() diff?: DiffInfo; |
| |
| @state() baseImage?: ImageInfo; |
| |
| @state() revisionImage?: ImageInfo; |
| |
| @state() diffPrefs: DiffPreferencesInfo = createDefaultDiffPrefs(); |
| |
| @state() renderPrefs: RenderPreferences = {}; |
| |
| @state() viewMode = DiffViewMode.SIDE_BY_SIDE; |
| |
| @state() groups: GrDiffGroup[] = []; |
| |
| @state() showFullContext: FullContext = FullContext.UNDECIDED; |
| |
| @state() errorMessage?: string; |
| |
| @state() responsiveMode: DiffResponsiveMode = 'NONE'; |
| |
| @state() loading = true; |
| |
| @state() columns: ColumnsToShow = NO_COLUMNS; |
| |
| @state() columnCount = 0; |
| |
| private getDiffModel = resolve(this, diffModelToken); |
| |
| /** |
| * 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; |
| } |
| |
| constructor() { |
| super(); |
| subscribe( |
| this, |
| () => this.getDiffModel().diff$, |
| diff => (this.diff = diff) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().baseImage$, |
| baseImage => (this.baseImage = baseImage) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().revisionImage$, |
| revisionImage => (this.revisionImage = revisionImage) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().diffPrefs$, |
| diffPrefs => (this.diffPrefs = diffPrefs) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().renderPrefs$, |
| renderPrefs => (this.renderPrefs = renderPrefs) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().columnsToShow$, |
| columnsToShow => (this.columns = columnsToShow) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().columnCount$, |
| columnCount => (this.columnCount = columnCount) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().viewMode$, |
| viewMode => (this.viewMode = viewMode) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().showFullContext$, |
| showFullContext => (this.showFullContext = showFullContext) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().errorMessage$, |
| errorMessage => (this.errorMessage = errorMessage) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().groups$, |
| groups => (this.groups = groups) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().loading$, |
| loading => (this.loading = loading) |
| ); |
| subscribe( |
| this, |
| () => this.getDiffModel().responsiveMode$, |
| responsiveMode => (this.responsiveMode = responsiveMode) |
| ); |
| } |
| |
| protected override async getUpdateComplete(): Promise<boolean> { |
| const result = await super.getUpdateComplete(); |
| const sections = [...(this.diffSections ?? [])]; |
| await Promise.all(sections.map(section => section.updateComplete)); |
| return result; |
| } |
| |
| protected override updated() { |
| if (this.diffSections?.length) { |
| this.fireRenderContent(); |
| } |
| } |
| |
| private async fireRenderContent() { |
| await this.updateComplete; |
| // TODO: Retire one of these two events. |
| fire(this, 'render-content', {}); |
| fire(this, 'render', {}); |
| } |
| |
| override render() { |
| fire(this.diffTable, 'render-start', {}); |
| return html` |
| ${this.renderHeader()} ${this.renderContainer()} |
| ${this.renderNewlineWarning()} ${this.renderLoadingError()} |
| `; |
| } |
| |
| private renderHeader() { |
| const diffheaderItems = this.computeDiffHeaderItems(); |
| if (diffheaderItems.length === 0) return nothing; |
| return html` |
| <div id="diffHeader"> |
| ${diffheaderItems.map(item => html`<div>${item}</div>`)} |
| </div> |
| `; |
| } |
| |
| private renderContainer() { |
| const cssClasses = { |
| diffContainer: true, |
| unified: this.viewMode === DiffViewMode.UNIFIED, |
| sideBySide: this.viewMode === DiffViewMode.SIDE_BY_SIDE, |
| canComment: !!this.renderPrefs.can_comment, |
| }; |
| const tableClasses = { |
| responsive: isResponsive(this.responsiveMode), |
| }; |
| const isBinary = !!this.diff?.binary; |
| const isImage = isImageDiff(this.diff); |
| return html` |
| <div class=${classMap(cssClasses)}> |
| <table |
| id="diffTable" |
| class=${classMap(tableClasses)} |
| ?contenteditable=${this.isContentEditable} |
| > |
| ${this.renderColumns()} |
| ${when(!this.showWarning(), () => |
| repeat( |
| this.groups, |
| group => group.id(), |
| group => this.renderSectionElement(group) |
| ) |
| )} |
| ${when(isImage, () => this.renderImageDiff())} |
| ${when(!isImage && isBinary, () => this.renderBinaryDiff())} |
| </table> |
| ${when( |
| this.showNoChangeMessage(), |
| () => html` |
| <div class="whitespace-change-only-message"> |
| This file only contains whitespace changes. Modify the whitespace |
| setting to see the changes. |
| </div> |
| ` |
| )} |
| ${when(this.showWarning(), () => this.renderSizeWarning())} |
| </div> |
| `; |
| } |
| |
| private renderNewlineWarning() { |
| const newlineWarning = this.computeNewlineWarning(); |
| if (!newlineWarning) return nothing; |
| return html`<div class="newlineWarning">${newlineWarning}</div>`; |
| } |
| |
| private renderLoadingError() { |
| if (!this.errorMessage) return nothing; |
| return html`<div id="loadingError">${this.errorMessage}</div>`; |
| } |
| |
| private renderSizeWarning() { |
| if (!this.showWarning()) return nothing; |
| // TODO: Update comment about 'Whole file' as it's not in settings. |
| return html` |
| <div id="sizeWarning"> |
| <p> |
| Prevented render because "Whole file" is enabled and this diff is very |
| large (about ${getDiffLength(this.diff)} lines). |
| </p> |
| <gr-button @click=${this.collapseContext}> |
| Render with limited context |
| </gr-button> |
| <gr-button @click=${this.handleFullBypass}> |
| Render anyway (may be slow) |
| </gr-button> |
| </div> |
| `; |
| } |
| |
| // Private but used in tests. |
| showNoChangeMessage() { |
| return ( |
| !this.loading && |
| this.diff && |
| !this.diff.binary && |
| this.diffPrefs.ignore_whitespace !== 'IGNORE_NONE' && |
| getDiffLength(this.diff) === 0 |
| ); |
| } |
| |
| private showWarning() { |
| return ( |
| this.diffPrefs?.context === FULL_CONTEXT && |
| this.showFullContext === FullContext.UNDECIDED && |
| getDiffLength(this.diff) >= LARGE_DIFF_THRESHOLD_LINES |
| ); |
| } |
| |
| // Private but used in tests. |
| computeDiffHeaderItems() { |
| return (this.diff?.diff_header ?? []) |
| .filter( |
| item => |
| !( |
| item.startsWith('diff --git ') || |
| item.startsWith('index ') || |
| item.startsWith('+++ ') || |
| item.startsWith('--- ') || |
| item === 'Binary files differ' |
| ) |
| ) |
| .map(expandFileMode); |
| } |
| |
| private handleFullBypass() { |
| this.getDiffModel().updateState({showFullContext: FullContext.YES}); |
| } |
| |
| private collapseContext() { |
| this.getDiffModel().updateState({showFullContext: FullContext.NO}); |
| } |
| |
| private computeNewlineWarning(): string | undefined { |
| const messages = []; |
| if (this.renderPrefs.show_newline_warning_left) { |
| messages.push('No newline at end of left file.'); |
| } |
| if (this.renderPrefs.show_newline_warning_right) { |
| messages.push('No newline at end of right file.'); |
| } |
| if (!messages.length) { |
| return undefined; |
| } |
| return messages.join(' \u2014 '); // \u2014 - '—' |
| } |
| |
| private renderImageDiff() { |
| return when( |
| this.renderPrefs.use_new_image_diff_ui, |
| () => this.renderImageDiffNew(), |
| () => this.renderImageDiffOld() |
| ); |
| } |
| |
| private renderImageDiffNew() { |
| const autoBlink = !!this.renderPrefs?.image_diff_prefs?.automatic_blink; |
| return html` |
| <gr-diff-image-new |
| .automaticBlink=${autoBlink} |
| .baseImage=${this.baseImage ?? undefined} |
| .revisionImage=${this.revisionImage ?? undefined} |
| .columnCount=${this.columnCount} |
| ></gr-diff-image-new> |
| `; |
| } |
| |
| private renderImageDiffOld() { |
| return html` |
| <gr-diff-image-old |
| .baseImage=${this.baseImage ?? undefined} |
| .revisionImage=${this.revisionImage ?? undefined} |
| .columnCount=${this.columnCount} |
| ></gr-diff-image-old> |
| `; |
| } |
| |
| public renderBinaryDiff() { |
| return html` |
| <tbody class="binary-diff"> |
| <tr> |
| <td colspan=${this.columnCount}> |
| <span>Difference in binary files</span> |
| </td> |
| </tr> |
| </tbody> |
| `; |
| } |
| |
| renderSectionElement(group: GrDiffGroup) { |
| const leftClass = `left-${group.startLine(Side.LEFT)}`; |
| const rightClass = `right-${group.startLine(Side.RIGHT)}`; |
| if (this.diff?.binary && group.startLine(Side.LEFT) === LOST) { |
| return nothing; |
| } |
| return html` |
| <gr-diff-section |
| class="${leftClass} ${rightClass}" |
| .group=${group} |
| ></gr-diff-section> |
| `; |
| } |
| |
| renderColumns() { |
| const lineNumberWidth = getLineNumberCellWidth( |
| this.diffPrefs ?? createDefaultDiffPrefs() |
| ); |
| return html` |
| <colgroup> |
| ${when(this.columns.blame, () => html`<col class="blame" />`)} |
| ${when( |
| this.columns.leftNumber, |
| () => html`<col class="left" width=${lineNumberWidth} />` |
| )} |
| ${when(this.columns.leftSign, () => html`<col class="left sign" />`)} |
| ${when(this.columns.leftContent, () => html`<col class="left" />`)} |
| ${when( |
| this.columns.rightNumber, |
| () => html`<col class="right" width=${lineNumberWidth} />` |
| )} |
| ${when(this.columns.rightSign, () => html`<col class="right sign" />`)} |
| ${when(this.columns.rightContent, () => html`<col class="right" />`)} |
| </colgroup> |
| `; |
| } |
| } |
| |
| function getLineNumberCellWidth(prefs: DiffPreferencesInfo) { |
| return prefs.font_size * 4; |
| } |
| |
| customElements.define('gr-diff-element', GrDiffElement); |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'gr-diff-element': GrDiffElement; |
| } |
| } |