| <!-- |
| Copyright (C) 2015 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="gr-ajax.html"> |
| <link rel="import" href="gr-diff-side.html"> |
| <link rel="import" href="gr-patch-range-select.html"> |
| <link rel="import" href="gr-request.html"> |
| |
| <dom-module id="gr-diff"> |
| <template> |
| <style> |
| .header { |
| display: flex; |
| margin: 0 var(--default-horizontal-margin) .75em; |
| } |
| .contextControl { |
| flex: 1; |
| text-align: right; |
| } |
| .diffContainer { |
| border-bottom: 1px solid #eee; |
| border-top: 1px solid #eee; |
| display: flex; |
| font-family: 'Source Code Pro', monospace; |
| overflow-x: auto; |
| white-space: pre; |
| } |
| gr-diff-side:first-of-type { |
| --light-highlight-color: #fee; |
| --dark-highlight-color: #ffd4d4; |
| } |
| gr-diff-side:last-of-type { |
| --light-highlight-color: #efe; |
| --dark-highlight-color: #d4ffd4; |
| border-right: 1px solid #ddd; |
| } |
| </style> |
| <gr-ajax id="diffXHR" |
| url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]" |
| params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]" |
| last-response="{{_diffResponse}}"></gr-ajax> |
| <gr-ajax id="baseCommentsXHR" |
| url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> |
| <gr-ajax id="commentsXHR" |
| url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> |
| <gr-ajax id="baseDraftsXHR" |
| url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax> |
| <gr-ajax id="draftsXHR" |
| url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax> |
| |
| <div class="header"> |
| <gr-patch-range-select |
| path="[[path]]" |
| change-num="[[changeNum]]" |
| patch-range="[[patchRange]]" |
| available-patches="[[availablePatches]]"></gr-patch-range-select> |
| <div class="contextControl"> |
| Context: |
| <select id="contextSelect" on-change="_handleContextSelectChange"> |
| <option value="3">3 lines</option> |
| <option value="10">10 lines</option> |
| <option value="25">25 lines</option> |
| <option value="50">50 lines</option> |
| <option value="75">75 lines</option> |
| <option value="100">100 lines</option> |
| <option value="ALL">Whole file</option> |
| </select> |
| </div> |
| </div> |
| |
| <div class="diffContainer"> |
| <gr-diff-side id="leftDiff" |
| change-num="[[changeNum]]" |
| patch-num="[[patchRange.basePatchNum]]" |
| path="[[path]]" |
| content="{{_diff.leftSide}}" |
| width="[[sideWidth]]" |
| can-comment="[[_loggedIn]]" |
| on-expand-context="_handleExpandContext" |
| on-thread-height-change="_handleThreadHeightChange" |
| on-add-draft="_handleAddDraft" |
| on-remove-thread="_handleRemoveThread"></gr-diff-side> |
| <gr-diff-side id="rightDiff" |
| change-num="[[changeNum]]" |
| patch-num="[[patchRange.patchNum]]" |
| path="[[path]]" |
| content="{{_diff.rightSide}}" |
| width="[[sideWidth]]" |
| can-comment="[[_loggedIn]]" |
| on-expand-context="_handleExpandContext" |
| on-thread-height-change="_handleThreadHeightChange" |
| on-add-draft="_handleAddDraft" |
| on-remove-thread="_handleRemoveThread"></gr-diff-side> |
| </div> |
| </template> |
| <script> |
| (function() { |
| 'use strict'; |
| |
| Polymer({ |
| is: 'gr-diff', |
| |
| /** |
| * Fired when the diff is rendered. |
| * |
| * @event render |
| */ |
| |
| properties: { |
| auto: { |
| type: Boolean, |
| value: false, |
| }, |
| availablePatches: Array, |
| changeNum: String, |
| /* |
| * A single object to encompass basePatchNum and patchNum is used |
| * so that both can be set at once without incremental observers |
| * firing after each property changes. |
| */ |
| patchRange: Object, |
| path: String, |
| sideWidth: { |
| type: Number, |
| value: 80, |
| }, |
| _context: { |
| type: Number, |
| value: 10, |
| observer: '_contextChanged', |
| }, |
| _baseComments: Array, |
| _comments: Array, |
| _drafts: Array, |
| _baseDrafts: Array, |
| /** |
| * Base (left side) comments and drafts grouped by line number. |
| */ |
| _groupedBaseComments: { |
| type: Object, |
| value: function() { return {}; }, |
| }, |
| /** |
| * Comments and drafts (right side) grouped by line number. |
| */ |
| _groupedComments: { |
| type: Object, |
| value: function() { return {}; }, |
| }, |
| _diffResponse: Object, |
| _diff: { |
| type: Object, |
| value: function() { return {}; }, |
| }, |
| _loggedIn: { |
| type: Boolean, |
| value: false, |
| }, |
| _diffRequestsPromise: Object, // Used for testing. |
| }, |
| |
| observers: [ |
| '_diffOptionsChanged(changeNum, patchRange, path)' |
| ], |
| |
| ready: function() { |
| app.accountReady.then(function() { |
| this._loggedIn = app.loggedIn; |
| }.bind(this)); |
| }, |
| |
| scrollToLine: function(lineNum) { |
| // TODO(andybons): Should this always be the right side? |
| this.$.rightDiff.scrollToLine(lineNum); |
| }, |
| |
| _contextChanged: function(context) { |
| if (context == Infinity) { |
| this.$.contextSelect.value = 'ALL'; |
| } else { |
| this.$.contextSelect.value = context; |
| } |
| }, |
| |
| _diffOptionsChanged: function(changeNum, patchRange, path) { |
| if (!this.auto) { return; } |
| |
| var promises = [this.$.diffXHR.generateRequest().completes]; |
| |
| var basePatchNum = patchRange.basePatchNum; |
| var patchNum = patchRange.patchNum; |
| |
| app.accountReady.then(function() { |
| promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn)); |
| this._diffRequestsPromise = Promise.all(promises).then(function() { |
| this._render(); |
| }.bind(this)).catch(function(err) { |
| alert('Oops. Something went wrong. Check the console and bug the ' + |
| 'PolyGerrit team for assistance.'); |
| throw err; |
| }); |
| }.bind(this)); |
| }, |
| |
| _render: function() { |
| this._groupCommentsAndDrafts(); |
| this._processContent(); |
| |
| // Allow for the initial rendering to complete before firing the event. |
| this.async(function() { |
| this.fire('render', null, {bubbles: false}); |
| }.bind(this), 1); |
| }, |
| |
| _getCommentsAndDrafts: function(basePatchNum, loggedIn) { |
| var promises = []; |
| |
| function onlyParent(c) { return c.side == 'PARENT'; } |
| function withoutParent(c) { return c.side != 'PARENT'; } |
| |
| var promises = []; |
| var commentsPromise = this.$.commentsXHR.generateRequest().completes; |
| promises.push(commentsPromise.then(function(req) { |
| var comments = req.response[this.path] || []; |
| if (basePatchNum == 'PARENT') { |
| this._baseComments = comments.filter(onlyParent); |
| } |
| this._comments = comments.filter(withoutParent); |
| }.bind(this))); |
| |
| if (basePatchNum != 'PARENT') { |
| commentsPromise = this.$.baseCommentsXHR.generateRequest().completes; |
| promises.push(commentsPromise.then(function(req) { |
| this._baseComments = |
| (req.response[this.path] || []).filter(withoutParent); |
| }.bind(this))); |
| } |
| |
| if (!loggedIn) { |
| this._baseDrafts = []; |
| this._drafts = []; |
| return Promise.all(promises); |
| } |
| |
| var draftsPromise = this.$.draftsXHR.generateRequest().completes; |
| promises.push(draftsPromise.then(function(req) { |
| var drafts = req.response[this.path] || []; |
| if (basePatchNum == 'PARENT') { |
| this._baseDrafts = drafts.filter(onlyParent); |
| } |
| this._drafts = drafts.filter(withoutParent); |
| }.bind(this))); |
| |
| if (basePatchNum != 'PARENT') { |
| draftsPromise = this.$.baseDraftsXHR.generateRequest().completes; |
| promises.push(draftsPromise.then(function(req) { |
| this._baseDrafts = |
| (req.response[this.path] || []).filter(withoutParent); |
| }.bind(this))); |
| } |
| |
| return Promise.all(promises); |
| }, |
| |
| _computeDiffPath: function(changeNum, patchNum, path) { |
| return Changes.baseURL(changeNum, patchNum) + '/files/' + |
| encodeURIComponent(path) + '/diff'; |
| }, |
| |
| _computeCommentsPath: function(changeNum, patchNum) { |
| return Changes.baseURL(changeNum, patchNum) + '/comments'; |
| }, |
| |
| _computeDraftsPath: function(changeNum, patchNum) { |
| return Changes.baseURL(changeNum, patchNum) + '/drafts'; |
| }, |
| |
| _computeDiffQueryParams: function(basePatchNum) { |
| var params = { |
| context: 'ALL', |
| intraline: null |
| }; |
| if (basePatchNum != 'PARENT') { |
| params.base = basePatchNum; |
| } |
| return params; |
| }, |
| |
| _handleContextSelectChange: function(e) { |
| var selectEl = Polymer.dom(e).rootTarget; |
| if (selectEl.value == 'ALL') { |
| this._context = Infinity; |
| } else { |
| this._context = parseInt(selectEl.value, 10); |
| } |
| this._render(); |
| }, |
| |
| _handleExpandContext: function(e) { |
| var ctx = e.detail.context; |
| var contextControlIndex = -1; |
| for (var i = ctx.start; i <= ctx.end; i++) { |
| this._diff.leftSide[i].hidden = false; |
| this._diff.rightSide[i].hidden = false; |
| if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' && |
| this._diff.rightSide[i].type == 'CONTEXT_CONTROL') { |
| contextControlIndex = i; |
| } |
| } |
| this._diff.leftSide[contextControlIndex].hidden = true; |
| this._diff.rightSide[contextControlIndex].hidden = true; |
| |
| this.$.leftDiff.hideElementsWithIndex(contextControlIndex); |
| this.$.rightDiff.hideElementsWithIndex(contextControlIndex); |
| |
| this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end); |
| this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end); |
| }, |
| |
| _handleThreadHeightChange: function(e) { |
| var index = e.detail.index; |
| var diffEl = Polymer.dom(e).rootTarget; |
| var otherSide = diffEl == this.$.leftDiff ? |
| this.$.rightDiff : this.$.leftDiff; |
| |
| var threadHeight = e.detail.height; |
| var otherSideHeight; |
| if (otherSide.content[index].type == 'COMMENT_THREAD') { |
| otherSideHeight = otherSide.getRowNaturalHeight(index); |
| } else { |
| otherSideHeight = otherSide.getRowHeight(index); |
| } |
| var maxHeight = Math.max(threadHeight, otherSideHeight); |
| this.$.leftDiff.setRowHeight(index, maxHeight); |
| this.$.rightDiff.setRowHeight(index, maxHeight); |
| }, |
| |
| _handleAddDraft: function(e) { |
| var insertIndex = e.detail.index + 1; |
| var diffEl = Polymer.dom(e).rootTarget; |
| var content = diffEl.content; |
| if (content[insertIndex] && |
| content[insertIndex].type == 'COMMENT_THREAD') { |
| // A thread is already here. Do nothing. |
| return; |
| } |
| var comment = { |
| type: 'COMMENT_THREAD', |
| comments: [{ |
| __draft: true, |
| __draftID: Math.random().toString(36), |
| line: e.detail.line, |
| path: this.path, |
| }] |
| }; |
| if (content[insertIndex] && |
| content[insertIndex].type == 'FILLER') { |
| content[insertIndex] = comment; |
| diffEl.rowUpdated(insertIndex); |
| } else { |
| content.splice(insertIndex, 0, comment); |
| diffEl.rowInserted(insertIndex); |
| } |
| |
| var otherSide = diffEl == this.$.leftDiff ? |
| this.$.rightDiff : this.$.leftDiff; |
| if (otherSide.content[insertIndex] == null || |
| otherSide.content[insertIndex].type != 'COMMENT_THREAD') { |
| otherSide.content.splice(insertIndex, 0, { |
| type: 'FILLER', |
| }); |
| otherSide.rowInserted(insertIndex); |
| } |
| }, |
| |
| _handleRemoveThread: function(e) { |
| var diffEl = Polymer.dom(e).rootTarget; |
| var otherSide = diffEl == this.$.leftDiff ? |
| this.$.rightDiff : this.$.leftDiff; |
| var index = e.detail.index; |
| |
| if (otherSide.content[index].type == 'FILLER') { |
| otherSide.content.splice(index, 1); |
| otherSide.rowRemoved(index); |
| diffEl.content.splice(index, 1); |
| diffEl.rowRemoved(index); |
| } else if (otherSide.content[index].type == 'COMMENT_THREAD') { |
| diffEl.content[index] = {type: 'FILLER'}; |
| diffEl.rowUpdated(index); |
| var height = otherSide.setRowNaturalHeight(index); |
| diffEl.setRowHeight(index, height); |
| } else { |
| throw Error('A thread cannot be opposite anything but filler or ' + |
| 'another thread'); |
| } |
| }, |
| |
| _processContent: function() { |
| var leftSide = []; |
| var rightSide = []; |
| var initialLineNum = 0 + (this._diffResponse.content.skip || 0); |
| var ctx = { |
| hidingLines: false, |
| lastNumLinesHidden: 0, |
| left: { |
| lineNum: initialLineNum, |
| }, |
| right: { |
| lineNum: initialLineNum, |
| } |
| }; |
| var content = this._diffResponse.content; |
| for (var i = 0; i < content.length; i++) { |
| if (i == 0) { |
| ctx.skipRange = [0, this._context]; |
| } else if (i == content.length - 1) { |
| ctx.skipRange = [this._context, 0]; |
| } else { |
| ctx.skipRange = [this._context, this._context]; |
| } |
| ctx.diffChunkIndex = i; |
| this._addDiffChunk(ctx, content[i], leftSide, rightSide); |
| } |
| |
| this._diff = { |
| leftSide: leftSide, |
| rightSide: rightSide, |
| }; |
| }, |
| |
| _groupCommentsAndDrafts: function() { |
| this._baseDrafts.forEach(function(d) { d.__draft = true; }); |
| this._drafts.forEach(function(d) { d.__draft = true; }); |
| var allLeft = this._baseComments.concat(this._baseDrafts); |
| var allRight = this._comments.concat(this._drafts); |
| |
| var leftByLine = {}; |
| var rightByLine = {}; |
| var mapFunc = function(byLine) { |
| return function(c) { |
| // File comments/drafts are grouped with line 1 for now. |
| var line = c.line || 1; |
| if (byLine[line] == null) { |
| byLine[line] = []; |
| } |
| byLine[line].push(c); |
| } |
| }; |
| allLeft.forEach(mapFunc(leftByLine)); |
| allRight.forEach(mapFunc(rightByLine)); |
| |
| this._groupedBaseComments = leftByLine; |
| this._groupedComments = rightByLine; |
| }, |
| |
| _addContextControl: function(ctx, leftSide, rightSide) { |
| var numLinesHidden = ctx.lastNumLinesHidden; |
| var leftStart = leftSide.length - numLinesHidden; |
| var leftEnd = leftSide.length; |
| var rightStart = rightSide.length - numLinesHidden; |
| var rightEnd = rightSide.length; |
| if (leftStart != rightStart || leftEnd != rightEnd) { |
| throw Error( |
| 'Left and right ranges for context control should be equal:' + |
| 'Left: [' + leftStart + ', ' + leftEnd + '] ' + |
| 'Right: ['+ rightStart + ', ' + rightEnd + ']'); |
| } |
| var obj = { |
| type: 'CONTEXT_CONTROL', |
| numLines: numLinesHidden, |
| start: leftStart, |
| end: leftEnd, |
| }; |
| // NOTE: Be careful, here. This object is meant to be immutable. If the |
| // object is altered within one side's array it will reflect the |
| // alterations in another. |
| leftSide.push(obj); |
| rightSide.push(obj); |
| }, |
| |
| _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) { |
| for (var i = 0; i < chunk.ab.length; i++) { |
| var numLines = Math.ceil(chunk.ab[i].length / this.sideWidth); |
| var hidden = i >= ctx.skipRange[0] && |
| i < chunk.ab.length - ctx.skipRange[1]; |
| if (ctx.hidingLines && hidden == false) { |
| // No longer hiding lines. Add a context control. |
| this._addContextControl(ctx, leftSide, rightSide); |
| ctx.lastNumLinesHidden = 0; |
| } |
| ctx.hidingLines = hidden; |
| if (hidden) { |
| ctx.lastNumLinesHidden++; |
| } |
| |
| // Blank lines within a diff content array indicate a newline. |
| leftSide.push({ |
| type: 'CODE', |
| hidden: hidden, |
| content: chunk.ab[i] || '\n', |
| numLines: numLines, |
| lineNum: ++ctx.left.lineNum, |
| }); |
| rightSide.push({ |
| type: 'CODE', |
| hidden: hidden, |
| content: chunk.ab[i] || '\n', |
| numLines: numLines, |
| lineNum: ++ctx.right.lineNum, |
| }); |
| |
| this._addCommentsIfPresent(ctx, leftSide, rightSide); |
| } |
| if (ctx.lastNumLinesHidden > 0) { |
| this._addContextControl(ctx, leftSide, rightSide); |
| } |
| }, |
| |
| _addDiffChunk: function(ctx, chunk, leftSide, rightSide) { |
| if (chunk.ab) { |
| this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide); |
| return; |
| } |
| |
| var leftHighlights = []; |
| if (chunk.edit_a) { |
| leftHighlights = |
| this._normalizeIntralineHighlights(chunk.a, chunk.edit_a); |
| } |
| var rightHighlights = []; |
| if (chunk.edit_b) { |
| rightHighlights = |
| this._normalizeIntralineHighlights(chunk.b, chunk.edit_b); |
| } |
| |
| var aLen = (chunk.a && chunk.a.length) || 0; |
| var bLen = (chunk.b && chunk.b.length) || 0; |
| var maxLen = Math.max(aLen, bLen); |
| for (var i = 0; i < maxLen; i++) { |
| var hasLeftContent = chunk.a && i < chunk.a.length; |
| var hasRightContent = chunk.b && i < chunk.b.length; |
| var leftContent = hasLeftContent ? chunk.a[i] : ''; |
| var rightContent = hasRightContent ? chunk.b[i] : ''; |
| var maxNumLines = this._maxLinesSpanned(leftContent, rightContent); |
| if (hasLeftContent) { |
| leftSide.push({ |
| type: 'CODE', |
| content: leftContent || '\n', |
| numLines: maxNumLines, |
| lineNum: ++ctx.left.lineNum, |
| highlight: true, |
| intraline: leftHighlights.filter(function(hl) { |
| return hl.contentIndex == i; |
| }), |
| }); |
| } else { |
| leftSide.push({ |
| type: 'FILLER', |
| numLines: maxNumLines, |
| }); |
| } |
| if (hasRightContent) { |
| rightSide.push({ |
| type: 'CODE', |
| content: rightContent || '\n', |
| numLines: maxNumLines, |
| lineNum: ++ctx.right.lineNum, |
| highlight: true, |
| intraline: rightHighlights.filter(function(hl) { |
| return hl.contentIndex == i; |
| }), |
| }); |
| } else { |
| rightSide.push({ |
| type: 'FILLER', |
| numLines: maxNumLines, |
| }); |
| } |
| this._addCommentsIfPresent(ctx, leftSide, rightSide); |
| } |
| }, |
| |
| _addCommentsIfPresent: function(ctx, leftSide, rightSide) { |
| var leftComments = this._groupedBaseComments[ctx.left.lineNum]; |
| var rightComments = this._groupedComments[ctx.right.lineNum]; |
| if (leftComments) { |
| leftSide.push({ |
| type: 'COMMENT_THREAD', |
| comments: leftComments, |
| }); |
| } |
| if (rightComments) { |
| rightSide.push({ |
| type: 'COMMENT_THREAD', |
| comments: rightComments, |
| }); |
| } |
| if (leftComments && !rightComments) { |
| rightSide.push({ type: 'FILLER' }); |
| } else if (!leftComments && rightComments) { |
| leftSide.push({ type: 'FILLER' }); |
| } |
| delete(this._groupedBaseComments[ctx.left.lineNum]); |
| delete(this._groupedComments[ctx.right.lineNum]); |
| }, |
| |
| // The `highlights` array consists of a list of <skip length, mark length> |
| // pairs, where the skip length is the number of characters between the |
| // end of the previous edit and the start of this edit, and the mark |
| // length is the number of edited characters following the skip. The start |
| // of the edits is from the beginning of the related diff content lines. |
| // |
| // Note that the implied newline character at the end of each line is |
| // included in the length calculation, and thus it is possible for the |
| // edits to span newlines. |
| // |
| // A line highlight object consists of three fields: |
| // - contentIndex: The index of the diffChunk `content` field (the line |
| // being referred to). |
| // - startIndex: Where the highlight should begin. |
| // - endIndex: (optional) Where the highlight should end. If omitted, the |
| // highlight is meant to be a continuation onto the next line. |
| _normalizeIntralineHighlights: function(content, highlights) { |
| var contentIndex = 0; |
| var idx = 0; |
| var normalized = []; |
| for (var i = 0; i < highlights.length; i++) { |
| var line = content[contentIndex] + '\n'; |
| var hl = highlights[i]; |
| var j = 0; |
| while (j < hl[0]) { |
| if (idx == line.length) { |
| idx = 0; |
| line = content[++contentIndex] + '\n'; |
| continue; |
| } |
| idx++; |
| j++; |
| } |
| var lineHighlight = { |
| contentIndex: contentIndex, |
| startIndex: idx, |
| }; |
| |
| j = 0; |
| while (line && j < hl[1]) { |
| if (idx == line.length) { |
| idx = 0; |
| line = content[++contentIndex] + '\n'; |
| normalized.push(lineHighlight); |
| lineHighlight = { |
| contentIndex: contentIndex, |
| startIndex: idx, |
| }; |
| continue; |
| } |
| idx++; |
| j++; |
| } |
| lineHighlight.endIndex = idx; |
| normalized.push(lineHighlight); |
| } |
| return normalized; |
| }, |
| |
| _maxLinesSpanned: function(left, right) { |
| return Math.max(Math.ceil(left.length / this.sideWidth), |
| Math.ceil(right.length / this.sideWidth)); |
| }, |
| |
| }); |
| })(); |
| </script> |
| </dom-module> |