| <!-- |
| @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. |
| --> |
| <link rel="import" href="../../../bower_components/polymer/polymer.html"> |
| <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> |
| <link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html"> |
| <link rel="import" href="../gr-diff-processor/gr-diff-processor.html"> |
| <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html"> |
| <link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html"> |
| |
| <dom-module id="gr-diff-builder"> |
| <template> |
| <div class="contentWrapper"> |
| <slot></slot> |
| </div> |
| <gr-ranged-comment-layer |
| id="rangeLayer" |
| comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer> |
| <gr-syntax-layer |
| id="syntaxLayer" |
| diff="[[diff]]"></gr-syntax-layer> |
| <gr-coverage-layer |
| id="coverageLayerLeft" |
| coverage-ranges="[[_leftCoverageRanges]]" |
| side="left"></gr-coverage-layer> |
| <gr-coverage-layer |
| id="coverageLayerRight" |
| coverage-ranges="[[_rightCoverageRanges]]" |
| side="right"></gr-coverage-layer> |
| <gr-diff-processor |
| id="processor" |
| groups="{{_groups}}"></gr-diff-processor> |
| <gr-js-api-interface id="jsAPI"></gr-js-api-interface> |
| </template> |
| <script src="../../../scripts/util.js"></script> |
| <script src="../gr-diff/gr-diff-line.js"></script> |
| <script src="../gr-diff/gr-diff-group.js"></script> |
| <script src="../gr-diff-highlight/gr-annotation.js"></script> |
| <script src="gr-diff-builder.js"></script> |
| <script src="gr-diff-builder-side-by-side.js"></script> |
| <script src="gr-diff-builder-unified.js"></script> |
| <script src="gr-diff-builder-image.js"></script> |
| <script src="gr-diff-builder-binary.js"></script> |
| <script> |
| (function() { |
| 'use strict'; |
| |
| const DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| // If any line of the diff is more than the character limit, then disable |
| // syntax highlighting for the entire file. |
| const SYNTAX_MAX_LINE_LENGTH = 500; |
| |
| // Disable syntax highlighting if the overall diff is too large. |
| const SYNTAX_MAX_DIFF_LENGTH = 20000; |
| |
| const TRAILING_WHITESPACE_PATTERN = /\s+$/; |
| |
| Polymer({ |
| is: 'gr-diff-builder', |
| _legacyUndefinedCheck: true, |
| |
| /** |
| * Fired when the diff begins rendering. |
| * |
| * @event render-start |
| */ |
| |
| /** |
| * Fired when the diff finishes rendering text content and starts |
| * syntax highlighting. |
| * |
| * @event render-content |
| */ |
| |
| /** |
| * Fired when the diff finishes syntax highlighting. |
| * |
| * @event render-syntax |
| */ |
| |
| properties: { |
| diff: Object, |
| diffPath: String, |
| 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, |
| }, |
| |
| get diffElement() { |
| return this.queryEffectiveChildren('#diffTable'); |
| }, |
| |
| observers: [ |
| '_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.$.syntaxLayer.enabled = prefs.syntax_highlighting; |
| this._showTabs = !!prefs.show_tabs; |
| this._showTrailingWhitespace = !!prefs.show_whitespace_errors; |
| |
| // Stop the processor and syntax layer (if they're 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})); |
| 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})); |
| |
| if (this._diffTooLargeForSyntax()) { |
| this.$.syntaxLayer.enabled = false; |
| } |
| |
| return this.$.syntaxLayer.process(); |
| }) |
| .then(() => { |
| this.dispatchEvent( |
| new CustomEvent('render-syntax', {bubbles: 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.$.syntaxLayer, |
| this._createIntralineLayer(), |
| this._createTabIndicatorLayer(), |
| this.$.rangeLayer, |
| this.$.coverageLayerLeft, |
| this.$.coverageLayerRight, |
| ]; |
| |
| // Get layers from plugins (if any). |
| for (const pluginLayer of this.$.jsAPI.getDiffLayers( |
| this.diffPath, this.changeNum, this.patchNum)) { |
| layers.push(pluginLayer); |
| } |
| |
| 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 = Polymer.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].concat(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(); |
| this.$.syntaxLayer.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})); |
| 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; |
| } |
| |
| let builder = null; |
| if (this.isImageDiff) { |
| builder = new GrDiffBuilderImage(diff, prefs, this.diffElement, |
| this.baseImage, this.revisionImage); |
| } else if (diff.binary) { |
| // If the diff is binary, but not an image. |
| return new GrDiffBuilderBinary(diff, prefs, this.diffElement); |
| } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { |
| builder = new GrDiffBuilderSideBySide(diff, prefs, this.diffElement, |
| this._layers); |
| } else if (this.viewMode === DiffViewMode.UNIFIED) { |
| builder = new GrDiffBuilderUnified(diff, prefs, 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'); |
| } |
| }, |
| }; |
| }, |
| |
| /** |
| * @return {boolean} whether any of the lines in _groups are longer |
| * than SYNTAX_MAX_LINE_LENGTH. |
| */ |
| _anyLineTooLong() { |
| return this._groups.reduce((acc, group) => { |
| return acc || group.lines.reduce((acc, line) => { |
| return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH; |
| }, false); |
| }, false); |
| }, |
| |
| _diffTooLargeForSyntax() { |
| return this._anyLineTooLong() || |
| this.getDiffLength() > SYNTAX_MAX_DIFF_LENGTH; |
| }, |
| |
| setBlame(blame) { |
| if (!this._builder || !blame) { return; } |
| this._builder.setBlame(blame); |
| }, |
| |
| /** |
| * Get the approximate length of the diff as the sum of the maximum |
| * length of the chunks. |
| * |
| * @return {number} |
| */ |
| getDiffLength() { |
| return this.diff.content.reduce((sum, sec) => { |
| if (sec.hasOwnProperty('ab')) { |
| return sum + sec.ab.length; |
| } else { |
| return sum + Math.max( |
| sec.hasOwnProperty('a') ? sec.a.length : 0, |
| sec.hasOwnProperty('b') ? sec.b.length : 0); |
| } |
| }, 0); |
| }, |
| }); |
| })(); |
| </script> |
| </dom-module> |