| /** |
| * @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.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 './gr-diff-builder-side-by-side.js'; |
| import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.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'; |
| import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js'; |
| import {GrDiffBuilder} from './gr-diff-builder.js'; |
| import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js'; |
| import {GrDiffBuilderImage} from './gr-diff-builder-image.js'; |
| import {GrDiffBuilderUnified} from './gr-diff-builder-unified.js'; |
| import {GrDiffBuilderBinary} from './gr-diff-builder-binary.js'; |
| import {util} from '../../../scripts/util.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; |
| |
| /** |
| * @extends PolymerElement |
| */ |
| class GrDiffBuilderElement extends 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<!Gerrit.HoveredRange>} */ |
| commentRanges: { |
| type: Array, |
| value: () => [], |
| }, |
| /** @type {!Array<!Gerrit.CoverageRange>} */ |
| 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: [], |
| }, |
| }; |
| } |
| |
| /** @override */ |
| detached() { |
| super.detached(); |
| if (this._builder) { |
| this._builder.clear(); |
| } |
| } |
| |
| 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(); |
| |
| if (this._builder) { |
| this._builder.clear(); |
| } |
| 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; |
| } |
| |
| getContentTdByLine(lineNumber, opt_side, opt_root) { |
| return this._builder.getContentTdByLine(lineNumber, opt_side, opt_root); |
| } |
| |
| _getDiffRowByChild(child) { |
| while (!child.classList.contains('diff-row') && child.parentElement) { |
| child = child.parentElement; |
| } |
| return child; |
| } |
| |
| getContentTdByLineEl(lineEl) { |
| if (!lineEl) return; |
| const line = lineEl.getAttribute('data-value'); |
| const side = this.getSideByLineEl(lineEl); |
| // Performance optimization because we already have an element in the |
| // correct row |
| const row = dom(this._getDiffRowByChild(lineEl)); |
| return this.getContentTdByLine(line, side, row); |
| } |
| |
| 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.dispatchEvent(new CustomEvent('render-content', { |
| composed: true, bubbles: true, |
| })), 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); |