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>
 |