Refactor directory structure of components
There is no change in functionality. Only moving things around.
+ Separate html from the js.
+ Place the unit test for a component within the same folder.
+ Organize the components in subfolders.
Change-Id: I51fdc510db75fc1b33f040ca63decbbdfd4d5513
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
new file mode 100644
index 0000000..21ee076
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -0,0 +1,123 @@
+<!--
+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="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-request/gr-request.html">
+
+<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
+<link rel="import" href="../gr-diff-side/gr-diff-side.html">
+<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
+
+<dom-module id="gr-diff">
+ <template>
+ <style>
+ .loading {
+ padding: 0 var(--default-horizontal-margin) 1em;
+ color: #666;
+ }
+ .header {
+ display: flex;
+ justify-content: space-between;
+ margin: 0 var(--default-horizontal-margin) .75em;
+ }
+ .prefsButton {
+ text-align: right;
+ }
+ .diffContainer {
+ border-bottom: 1px solid #eee;
+ border-top: 1px solid #eee;
+ display: flex;
+ font: 12px var(--monospace-font-family);
+ overflow-x: auto;
+ }
+ 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}}"
+ loading="{{_loading}}"></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="loading" hidden$="[[!_loading]]">Loading...</div>
+ <div hidden$="[[_loading]]" hidden>
+ <div class="header">
+ <gr-patch-range-select
+ path="[[path]]"
+ change-num="[[changeNum]]"
+ patch-range="[[patchRange]]"
+ available-patches="[[availablePatches]]"></gr-patch-range-select>
+ <gr-button link
+ class="prefsButton"
+ on-tap="_handlePrefsTap"
+ hidden$="[[!prefs]]"
+ hidden>Diff View Preferences</gr-button>
+ </div>
+ <gr-overlay id="prefsOverlay" with-backdrop>
+ <gr-diff-preferences
+ prefs="{{prefs}}"
+ on-save="_handlePrefsSave"
+ on-cancel="_handlePrefsCancel"></gr-diff-preferences>
+ </gr-overlay>
+
+ <div class="diffContainer">
+ <gr-diff-side id="leftDiff"
+ change-num="[[changeNum]]"
+ patch-num="[[patchRange.basePatchNum]]"
+ path="[[path]]"
+ content="{{_diff.leftSide}}"
+ prefs="[[prefs]]"
+ can-comment="[[_loggedIn]]"
+ project-config="[[projectConfig]]"
+ 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}}"
+ prefs="[[prefs]]"
+ can-comment="[[_loggedIn]]"
+ project-config="[[projectConfig]]"
+ on-expand-context="_handleExpandContext"
+ on-thread-height-change="_handleThreadHeightChange"
+ on-add-draft="_handleAddDraft"
+ on-remove-thread="_handleRemoveThread"></gr-diff-side>
+ </div>
+ </div>
+ </template>
+ <script src="gr-diff.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
new file mode 100644
index 0000000..485e2cc
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -0,0 +1,746 @@
+// 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.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff',
+
+ /**
+ * Fired when the diff is rendered.
+ *
+ * @event render
+ */
+
+ properties: {
+ 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,
+ prefs: {
+ type: Object,
+ notify: true,
+ },
+ projectConfig: Object,
+
+ _prefsReady: {
+ type: Object,
+ readOnly: true,
+ value: function() {
+ return new Promise(function(resolve) {
+ this._resolvePrefsReady = resolve;
+ }.bind(this));
+ },
+ },
+ _baseComments: Array,
+ _comments: Array,
+ _drafts: Array,
+ _baseDrafts: Array,
+ /**
+ * Base (left side) comments and drafts grouped by line number.
+ * Only used for initial rendering.
+ */
+ _groupedBaseComments: {
+ type: Object,
+ value: function() { return {}; },
+ },
+ /**
+ * Comments and drafts (right side) grouped by line number.
+ * Only used for initial rendering.
+ */
+ _groupedComments: {
+ type: Object,
+ value: function() { return {}; },
+ },
+ _diffResponse: Object,
+ _diff: {
+ type: Object,
+ value: function() { return {}; },
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _initialRenderComplete: {
+ type: Boolean,
+ value: false,
+ },
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _savedPrefs: Object,
+
+ _diffRequestsPromise: Object, // Used for testing.
+ _diffPreferencesPromise: Object, // Used for testing.
+ },
+
+ behaviors: [
+ Gerrit.RESTClientBehavior,
+ ],
+
+ observers: [
+ '_prefsChanged(prefs.*)',
+ ],
+
+ 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);
+ },
+
+ scrollToNextDiffChunk: function() {
+ this.$.rightDiff.scrollToNextDiffChunk();
+ },
+
+ scrollToPreviousDiffChunk: function() {
+ this.$.rightDiff.scrollToPreviousDiffChunk();
+ },
+
+ scrollToNextCommentThread: function() {
+ this.$.rightDiff.scrollToNextCommentThread();
+ },
+
+ scrollToPreviousCommentThread: function() {
+ this.$.rightDiff.scrollToPreviousCommentThread();
+ },
+
+ reload: function(changeNum, patchRange, path) {
+ // If a diff takes a considerable amount of time to render, the previous
+ // diff can end up showing up while the DOM is constructed. Clear the
+ // content on a reload to prevent this.
+ this._diff = {
+ leftSide: [],
+ rightSide: [],
+ };
+
+ var promises = [
+ this._prefsReady,
+ this.$.diffXHR.generateRequest().completes
+ ];
+
+ var basePatchNum = this.patchRange.basePatchNum;
+
+ return 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));
+ },
+
+ showDiffPreferences: function() {
+ this.$.prefsOverlay.open();
+ },
+
+ _prefsChanged: function(changeRecord) {
+ if (this._initialRenderComplete) {
+ this._render();
+ }
+ this._resolvePrefsReady(changeRecord.base);
+ },
+
+ _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);
+
+ this._initialRenderComplete = true;
+ },
+
+ _getCommentsAndDrafts: function(basePatchNum, loggedIn) {
+ 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 this.changeBaseURL(changeNum, patchNum) + '/files/' +
+ encodeURIComponent(path) + '/diff';
+ },
+
+ _computeCommentsPath: function(changeNum, patchNum) {
+ return this.changeBaseURL(changeNum, patchNum) + '/comments';
+ },
+
+ _computeDraftsPath: function(changeNum, patchNum) {
+ return this.changeBaseURL(changeNum, patchNum) + '/drafts';
+ },
+
+ _computeDiffQueryParams: function(basePatchNum) {
+ var params = {
+ context: 'ALL',
+ intraline: null
+ };
+ if (basePatchNum != 'PARENT') {
+ params.base = basePatchNum;
+ }
+ return params;
+ },
+
+ _handlePrefsTap: function(e) {
+ e.preventDefault();
+
+ // TODO(andybons): This is not supported in IE. Implement a polyfill.
+ // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds
+ // an object as a value, it must be marked enumerable.
+ this._savedPrefs = Object.assign({}, this.prefs);
+ this.$.prefsOverlay.open();
+ },
+
+ _handlePrefsSave: function(e) {
+ e.stopPropagation();
+ var el = Polymer.dom(e).rootTarget;
+ el.disabled = true;
+ app.accountReady.then(function() {
+ if (!this._loggedIn) {
+ el.disabled = false;
+ this.$.prefsOverlay.close();
+ return;
+ }
+ this._saveDiffPreferences().then(function() {
+ this.$.prefsOverlay.close();
+ el.disabled = false;
+ }.bind(this)).catch(function(err) {
+ el.disabled = false;
+ alert('Oops. Something went wrong. Check the console and bug the ' +
+ 'PolyGerrit team for assistance.');
+ throw err;
+ });
+ }.bind(this));
+ },
+
+ _saveDiffPreferences: function() {
+ var xhr = document.createElement('gr-request');
+ this._diffPreferencesPromise = xhr.send({
+ method: 'PUT',
+ url: '/accounts/self/preferences.diff',
+ body: this.prefs,
+ });
+ return this._diffPreferencesPromise;
+ },
+
+ _handlePrefsCancel: function(e) {
+ e.stopPropagation();
+ this.prefs = this._savedPrefs;
+ this.$.prefsOverlay.close();
+ },
+
+ _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 (diffEl == this.$.leftDiff &&
+ this.patchRange.basePatchNum == 'PARENT') {
+ comment.comments[0].side = 'PARENT';
+ comment.patchNum = this.patchRange.patchNum;
+ }
+
+ 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._breakUpCommonChunksWithComments(ctx,
+ this._diffResponse.content);
+ var context = this.prefs.context;
+ if (context == -1) {
+ // Show the entire file.
+ context = Infinity;
+ }
+ for (var i = 0; i < content.length; i++) {
+ if (i == 0) {
+ ctx.skipRange = [0, context];
+ } else if (i == content.length - 1) {
+ ctx.skipRange = [context, 0];
+ } else {
+ ctx.skipRange = [context, context];
+ }
+ ctx.diffChunkIndex = i;
+ this._addDiffChunk(ctx, content[i], leftSide, rightSide);
+ }
+
+ this._diff = {
+ leftSide: leftSide,
+ rightSide: rightSide,
+ };
+ },
+
+ // In order to show comments out of the bounds of the selected context,
+ // treat them as diffs within the model so that the content (and context
+ // surrounding it) renders correctly.
+ _breakUpCommonChunksWithComments: function(ctx, content) {
+ var result = [];
+ var leftLineNum = ctx.left.lineNum;
+ var rightLineNum = ctx.right.lineNum;
+ for (var i = 0; i < content.length; i++) {
+ if (!content[i].ab) {
+ result.push(content[i]);
+ if (content[i].a) {
+ leftLineNum += content[i].a.length;
+ }
+ if (content[i].b) {
+ rightLineNum += content[i].b.length;
+ }
+ continue;
+ }
+ var chunk = content[i].ab;
+ var currentChunk = {ab: []};
+ for (var j = 0; j < chunk.length; j++) {
+ leftLineNum++;
+ rightLineNum++;
+ if (this._groupedBaseComments[leftLineNum] == null &&
+ this._groupedComments[rightLineNum] == null) {
+ currentChunk.ab.push(chunk[j]);
+ } else {
+ if (currentChunk.ab && currentChunk.ab.length > 0) {
+ result.push(currentChunk);
+ currentChunk = {ab: []};
+ }
+ // Append an annotation to indicate that this line should not be
+ // highlighted even though it's implied with both `a` and `b`
+ // defined. This is needed since there may be two lines that
+ // should be highlighted but are equal (blank lines, for example).
+ result.push({
+ __noHighlight: true,
+ a: [chunk[j]],
+ b: [chunk[j]],
+ });
+ }
+ }
+ if (currentChunk.ab != null && currentChunk.ab.length > 0) {
+ result.push(currentChunk);
+ }
+ }
+ return result;
+ },
+
+ _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(
+ this._visibleLineLength(chunk.ab[i]) / this.prefs.line_length);
+ 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 highlight = !chunk.__noHighlight;
+ var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
+ if (hasLeftContent) {
+ leftSide.push({
+ type: 'CODE',
+ content: leftContent || '\n',
+ numLines: maxNumLines,
+ lineNum: ++ctx.left.lineNum,
+ highlight: highlight,
+ intraline: highlight && 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: highlight,
+ intraline: highlight && 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) {
+ var thread = {
+ type: 'COMMENT_THREAD',
+ comments: leftComments,
+ };
+ if (this.patchRange.basePatchNum == 'PARENT') {
+ thread.patchNum = this.patchRange.patchNum;
+ }
+ leftSide.push(thread);
+ }
+ if (rightComments) {
+ rightSide.push({
+ type: 'COMMENT_THREAD',
+ comments: rightComments,
+ });
+ }
+ if (leftComments && !rightComments) {
+ rightSide.push({type: 'FILLER'});
+ } else if (!leftComments && rightComments) {
+ leftSide.push({type: 'FILLER'});
+ }
+ this._groupedBaseComments[ctx.left.lineNum] = null;
+ this._groupedComments[ctx.right.lineNum] = null;
+ },
+
+ // 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;
+ },
+
+ _visibleLineLength: function(contents) {
+ // http://jsperf.com/performance-of-match-vs-split
+ var numTabs = contents.split('\t').length - 1;
+ return contents.length - numTabs + (this.prefs.tab_size * numTabs);
+ },
+
+ _maxLinesSpanned: function(left, right) {
+ return Math.max(
+ Math.ceil(this._visibleLineLength(left) / this.prefs.line_length),
+ Math.ceil(this._visibleLineLength(right) / this.prefs.line_length));
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
new file mode 100644
index 0000000..9a8cb81
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -0,0 +1,574 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="../../../scripts/fake-app.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-diff.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff></gr-diff>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.path = 'sieve.go';
+ element.prefs = {
+ context: 10,
+ tab_size: 8,
+ };
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ /\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/,
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ change_type: 'MODIFIED',
+ content: [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ '<title>My great page</title>',
+ '<style>',
+ ' *,',
+ ' *:before,',
+ ' *:after {',
+ ' box-sizing: border-box;',
+ ' }',
+ '</style>',
+ '<header>',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ '</header>',
+ '<body>',
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ]
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/comments',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_5f2b8b82',
+ line: 1,
+ message: 'this isn’t quite right',
+ updated: '2015-12-10 02:50:21.627000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2015-12-10 00:08:42.255000000',
+ },
+ ],
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/comments',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f30f3048',
+ line: 12,
+ message: 'What on earth are you thinking, here?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f6b1b1c8',
+ in_reply_to: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: 'a0407443_30dfe8fb',
+ in_reply_to: '001a2067_f30f3048',
+ line: 12,
+ message: '¯\\_(ツ)_/¯',
+ updated: '2015-12-12 18:50:21.627000000',
+ },
+ ],
+ }),
+ ]
+ );
+
+ server.respondWith(
+ 'PUT',
+ '/accounts/self/preferences.diff',
+ [
+ 200,
+ {'Content-Type': 'application/json'},
+ ')]}\'\n' +
+ JSON.stringify({context: 25}),
+ ]
+ );
+
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('comments with parent', function(done) {
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+
+ element.reload();
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 1);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comments between two patches', function(done) {
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ element.reload();
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 2);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comment rendering', function(done) {
+ element.prefs.context = -1;
+ element._loggedIn = true;
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ element.reload();
+ server.respond();
+
+ // Allow events to fire and the threads to render.
+ element.async(function() {
+ var leftThreadEls =
+ Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+ 'gr-diff-comment-thread');
+ assert.equal(leftThreadEls.length, 1);
+ assert.equal(leftThreadEls[0].comments.length, 1);
+
+ var rightThreadEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ 'gr-diff-comment-thread');
+ assert.equal(rightThreadEls.length, 1);
+ assert.equal(rightThreadEls[0].comments.length, 2);
+
+ var index = leftThreadEls[0].getAttribute('data-index');
+ var leftFillerEls =
+ Polymer.dom(element.$.leftDiff.root).querySelectorAll(
+ '.commentThread.filler[data-index="' + index + '"]');
+ assert.equal(leftFillerEls.length, 1);
+ var rightFillerEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(rightFillerEls.length, 2);
+
+ for (var i = 0; i < rightFillerEls.length; i++) {
+ assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+ }
+ var originalHeight = rightFillerEls[0].offsetHeight;
+ assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
+
+ // Create a comment on the opposite side of the first comment.
+ var rightLineEL = element.$.rightDiff.$$(
+ '.lineNum[data-index="' + (index - 1) + '"]');
+ assert.ok(rightLineEL);
+ MockInteractions.tap(rightLineEL);
+ element.async(function() {
+ var newThreadEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(newThreadEls.length, 2);
+ for (var i = 0; i < newThreadEls.length; i++) {
+ assert.isTrue(
+ newThreadEls[i].classList.contains('commentThread') ||
+ newThreadEls[i].tagName == 'GR-DIFF-COMMENT-THREAD');
+ }
+ var newHeight = newThreadEls[0].offsetHeight;
+ assert.equal(newThreadEls[1].offsetHeight, newHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, newHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, newHeight);
+
+ // The editing mode height of the right comment will be greater than
+ // the non-editing mode height of the left comment.
+ assert.isAbove(newHeight, originalHeight);
+
+ // Discard the right thread and ensure the left comment heights are
+ // back to their original values.
+ newThreadEls[1].addEventListener('discard', function() {
+ rightFillerEls =
+ Polymer.dom(element.$.rightDiff.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ assert.equal(rightFillerEls.length, 2);
+
+ for (var i = 0; i < rightFillerEls.length; i++) {
+ assert.isTrue(rightFillerEls[i].classList.contains('filler'));
+ }
+ var originalHeight = rightFillerEls[0].offsetHeight;
+ assert.equal(rightFillerEls[1].offsetHeight, originalHeight);
+ assert.equal(leftThreadEls[0].offsetHeight, originalHeight);
+ assert.equal(leftFillerEls[0].offsetHeight, originalHeight);
+ done();
+ });
+ var commentEl = newThreadEls[1].$$('gr-diff-comment');
+ commentEl.fire('discard', null, {bubbles: false});
+ }, 1);
+ }, 1);
+ });
+
+ test('intraline normalization', function() {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ var content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ var highlights = [
+ [31, 34], [42, 26]
+ ];
+ var results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ }
+ ]);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a possibility that the',
+ ' // patch number is no longer a part of the URL (say when navigating to',
+ ' // the top-level change info view) and therefore undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ }
+ ]);
+ });
+
+ test('context', function() {
+ element.prefs.context = 3;
+ element._diffResponse = {
+ content: [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ '<title>My great page</title>',
+ '<style>',
+ ' *,',
+ ' *:before,',
+ ' *:after {',
+ ' box-sizing: border-box;',
+ ' }',
+ '</style>',
+ '<header>',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ '</header>',
+ '<body>',
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ]
+ };
+ element._processContent();
+
+ // First eight lines should be hidden on both sides.
+ for (var i = 0; i < 8; i++) {
+ assert.isTrue(element._diff.leftSide[i].hidden);
+ assert.isTrue(element._diff.rightSide[i].hidden);
+ }
+ // A context control should be at index 8 on both sides.
+ var leftContext = element._diff.leftSide[8];
+ var rightContext = element._diff.rightSide[8];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 8);
+ assert.equal(leftContext.start, 0);
+ assert.equal(leftContext.end, 8);
+
+ // Line indices 9-16 should be shown.
+ for (var i = 9; i <= 16; i++) {
+ // notOk (falsy) because the `hidden` attribute may not be present.
+ assert.notOk(element._diff.leftSide[i].hidden);
+ assert.notOk(element._diff.rightSide[i].hidden);
+ }
+
+ // Lines at indices 17 and 18 should be hidden.
+ assert.isTrue(element._diff.leftSide[17].hidden);
+ assert.isTrue(element._diff.rightSide[17].hidden);
+ assert.isTrue(element._diff.leftSide[18].hidden);
+ assert.isTrue(element._diff.rightSide[18].hidden);
+
+ // Context control at index 19.
+ leftContext = element._diff.leftSide[19];
+ rightContext = element._diff.rightSide[19];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 2);
+ assert.equal(leftContext.start, 17);
+ assert.equal(leftContext.end, 19);
+ });
+
+ test('save prefs', function(done) {
+ element._loggedIn = false;
+
+ element.prefs = {
+ tab_size: 4,
+ context: 50,
+ };
+ element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
+ assert.isTrue(element._diffPreferencesPromise == null);
+
+ element._loggedIn = true;
+ element.fire('save', {}, {node: element.$$('gr-diff-preferences')});
+ server.respond();
+
+ element._diffPreferencesPromise.then(function(req) {
+ assert.equal(req.xhr.requestBody, JSON.stringify(element.prefs));
+ done();
+ });
+ });
+
+ test('visible line length', function() {
+ assert.equal(element._visibleLineLength('A'.repeat(5)), 5);
+ assert.equal(
+ element._visibleLineLength('A'.repeat(5) + '\t' + 'A'.repeat(5)), 18);
+ });
+
+ test('break up common diff chunks', function() {
+ element._groupedBaseComments = {
+ 1: {},
+ };
+ element._groupedComments = {
+ 10: {},
+ };
+ var ctx = {
+ left: {lineNum: 0},
+ right: {lineNum: 0},
+ };
+ var content = [
+ {
+ ab: [
+ '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.',
+ ]
+ }
+ ];
+ var result = element._breakUpCommonChunksWithComments(ctx, content);
+ assert.deepEqual(result, [
+ {
+ __noHighlight: true,
+ a: ['Copyright (C) 2015 The Android Open Source Project'],
+ b: ['Copyright (C) 2015 The Android Open Source Project'],
+ },
+ {
+ ab: [
+ '',
+ '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, ',
+ ]
+ },
+ {
+ __noHighlight: true,
+ a: ['software distributed under the License is distributed on an '],
+ b: ['software distributed under the License is distributed on an ']
+ },
+ {
+ ab: [
+ '"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.',
+ ]
+ }
+ ]);
+ });
+ });
+
+</script>