| <!-- |
| @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="../../core/gr-reporting/gr-reporting.html"> |
| <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> |
| <link rel="import" href="../gr-diff-comment-thread-group/gr-diff-comment-thread-group.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" |
| comments="[[comments]]"></gr-ranged-comment-layer> |
| <gr-syntax-layer |
| id="syntaxLayer" |
| diff="[[diff]]"></gr-syntax-layer> |
| <gr-diff-processor |
| id="processor" |
| groups="{{_groups}}"></gr-diff-processor> |
| <gr-reporting id="reporting"></gr-reporting> |
| <gr-js-api-interface id="jsAPI"></gr-js-api-interface> |
| </template> |
| <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 Defs = {}; |
| |
| /** |
| * @typedef {{ |
| * number: number, |
| * leftSide: {boolean} |
| * }} |
| */ |
| Defs.LineOfInterest; |
| |
| const DiffViewMode = { |
| SIDE_BY_SIDE: 'SIDE_BY_SIDE', |
| UNIFIED: 'UNIFIED_DIFF', |
| }; |
| |
| const TimingLabel = { |
| TOTAL: 'Diff Total Render', |
| CONTENT: 'Diff Content Render', |
| SYNTAX: 'Diff Syntax Render', |
| }; |
| |
| // 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', |
| |
| /** |
| * Fired when the diff begins rendering. |
| * |
| * @event render-start |
| */ |
| |
| /** |
| * Fired when the diff is rendered. |
| * |
| * @event render |
| */ |
| |
| /** |
| * Fired when the diff finishes rendering text content, but not |
| * necessarily syntax highlights. |
| * |
| * @event render-content |
| */ |
| |
| properties: { |
| diff: Object, |
| diffPath: String, |
| changeNum: String, |
| patchNum: String, |
| viewMode: String, |
| comments: Object, |
| isImageDiff: Boolean, |
| baseImage: Object, |
| revisionImage: Object, |
| projectName: String, |
| parentIndex: Number, |
| /** |
| * @type {Defs.LineOfInterest|null} |
| */ |
| lineOfInterest: Object, |
| |
| /** |
| * @type {function(number, booleam, !string)} |
| */ |
| createCommentFn: Function, |
| |
| _builder: Object, |
| _groups: Array, |
| _layers: Array, |
| _showTabs: Boolean, |
| }, |
| |
| get diffElement() { |
| return this.queryEffectiveChildren('#diffTable'); |
| }, |
| |
| observers: [ |
| '_groupsChanged(_groups.splices)', |
| ], |
| |
| attached() { |
| // Setup annotation layers. |
| const layers = [ |
| this._createTrailingWhitespaceLayer(), |
| this.$.syntaxLayer, |
| this._createIntralineLayer(), |
| this._createTabIndicatorLayer(), |
| this.$.rangeLayer, |
| ]; |
| |
| // 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; |
| |
| this.async(() => { |
| this._preRenderThread(); |
| }); |
| }, |
| |
| render(comments, prefs) { |
| 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, comments, prefs); |
| |
| this.$.processor.context = prefs.context; |
| this.$.processor.keyLocations = this._getKeyLocations(comments, |
| this.lineOfInterest); |
| |
| this._clearDiffContent(); |
| this._builder.addColumns(this.diffElement, prefs.font_size); |
| |
| const reporting = this.$.reporting; |
| const isBinary = !!(this.isImageDiff || this.diff.binary); |
| |
| reporting.time(TimingLabel.TOTAL); |
| reporting.time(TimingLabel.CONTENT); |
| this.dispatchEvent(new CustomEvent('render-start', {bubbles: true})); |
| return 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; |
| } |
| |
| reporting.timeEnd(TimingLabel.CONTENT); |
| reporting.time(TimingLabel.SYNTAX); |
| return this.$.syntaxLayer.process().then(() => { |
| reporting.timeEnd(TimingLabel.SYNTAX); |
| reporting.timeEnd(TimingLabel.TOTAL); |
| this.dispatchEvent( |
| new CustomEvent('render', {bubbles: true})); |
| }); |
| }); |
| }, |
| |
| 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(); |
| }, |
| |
| _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, comments, 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; |
| const createFn = this.createCommentFn; |
| if (this.isImageDiff) { |
| builder = new GrDiffBuilderImage(diff, comments, createFn, prefs, |
| this.diffElement, this.baseImage, this.revisionImage); |
| } else if (diff.binary) { |
| // If the diff is binary, but not an image. |
| return new GrDiffBuilderBinary(diff, comments, prefs, |
| this.diffElement); |
| } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { |
| builder = new GrDiffBuilderSideBySide(diff, comments, createFn, |
| prefs, this.diffElement, this._layers); |
| } else if (this.viewMode === DiffViewMode.UNIFIED) { |
| builder = new GrDiffBuilderUnified(diff, comments, createFn, prefs, |
| this.diffElement, this._layers); |
| } |
| if (!builder) { |
| throw Error('Unsupported diff view mode: ' + this.viewMode); |
| } |
| return builder; |
| }, |
| |
| _clearDiffContent() { |
| this.diffElement.innerHTML = null; |
| }, |
| |
| /** |
| * @param {!Object} comments |
| * @param {Defs.LineOfInterest|null} lineOfInterest |
| */ |
| _getKeyLocations(comments, lineOfInterest) { |
| const result = { |
| left: {}, |
| right: {}, |
| }; |
| for (const side in comments) { |
| if (side !== GrDiffBuilder.Side.LEFT && |
| side !== GrDiffBuilder.Side.RIGHT) { |
| continue; |
| } |
| for (const c of comments[side]) { |
| result[side][c.line || GrDiffLine.FILE] = true; |
| } |
| } |
| |
| if (lineOfInterest) { |
| const side = lineOfInterest.leftSide ? 'left' : 'right'; |
| result[side][lineOfInterest.number] = true; |
| } |
| |
| return result; |
| }, |
| |
| _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(el, 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( |
| el, |
| highlight.startIndex, |
| endIndex - highlight.startIndex, |
| HL_CLASS); |
| } |
| }, |
| }; |
| }, |
| |
| _createTabIndicatorLayer() { |
| const show = () => this._showTabs; |
| return { |
| annotate(el, 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(el, 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(el, 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(el, index, length, |
| 'style-scope gr-diff trailing-whitespace'); |
| } |
| }, |
| }; |
| }, |
| |
| /** |
| * In pages with large diffs, creating the first comment thread can be |
| * slow because nested Polymer elements (particularly |
| * iron-autogrow-textarea) add style elements to the document head, |
| * which, in turn, triggers a reflow on the page. Create a hidden |
| * thread, attach it to the page, and remove it so the stylesheet will |
| * already exist and the user's comment will be quick to load. |
| * @see https://gerrit-review.googlesource.com/c/82213/ |
| */ |
| _preRenderThread() { |
| const thread = document.createElement('gr-diff-comment-thread'); |
| thread.setAttribute('hidden', true); |
| thread.addDraft(); |
| const parent = Polymer.dom(this.root); |
| parent.appendChild(thread); |
| Polymer.dom.flush(); |
| parent.removeChild(thread); |
| }, |
| |
| /** |
| * @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> |