 41b0f951fc
			
		
	
	41b0f951fc
	
	
	
		
			
			This change decouples gr-diff-builder and gr-syntax-layer. gr-syntax- layer will now live in gr-diff-host. As a result, from the perspective of gr-diff/gr-diff-builder, plugin layers and the syntax layer will now be part of the same list. (This change fixes a time reporting regression in https://gerrit-review.googlesource.com/c/gerrit/+/239972.) A future potential refactor could also lift gr-coverage-layer instances in a similar fashion. Change-Id: I47e91fdac00e17460aab9ca57e4569ade63c2eeb
		
			
				
	
	
		
			426 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			426 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!--
 | |
| @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="../../../behaviors/fire-behavior/fire-behavior.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">
 | |
| 
 | |
| <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-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',
 | |
|       };
 | |
| 
 | |
|       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.
 | |
|          *
 | |
|          * @event render-content
 | |
|          */
 | |
| 
 | |
|         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,
 | |
|           layers: {
 | |
|             type: Array,
 | |
|             value: [],
 | |
|           },
 | |
|         },
 | |
| 
 | |
|         behaviors: [
 | |
|           Gerrit.FireBehavior,
 | |
|         ],
 | |
| 
 | |
|         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._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 = 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, ...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;
 | |
|           }
 | |
| 
 | |
|           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');
 | |
|               }
 | |
|             },
 | |
|           };
 | |
|         },
 | |
| 
 | |
|         setBlame(blame) {
 | |
|           if (!this._builder || !blame) { return; }
 | |
|           this._builder.setBlame(blame);
 | |
|         },
 | |
|       });
 | |
|     })();
 | |
|   </script>
 | |
| </dom-module>
 |