/** * @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 '../../../scripts/bundled-polymer.js'; import '../../../behaviors/fire-behavior/fire-behavior.js'; import '../gr-coverage-layer/gr-coverage-layer.js'; import '../gr-diff-processor/gr-diff-processor.js'; import '../../shared/gr-hovercard/gr-hovercard.js'; import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js'; import '../../../scripts/util.js'; import '../gr-diff/gr-diff-line.js'; import '../gr-diff/gr-diff-group.js'; import '../gr-diff-highlight/gr-annotation.js'; import './gr-diff-builder.js'; import './gr-diff-builder-side-by-side.js'; import './gr-diff-builder-unified.js'; import './gr-diff-builder-image.js'; import './gr-diff-builder-binary.js'; import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; import {PolymerElement} from '@polymer/polymer/polymer-element.js'; import {htmlTemplate} from './gr-diff-builder-element_html.js'; 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; /** * @appliesMixin Gerrit.FireMixin */ class GrDiffBuilderElement extends mixinBehaviors( [ Gerrit.FireBehavior, ], GestureEventListeners( LegacyElementMixin( PolymerElement))) { static get template() { return htmlTemplate; } static get is() { return 'gr-diff-builder'; } /** * Fired when the diff begins rendering. * * @event render-start */ /** * Fired when the diff finishes rendering text content. * * @event render-content */ static get properties() { return { diff: Object, changeNum: String, patchNum: String, viewMode: String, isImageDiff: Boolean, baseImage: Object, revisionImage: Object, parentIndex: Number, path: String, projectName: String, _builder: Object, _groups: Array, _layers: Array, _showTabs: Boolean, /** @type {!Array} */ commentRanges: { type: Array, value: () => [], }, /** @type {!Array} */ coverageRanges: { type: Array, value: () => [], }, _leftCoverageRanges: { type: Array, computed: '_computeLeftCoverageRanges(coverageRanges)', }, _rightCoverageRanges: { type: Array, computed: '_computeRightCoverageRanges(coverageRanges)', }, /** * The promise last returned from `render()` while the asynchronous * rendering is running - `null` otherwise. Provides a `cancel()` * method that rejects it with `{isCancelled: true}`. * * @type {?Object} */ _cancelableRenderPromise: Object, layers: { type: Array, value: [], }, }; } get diffElement() { return this.queryEffectiveChildren('#diffTable'); } static get observers() { return [ '_groupsChanged(_groups.splices)', ]; } _computeLeftCoverageRanges(coverageRanges) { return coverageRanges.filter(range => range && range.side === 'left'); } _computeRightCoverageRanges(coverageRanges) { return coverageRanges.filter(range => range && range.side === 'right'); } render(keyLocations, prefs) { // 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(); 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); this.dispatchEvent(new CustomEvent( 'render-start', {bubbles: true, composed: true})); this._cancelableRenderPromise = util.makeCancelable( this.$.processor.process(this.diff.content, isBinary) .then(() => { if (this.isImageDiff) { this._builder.renderDiff(); } this.dispatchEvent(new CustomEvent('render-content', {bubbles: true, composed: true})); })); 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); }); } _setupAnnotationLayers() { const layers = [ this._createTrailingWhitespaceLayer(), this._createIntralineLayer(), this._createTabIndicatorLayer(), this.$.rangeLayer, this.$.coverageLayerLeft, this.$.coverageLayerRight, ]; if (this.layers) { layers.push(...this.layers); } this._layers = layers; } getLineElByChild(node) { while (node) { if (node instanceof Element) { if (node.classList.contains('lineNum')) { return node; } if (node.classList.contains('section')) { return null; } } node = node.previousSibling || node.parentElement; } return null; } getLineNumberByChild(node) { const lineEl = this.getLineElByChild(node); return lineEl ? parseInt(lineEl.getAttribute('data-value'), 10) : null; } getContentByLine(lineNumber, opt_side, opt_root) { return this._builder.getContentByLine(lineNumber, opt_side, opt_root); } getContentByLineEl(lineEl) { const root = dom(lineEl.parentElement); const side = this.getSideByLineEl(lineEl); const line = lineEl.getAttribute('data-value'); return this.getContentByLine(line, side, root); } getLineElByNumber(lineNumber, opt_side) { const sideSelector = opt_side ? ('.' + opt_side) : ''; return this.diffElement.querySelector( '.lineNum[data-value="' + lineNumber + '"]' + sideSelector); } getContentsByLineRange(startLine, endLine, opt_side) { const result = []; this._builder.findLinesByRange(startLine, endLine, opt_side, null, result); return result; } getSideByLineEl(lineEl) { return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; } emitGroup(group, sectionEl) { this._builder.emitGroup(group, sectionEl); } showContext(newGroups, sectionEl) { 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); } sectionEl.parentNode.removeChild(sectionEl); this.async(() => this.fire('render-content'), 1); } cancel() { this.$.processor.cancel(); if (this._cancelableRenderPromise) { this._cancelableRenderPromise.cancel(); this._cancelableRenderPromise = null; } } _handlePreferenceError(pref) { const message = `The value of the '${pref}' user preference is ` + `invalid. Fix in diff preferences`; this.dispatchEvent(new CustomEvent('show-alert', { detail: { message, }, bubbles: true, composed: true})); throw Error(`Invalid preference value: ${pref}`); } _getDiffBuilder(diff, prefs) { if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) { this._handlePreferenceError('tab size'); return; } if (isNaN(prefs.line_length) || prefs.line_length <= 0) { this._handlePreferenceError('diff width'); return; } const localPrefs = Object.assign({}, 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 ); } else if (this.viewMode === DiffViewMode.UNIFIED) { builder = new GrDiffBuilderUnified( diff, localPrefs, this.diffElement, this._layers); } if (!builder) { throw Error('Unsupported diff view mode: ' + this.viewMode); } return builder; } _clearDiffContent() { this.diffElement.innerHTML = null; } _groupsChanged(changeRecord) { if (!changeRecord) { 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); } } } _createIntralineLayer() { 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, lineNumberEl, line) { 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() { const show = () => this._showTabs; return { annotate(contentEl, lineNumberEl, line) { // 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() { const show = function() { return this._showTrailingWhitespace; }.bind(this); return { annotate(contentEl, lineNumberEl, line) { 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) { if (!this._builder || !blame) { return; } this._builder.setBlame(blame); } } customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);