| <!-- | |
| @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; | |
| 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, | |
| _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._anyLineTooLong()) { | |
| 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; | |
| }, | |
| createCommentThreadGroup(changeNum, patchNum, path, | |
| isOnParent, commentSide) { | |
| return this._builder.createCommentThreadGroup(changeNum, patchNum, | |
| path, isOnParent, commentSide); | |
| }, | |
| 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; | |
| if (this.isImageDiff) { | |
| builder = new GrDiffBuilderImage(diff, comments, prefs, | |
| this.projectName, 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.projectName, this.diffElement); | |
| } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { | |
| builder = new GrDiffBuilderSideBySide(diff, comments, prefs, | |
| this.projectName, this.diffElement, this._layers); | |
| } else if (this.viewMode === DiffViewMode.UNIFIED) { | |
| builder = new GrDiffBuilderUnified(diff, comments, prefs, | |
| this.projectName, this.diffElement, this._layers); | |
| } | |
| if (!builder) { | |
| throw Error('Unsupported diff view mode: ' + this.viewMode); | |
| } | |
| if (this.parentIndex) { | |
| builder.setParentIndex(this.parentIndex); | |
| } | |
| 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); | |
| }, | |
| setBlame(blame) { | |
| if (!this._builder || !blame) { return; } | |
| this._builder.setBlame(blame); | |
| }, | |
| }); | |
| })(); | |
| </script> | |
| </dom-module> |