
Adds a simple util function for firing a CustomEvent without detail and composed:true,bubbles:true. Replaces all dispatchEvent() calls with this util. Does not replace, if a detail is used of composed/bubbles are set differently. Does not add event type enum values, but just uses strings. Change-Id: I3148f4bfab5696f92320d22372ce215b61174256
529 lines
15 KiB
TypeScript
529 lines
15 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
import '../gr-coverage-layer/gr-coverage-layer';
|
|
import '../gr-diff-processor/gr-diff-processor';
|
|
import '../../shared/gr-hovercard/gr-hovercard';
|
|
import '../gr-ranged-comment-layer/gr-ranged-comment-layer';
|
|
import './gr-diff-builder-side-by-side';
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
|
import {htmlTemplate} from './gr-diff-builder-element_html';
|
|
import {GrAnnotation} from '../gr-diff-highlight/gr-annotation';
|
|
import {GrDiffBuilder} from './gr-diff-builder';
|
|
import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side';
|
|
import {GrDiffBuilderImage} from './gr-diff-builder-image';
|
|
import {GrDiffBuilderUnified} from './gr-diff-builder-unified';
|
|
import {GrDiffBuilderBinary} from './gr-diff-builder-binary';
|
|
import {CancelablePromise, util} from '../../../scripts/util';
|
|
import {customElement, property, observe} from '@polymer/decorators';
|
|
import {BlameInfo, ImageInfo} from '../../../types/common';
|
|
import {DiffInfo, DiffPreferencesInfo} from '../../../types/diff';
|
|
import {CoverageRange, DiffLayer} from '../../../types/types';
|
|
import {
|
|
GrDiffProcessor,
|
|
KeyLocations,
|
|
} from '../gr-diff-processor/gr-diff-processor';
|
|
import {
|
|
CommentRangeLayer,
|
|
GrRangedCommentLayer,
|
|
} from '../gr-ranged-comment-layer/gr-ranged-comment-layer';
|
|
import {GrCoverageLayer} from '../gr-coverage-layer/gr-coverage-layer';
|
|
import {Side} from '../../../constants/constants';
|
|
import {GrDiffLine, LineNumber} from '../gr-diff/gr-diff-line';
|
|
import {GrDiffGroup} from '../gr-diff/gr-diff-group';
|
|
import {PolymerSpliceChange} from '@polymer/polymer/interfaces';
|
|
import {getLineNumber} from '../gr-diff/gr-diff-utils';
|
|
import {fireAlert, fireEvent} from '../../../utils/event-util';
|
|
|
|
const DiffViewMode = {
|
|
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
|
|
UNIFIED: 'UNIFIED_DIFF',
|
|
};
|
|
|
|
const TRAILING_WHITESPACE_PATTERN = /\s+$/;
|
|
|
|
// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740
|
|
const COMMIT_MSG_PATH = '/COMMIT_MSG';
|
|
const COMMIT_MSG_LINE_LENGTH = 72;
|
|
|
|
export interface GrDiffBuilderElement {
|
|
$: {
|
|
processor: GrDiffProcessor;
|
|
rangeLayer: GrRangedCommentLayer;
|
|
coverageLayerLeft: GrCoverageLayer;
|
|
coverageLayerRight: GrCoverageLayer;
|
|
};
|
|
}
|
|
|
|
@customElement('gr-diff-builder')
|
|
export class GrDiffBuilderElement extends GestureEventListeners(
|
|
LegacyElementMixin(PolymerElement)
|
|
) {
|
|
static get template() {
|
|
return htmlTemplate;
|
|
}
|
|
|
|
/**
|
|
* Fired when the diff begins rendering.
|
|
*
|
|
* @event render-start
|
|
*/
|
|
|
|
/**
|
|
* Fired when the diff finishes rendering text content.
|
|
*
|
|
* @event render-content
|
|
*/
|
|
|
|
@property({type: Object})
|
|
diff?: DiffInfo;
|
|
|
|
@property({type: String})
|
|
changeNum?: string;
|
|
|
|
@property({type: String})
|
|
patchNum?: string;
|
|
|
|
@property({type: String})
|
|
viewMode?: string;
|
|
|
|
@property({type: Boolean})
|
|
isImageDiff?: boolean;
|
|
|
|
@property({type: Object})
|
|
baseImage: ImageInfo | null = null;
|
|
|
|
@property({type: Object})
|
|
revisionImage: ImageInfo | null = null;
|
|
|
|
@property({type: Number})
|
|
parentIndex?: number;
|
|
|
|
@property({type: String})
|
|
path?: string;
|
|
|
|
@property({type: Object})
|
|
_builder?: GrDiffBuilder;
|
|
|
|
@property({type: Array})
|
|
_groups: GrDiffGroup[] = [];
|
|
|
|
/**
|
|
* Layers passed in from the outside.
|
|
*/
|
|
@property({type: Array})
|
|
layers: DiffLayer[] = [];
|
|
|
|
/**
|
|
* All layers, both from the outside and the default ones.
|
|
*/
|
|
@property({type: Array})
|
|
_layers: DiffLayer[] = [];
|
|
|
|
@property({type: Boolean})
|
|
_showTabs?: boolean;
|
|
|
|
@property({type: Boolean})
|
|
_showTrailingWhitespace?: boolean;
|
|
|
|
@property({type: Array})
|
|
commentRanges: CommentRangeLayer[] = [];
|
|
|
|
@property({type: Array})
|
|
coverageRanges: CoverageRange[] = [];
|
|
|
|
@property({type: Boolean})
|
|
useNewContextControls = false;
|
|
|
|
@property({
|
|
type: Array,
|
|
computed: '_computeLeftCoverageRanges(coverageRanges)',
|
|
})
|
|
_leftCoverageRanges?: CoverageRange[];
|
|
|
|
@property({
|
|
type: Array,
|
|
computed: '_computeRightCoverageRanges(coverageRanges)',
|
|
})
|
|
_rightCoverageRanges?: CoverageRange[];
|
|
|
|
/**
|
|
* The promise last returned from `render()` while the asynchronous
|
|
* rendering is running - `null` otherwise. Provides a `cancel()`
|
|
* method that rejects it with `{isCancelled: true}`.
|
|
*/
|
|
@property({type: Object})
|
|
_cancelableRenderPromise: CancelablePromise<unknown> | null = null;
|
|
|
|
/** @override */
|
|
detached() {
|
|
super.detached();
|
|
if (this._builder) {
|
|
this._builder.clear();
|
|
}
|
|
}
|
|
|
|
get diffElement() {
|
|
return this.queryEffectiveChildren('#diffTable') as HTMLTableElement;
|
|
}
|
|
|
|
_computeLeftCoverageRanges(coverageRanges: CoverageRange[]) {
|
|
return coverageRanges.filter(range => range && range.side === 'left');
|
|
}
|
|
|
|
_computeRightCoverageRanges(coverageRanges: CoverageRange[]) {
|
|
return coverageRanges.filter(range => range && range.side === 'right');
|
|
}
|
|
|
|
render(keyLocations: KeyLocations, prefs: DiffPreferencesInfo) {
|
|
// Setting up annotation layers must happen after plugins are
|
|
// installed, and |render| satisfies the requirement, however,
|
|
// |attached| doesn't because in the diff view page, the element is
|
|
// attached before plugins are installed.
|
|
this._setupAnnotationLayers();
|
|
|
|
this._showTabs = !!prefs.show_tabs;
|
|
this._showTrailingWhitespace = !!prefs.show_whitespace_errors;
|
|
|
|
// Stop the processor if it's running.
|
|
this.cancel();
|
|
|
|
if (this._builder) {
|
|
this._builder.clear();
|
|
}
|
|
if (!this.diff) {
|
|
throw Error('Cannot render a diff without DiffInfo.');
|
|
}
|
|
this._builder = this._getDiffBuilder(this.diff, prefs);
|
|
|
|
this.$.processor.context = prefs.context;
|
|
this.$.processor.keyLocations = keyLocations;
|
|
|
|
this._clearDiffContent();
|
|
this._builder.addColumns(this.diffElement, prefs.font_size);
|
|
|
|
const isBinary = !!(this.isImageDiff || this.diff.binary);
|
|
|
|
fireEvent(this, 'render-start');
|
|
this._cancelableRenderPromise = util.makeCancelable(
|
|
this.$.processor.process(this.diff.content, isBinary).then(() => {
|
|
if (this.isImageDiff) {
|
|
(this._builder as GrDiffBuilderImage).renderDiff();
|
|
}
|
|
fireEvent(this, 'render-content');
|
|
})
|
|
);
|
|
return (
|
|
this._cancelableRenderPromise
|
|
.finally(() => {
|
|
this._cancelableRenderPromise = null;
|
|
})
|
|
// Mocca testing does not like uncaught rejections, so we catch
|
|
// the cancels which are expected and should not throw errors in
|
|
// tests.
|
|
.catch(e => {
|
|
if (!e.isCanceled) return Promise.reject(e);
|
|
return;
|
|
})
|
|
);
|
|
}
|
|
|
|
_setupAnnotationLayers() {
|
|
const layers: DiffLayer[] = [
|
|
this._createTrailingWhitespaceLayer(),
|
|
this._createIntralineLayer(),
|
|
this._createTabIndicatorLayer(),
|
|
this.$.rangeLayer,
|
|
this.$.coverageLayerLeft,
|
|
this.$.coverageLayerRight,
|
|
];
|
|
|
|
if (this.layers) {
|
|
layers.push(...this.layers);
|
|
}
|
|
this._layers = layers;
|
|
}
|
|
|
|
getLineElByChild(node?: Node): HTMLElement | null {
|
|
while (node) {
|
|
if (node instanceof Element) {
|
|
if (node.classList.contains('lineNum')) {
|
|
return node as HTMLElement;
|
|
}
|
|
if (node.classList.contains('section')) {
|
|
return null;
|
|
}
|
|
}
|
|
node = node.previousSibling ?? node.parentElement ?? undefined;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getLineNumberByChild(node: Node) {
|
|
const lineEl = this.getLineElByChild(node);
|
|
return getLineNumber(lineEl);
|
|
}
|
|
|
|
getContentTdByLine(lineNumber: LineNumber, side?: Side, root?: Element) {
|
|
if (!this._builder) return null;
|
|
return this._builder.getContentTdByLine(lineNumber, side, root);
|
|
}
|
|
|
|
_getDiffRowByChild(child: Element) {
|
|
while (!child.classList.contains('diff-row') && child.parentElement) {
|
|
child = child.parentElement;
|
|
}
|
|
return child;
|
|
}
|
|
|
|
getContentTdByLineEl(lineEl?: Element): Element | null {
|
|
if (!lineEl) return null;
|
|
const line = getLineNumber(lineEl);
|
|
if (!line) return null;
|
|
const side = this.getSideByLineEl(lineEl);
|
|
// Performance optimization because we already have an element in the
|
|
// correct row
|
|
const row = this._getDiffRowByChild(lineEl);
|
|
return this.getContentTdByLine(line, side, row);
|
|
}
|
|
|
|
getLineElByNumber(lineNumber: LineNumber, side?: Side) {
|
|
const sideSelector = side ? '.' + side : '';
|
|
return this.diffElement.querySelector(
|
|
`.lineNum[data-value="${lineNumber}"]${sideSelector}`
|
|
);
|
|
}
|
|
|
|
getSideByLineEl(lineEl: Element) {
|
|
return lineEl.classList.contains(Side.RIGHT) ? Side.RIGHT : Side.LEFT;
|
|
}
|
|
|
|
emitGroup(group: GrDiffGroup, sectionEl: HTMLElement) {
|
|
if (!this._builder) return;
|
|
this._builder.emitGroup(group, sectionEl);
|
|
}
|
|
|
|
showContext(newGroups: GrDiffGroup[], sectionEl: HTMLElement) {
|
|
if (!this._builder) return;
|
|
const groups = this._builder.groups;
|
|
|
|
const contextIndex = groups.findIndex(group => group.element === sectionEl);
|
|
groups.splice(contextIndex, 1, ...newGroups);
|
|
|
|
for (const newGroup of newGroups) {
|
|
this._builder.emitGroup(newGroup, sectionEl);
|
|
}
|
|
if (sectionEl.parentNode) {
|
|
sectionEl.parentNode.removeChild(sectionEl);
|
|
}
|
|
|
|
this.async(() => fireEvent(this, 'render-content'), 1);
|
|
}
|
|
|
|
cancel() {
|
|
this.$.processor.cancel();
|
|
if (this._cancelableRenderPromise) {
|
|
this._cancelableRenderPromise.cancel();
|
|
this._cancelableRenderPromise = null;
|
|
}
|
|
}
|
|
|
|
_handlePreferenceError(pref: string): never {
|
|
const message =
|
|
`The value of the '${pref}' user preference is ` +
|
|
'invalid. Fix in diff preferences';
|
|
fireAlert(this, message);
|
|
throw Error(`Invalid preference value: ${pref}`);
|
|
}
|
|
|
|
_getDiffBuilder(diff: DiffInfo, prefs: DiffPreferencesInfo): GrDiffBuilder {
|
|
if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
|
|
this._handlePreferenceError('tab size');
|
|
}
|
|
|
|
if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
|
|
this._handlePreferenceError('diff width');
|
|
}
|
|
|
|
const localPrefs = {...prefs};
|
|
if (this.path === COMMIT_MSG_PATH) {
|
|
// override line_length for commit msg the same way as
|
|
// in gr-diff
|
|
localPrefs.line_length = COMMIT_MSG_LINE_LENGTH;
|
|
}
|
|
|
|
let builder = null;
|
|
if (this.isImageDiff) {
|
|
builder = new GrDiffBuilderImage(
|
|
diff,
|
|
localPrefs,
|
|
this.diffElement,
|
|
this.baseImage,
|
|
this.revisionImage
|
|
);
|
|
} else if (diff.binary) {
|
|
// If the diff is binary, but not an image.
|
|
return new GrDiffBuilderBinary(diff, localPrefs, this.diffElement);
|
|
} else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) {
|
|
builder = new GrDiffBuilderSideBySide(
|
|
diff,
|
|
localPrefs,
|
|
this.diffElement,
|
|
this._layers,
|
|
this.useNewContextControls
|
|
);
|
|
} else if (this.viewMode === DiffViewMode.UNIFIED) {
|
|
builder = new GrDiffBuilderUnified(
|
|
diff,
|
|
localPrefs,
|
|
this.diffElement,
|
|
this._layers,
|
|
this.useNewContextControls
|
|
);
|
|
}
|
|
if (!builder) {
|
|
throw Error(`Unsupported diff view mode: ${this.viewMode}`);
|
|
}
|
|
return builder;
|
|
}
|
|
|
|
_clearDiffContent() {
|
|
this.diffElement.innerHTML = '';
|
|
}
|
|
|
|
@observe('_groups.splices')
|
|
_groupsChanged(changeRecord: PolymerSpliceChange<GrDiffGroup[]>) {
|
|
if (!changeRecord || !this._builder) {
|
|
return;
|
|
}
|
|
for (const splice of changeRecord.indexSplices) {
|
|
let group;
|
|
for (let i = 0; i < splice.addedCount; i++) {
|
|
group = splice.object[splice.index + i];
|
|
this._builder.groups.push(group);
|
|
this._builder.emitGroup(group, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
_createIntralineLayer(): DiffLayer {
|
|
return {
|
|
// Take a DIV.contentText element and a line object with intraline
|
|
// differences to highlight and apply them to the element as
|
|
// annotations.
|
|
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
|
|
const HL_CLASS = 'style-scope gr-diff intraline';
|
|
for (const highlight of line.highlights) {
|
|
// The start and end indices could be the same if a highlight is
|
|
// meant to start at the end of a line and continue onto the
|
|
// next one. Ignore it.
|
|
if (highlight.startIndex === highlight.endIndex) {
|
|
continue;
|
|
}
|
|
|
|
// If endIndex isn't present, continue to the end of the line.
|
|
const endIndex =
|
|
highlight.endIndex === undefined
|
|
? line.text.length
|
|
: highlight.endIndex;
|
|
|
|
GrAnnotation.annotateElement(
|
|
contentEl,
|
|
highlight.startIndex,
|
|
endIndex - highlight.startIndex,
|
|
HL_CLASS
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
_createTabIndicatorLayer(): DiffLayer {
|
|
const show = () => this._showTabs;
|
|
return {
|
|
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
|
|
// If visible tabs are disabled, do nothing.
|
|
if (!show()) {
|
|
return;
|
|
}
|
|
|
|
// Find and annotate the locations of tabs.
|
|
const split = line.text.split('\t');
|
|
if (!split) {
|
|
return;
|
|
}
|
|
for (let i = 0, pos = 0; i < split.length - 1; i++) {
|
|
// Skip forward by the length of the content
|
|
pos += split[i].length;
|
|
|
|
GrAnnotation.annotateElement(
|
|
contentEl,
|
|
pos,
|
|
1,
|
|
'style-scope gr-diff tab-indicator'
|
|
);
|
|
|
|
// Skip forward by one tab character.
|
|
pos++;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
_createTrailingWhitespaceLayer(): DiffLayer {
|
|
const show = () => {
|
|
return this._showTrailingWhitespace;
|
|
};
|
|
|
|
return {
|
|
annotate(contentEl: HTMLElement, _: HTMLElement, line: GrDiffLine) {
|
|
if (!show()) {
|
|
return;
|
|
}
|
|
|
|
const match = line.text.match(TRAILING_WHITESPACE_PATTERN);
|
|
if (match) {
|
|
// Normalize string positions in case there is unicode before or
|
|
// within the match.
|
|
const index = GrAnnotation.getStringLength(
|
|
line.text.substr(0, match.index)
|
|
);
|
|
const length = GrAnnotation.getStringLength(match[0]);
|
|
GrAnnotation.annotateElement(
|
|
contentEl,
|
|
index,
|
|
length,
|
|
'style-scope gr-diff trailing-whitespace'
|
|
);
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
setBlame(blame: BlameInfo[] | null) {
|
|
if (!this._builder) return;
|
|
this._builder.setBlame(blame);
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'gr-diff-builder': GrDiffBuilderElement;
|
|
}
|
|
}
|