Move gr-diff-new to gr-diff
Change-Id: Ifaad016f806c31f3df43143b3238b757faa18b20
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
new file mode 100644
index 0000000..77c790c
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-side-by-side.js
@@ -0,0 +1,63 @@
+// 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(window, GrDiffBuilder) {
+ 'use strict';
+
+ function GrDiffBuilderSideBySide(diff, comments, prefs, outputEl) {
+ GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+ }
+ GrDiffBuilderSideBySide.prototype = Object.create(GrDiffBuilder.prototype);
+ GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide;
+
+ GrDiffBuilderSideBySide.prototype.emitGroup = function(group,
+ opt_beforeSection) {
+ var sectionEl = this._createElement('tbody', 'section');
+ sectionEl.classList.add(group.type);
+ var pairs = group.getSideBySidePairs();
+ for (var i = 0; i < pairs.length; i++) {
+ sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,
+ pairs[i].right));
+ }
+ this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+ },
+
+ GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine,
+ rightLine) {
+ var row = this._createElement('tr');
+ this._appendPair(section, row, leftLine, leftLine.beforeNumber,
+ GrDiffBuilder.Side.LEFT);
+ this._appendPair(section, row, rightLine, rightLine.afterNumber,
+ GrDiffBuilder.Side.RIGHT);
+ return row;
+ };
+
+ GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line,
+ lineNumber, side) {
+ row.appendChild(this._createLineEl(line, lineNumber, line.type));
+ var action = this._createContextControl(section, line);
+ if (action) {
+ row.appendChild(action);
+ } else {
+ var textEl = this._createTextEl(line);
+ textEl.classList.add(side);
+ var threadEl = this._commentThreadForLine(line, side);
+ if (threadEl) {
+ textEl.appendChild(threadEl);
+ }
+ row.appendChild(textEl);
+ }
+ };
+
+ window.GrDiffBuilderSideBySide = GrDiffBuilderSideBySide;
+})(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
new file mode 100644
index 0000000..d9517d3
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder-unified.js
@@ -0,0 +1,55 @@
+// 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(window, GrDiffBuilder) {
+ 'use strict';
+
+ function GrDiffBuilderUnified(diff, comments, prefs, outputEl) {
+ GrDiffBuilder.call(this, diff, comments, prefs, outputEl);
+ }
+ GrDiffBuilderUnified.prototype = Object.create(GrDiffBuilder.prototype);
+ GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified;
+
+ GrDiffBuilderUnified.prototype.emitGroup = function(group,
+ opt_beforeSection) {
+ var sectionEl = this._createElement('tbody', 'section');
+
+ for (var i = 0; i < group.lines.length; ++i) {
+ sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));
+ }
+ this._outputEl.insertBefore(sectionEl, opt_beforeSection);
+ };
+
+ GrDiffBuilderUnified.prototype._createRow = function(section, line) {
+ var row = this._createElement('tr', line.type);
+ row.appendChild(this._createLineEl(line, line.beforeNumber,
+ GrDiffLine.Type.REMOVE));
+ row.appendChild(this._createLineEl(line, line.afterNumber,
+ GrDiffLine.Type.ADD));
+
+ var action = this._createContextControl(section, line);
+ if (action) {
+ row.appendChild(action);
+ } else {
+ var textEl = this._createTextEl(line);
+ var threadEl = this._commentThreadForLine(line);
+ if (threadEl) {
+ textEl.appendChild(threadEl);
+ }
+ row.appendChild(textEl);
+ }
+ return row;
+ };
+
+ window.GrDiffBuilderUnified = GrDiffBuilderUnified;
+})(window, GrDiffBuilder);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
new file mode 100644
index 0000000..338845e
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder.js
@@ -0,0 +1,580 @@
+// 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(window, GrDiffGroup, GrDiffLine) {
+ 'use strict';
+
+ function GrDiffBuilder(diff, comments, prefs, outputEl) {
+ this._comments = comments;
+ this._prefs = prefs;
+ this._outputEl = outputEl;
+ this._groups = [];
+
+ this._commentLocations = this._getCommentLocations(comments);
+ this._processContent(diff.content, this._groups, prefs.context);
+ }
+
+ GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
+ GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
+ GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
+ GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
+
+ GrDiffBuilder.TAB_REGEX = /\t/g;
+
+ GrDiffBuilder.LINE_FEED_HTML =
+ '<span class="style-scope gr-diff br"></span>';
+
+ GrDiffBuilder.GroupType = {
+ ADDED: 'b',
+ BOTH: 'ab',
+ REMOVED: 'a',
+ };
+
+ GrDiffBuilder.Highlights = {
+ ADDED: 'edit_b',
+ REMOVED: 'edit_a',
+ };
+
+ GrDiffBuilder.Side = {
+ LEFT: 'left',
+ RIGHT: 'right',
+ };
+
+ GrDiffBuilder.prototype.emitDiff = function() {
+ for (var i = 0; i < this._groups.length; i++) {
+ this.emitGroup(this._groups[i]);
+ }
+ };
+
+ GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
+ throw Error('Subclasses must implement emitGroup');
+ },
+
+ GrDiffBuilder.prototype._processContent = function(content, groups, context) {
+ this._appendFileComments(groups);
+
+ var WHOLE_FILE = -1;
+ context = content.length > 1 ? context : WHOLE_FILE;
+
+ var lineNums = {
+ left: 0,
+ right: 0,
+ };
+ content = this._splitCommonGroupsWithComments(content, lineNums);
+ for (var i = 0; i < content.length; i++) {
+ var group = content[i];
+ var lines = [];
+
+ if (group[GrDiffBuilder.GroupType.BOTH] !== undefined) {
+ var rows = group[GrDiffBuilder.GroupType.BOTH];
+ this._appendCommonLines(rows, lines, lineNums);
+
+ var hiddenRange = [context, rows.length - context];
+ if (i === 0) {
+ hiddenRange[0] = 0;
+ } else if (i === content.length - 1) {
+ hiddenRange[1] = rows.length;
+ }
+
+ if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
+ this._insertContextGroups(groups, lines, hiddenRange);
+ } else {
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines));
+ }
+ continue;
+ }
+
+ if (group[GrDiffBuilder.GroupType.REMOVED] !== undefined) {
+ var highlights;
+ if (group[GrDiffBuilder.Highlights.REMOVED] !== undefined) {
+ highlights = this._normalizeIntralineHighlights(
+ group[GrDiffBuilder.GroupType.REMOVED],
+ group[GrDiffBuilder.Highlights.REMOVED]);
+ }
+ this._appendRemovedLines(group[GrDiffBuilder.GroupType.REMOVED], lines,
+ lineNums, highlights);
+ }
+
+ if (group[GrDiffBuilder.GroupType.ADDED] !== undefined) {
+ var highlights;
+ if (group[GrDiffBuilder.Highlights.ADDED] !== undefined) {
+ highlights = this._normalizeIntralineHighlights(
+ group[GrDiffBuilder.GroupType.ADDED],
+ group[GrDiffBuilder.Highlights.ADDED]);
+ }
+ this._appendAddedLines(group[GrDiffBuilder.GroupType.ADDED], lines,
+ lineNums, highlights);
+ }
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.DELTA, lines));
+ }
+ };
+
+ GrDiffBuilder.prototype._appendFileComments = function(groups) {
+ var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = GrDiffLine.FILE;
+ line.afterNumber = GrDiffLine.FILE;
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]));
+ };
+
+ GrDiffBuilder.prototype._getCommentLocations = function(comments) {
+ var result = {
+ left: {},
+ right: {},
+ };
+ for (var side in comments) {
+ if (side !== GrDiffBuilder.Side.LEFT &&
+ side !== GrDiffBuilder.Side.RIGHT) {
+ continue;
+ }
+ comments[side].forEach(function(c) {
+ result[side][c.line || GrDiffLine.FILE] = true;
+ });
+ }
+ return result;
+ };
+
+ GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
+ return this._commentLocations[side][lineNum] === true;
+ };
+
+ // In order to show comments out of the bounds of the selected context,
+ // treat them as separate chunks within the model so that the content (and
+ // context surrounding it) renders correctly.
+ GrDiffBuilder.prototype._splitCommonGroupsWithComments = function(content,
+ lineNums) {
+ var result = [];
+ var leftLineNum = lineNums.left;
+ var rightLineNum = lineNums.right;
+ 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._commentIsAtLineNum(GrDiffBuilder.Side.LEFT, leftLineNum) ||
+ this._commentIsAtLineNum(GrDiffBuilder.Side.RIGHT, rightLineNum)) {
+ if (currentChunk.ab && currentChunk.ab.length > 0) {
+ result.push(currentChunk);
+ currentChunk = {ab: []};
+ }
+ result.push({ab: [chunk[j]]});
+ } else {
+ currentChunk.ab.push(chunk[j]);
+ }
+ }
+ if (currentChunk.ab != null && currentChunk.ab.length > 0) {
+ result.push(currentChunk);
+ }
+ }
+ return result;
+ };
+
+ // 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.
+ GrDiffBuilder.prototype._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;
+ };
+
+ GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
+ hiddenRange) {
+ var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
+ var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
+ var linesAfterCtx = lines.slice(hiddenRange[1]);
+
+ if (linesBeforeCtx.length > 0) {
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
+ }
+
+ var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
+ ctxLine.contextLines = hiddenLines;
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
+ [ctxLine]));
+
+ if (linesAfterCtx.length > 0) {
+ groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
+ }
+ };
+
+ GrDiffBuilder.prototype._appendCommonLines = function(rows, lines, lineNums) {
+ for (var i = 0; i < rows.length; i++) {
+ var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.text = rows[i];
+ line.beforeNumber = ++lineNums.left;
+ line.afterNumber = ++lineNums.right;
+ lines.push(line);
+ }
+ };
+
+ GrDiffBuilder.prototype._appendRemovedLines = function(rows, lines, lineNums,
+ opt_highlights) {
+ for (var i = 0; i < rows.length; i++) {
+ var line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.text = rows[i];
+ line.beforeNumber = ++lineNums.left;
+ if (opt_highlights) {
+ line.highlights = opt_highlights.filter(function(hl) {
+ return hl.contentIndex === i;
+ });
+ }
+ lines.push(line);
+ }
+ };
+
+ GrDiffBuilder.prototype._appendAddedLines = function(rows, lines, lineNums,
+ opt_highlights) {
+ for (var i = 0; i < rows.length; i++) {
+ var line = new GrDiffLine(GrDiffLine.Type.ADD);
+ line.text = rows[i];
+ line.afterNumber = ++lineNums.right;
+ if (opt_highlights) {
+ line.highlights = opt_highlights.filter(function(hl) {
+ return hl.contentIndex === i;
+ });
+ }
+ lines.push(line);
+ }
+ };
+
+ GrDiffBuilder.prototype._createContextControl = function(section, line) {
+ if (!line.contextLines.length) {
+ return null;
+ }
+ var td = this._createElement('td');
+ var button = this._createElement('gr-button', 'showContext');
+ button.setAttribute('link', true);
+ var commonLines = line.contextLines.length;
+ var text = 'Show ' + commonLines + ' common line';
+ if (commonLines > 1) {
+ text += 's';
+ }
+ text += '...';
+ button.textContent = text;
+ button.addEventListener('tap', function(e) {
+ e.detail = {
+ group: new GrDiffGroup(GrDiffGroup.Type.BOTH, line.contextLines),
+ section: section,
+ };
+ // Let it bubble up the DOM tree.
+ });
+ td.appendChild(button);
+ return td;
+ };
+
+ GrDiffBuilder.prototype._getCommentsForLine = function(comments, line,
+ opt_side) {
+ function byLineNum(lineNum) {
+ return function(c) {
+ return (c.line === lineNum) ||
+ (c.line === undefined && lineNum === GrDiffLine.FILE)
+ }
+ }
+ var leftComments =
+ comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
+ var rightComments =
+ comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber));
+
+ var result;
+
+ switch (opt_side) {
+ case GrDiffBuilder.Side.LEFT:
+ result = leftComments;
+ break;
+ case GrDiffBuilder.Side.RIGHT:
+ result = rightComments;
+ break;
+ default:
+ result = leftComments.concat(rightComments);
+ break;
+ }
+
+ return result;
+ };
+
+ GrDiffBuilder.prototype.createCommentThread = function(changeNum, patchNum,
+ path, side, projectConfig) {
+ var threadEl = document.createElement('gr-diff-comment-thread');
+ threadEl.changeNum = changeNum;
+ threadEl.patchNum = patchNum;
+ threadEl.path = path;
+ threadEl.side = side;
+ threadEl.projectConfig = projectConfig;
+ return threadEl;
+ },
+
+ GrDiffBuilder.prototype._commentThreadForLine = function(line, opt_side) {
+ var comments = this._getCommentsForLine(this._comments, line, opt_side);
+ if (!comments || comments.length === 0) {
+ return null;
+ }
+
+ var patchNum = this._comments.meta.patchRange.patchNum;
+ var side = 'REVISION';
+ if (line.type === GrDiffLine.Type.REMOVE ||
+ opt_side === GrDiffBuilder.Side.LEFT) {
+ if (this._comments.meta.patchRange.basePatchNum === 'PARENT') {
+ side = 'PARENT';
+ } else {
+ patchNum = this._comments.meta.patchRange.basePatchNum;
+ }
+ }
+ var threadEl = this.createCommentThread(
+ this._comments.meta.changeNum,
+ patchNum,
+ this._comments.meta.path,
+ side,
+ this._comments.meta.projectConfig);
+ threadEl.comments = comments;
+ return threadEl;
+ };
+
+ GrDiffBuilder.prototype._createLineEl = function(line, number, type) {
+ var td = this._createElement('td');
+ if (line.type === GrDiffLine.Type.BLANK) {
+ return td;
+ } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
+ td.classList.add('contextLineNum');
+ td.setAttribute('data-value', '@@');
+ } else if (line.type === GrDiffLine.Type.BOTH || line.type == type) {
+ td.classList.add('lineNum');
+ td.setAttribute('data-value', number);
+ }
+ return td;
+ };
+
+ GrDiffBuilder.prototype._createTextEl = function(line) {
+ var td = this._createElement('td');
+ if (line.type !== GrDiffLine.Type.BLANK) {
+ td.classList.add('content');
+ }
+ td.classList.add(line.type);
+ var text = line.text;
+ var html = util.escapeHTML(text);
+
+ td.classList.add(line.highlights.length > 0 ?
+ 'lightHighlight' : 'darkHighlight');
+
+ if (line.highlights.length > 0) {
+ html = this._addIntralineHighlights(text, html, line.highlights);
+ }
+
+ if (text.length > this._prefs.line_length) {
+ html = this._addNewlines(text, html);
+ }
+ html = this._addTabWrappers(html);
+
+ // If the html is equivalent to the text then it didn't get highlighted
+ // or escaped. Use textContent which is faster than innerHTML.
+ if (html == text) {
+ td.textContent = text;
+ } else {
+ td.innerHTML = html;
+ }
+ return td;
+ };
+
+ // Advance `index` by the appropriate number of characters that would
+ // represent one source code character and return that index. For
+ // example, for source code '<span>' the escaped html string is
+ // '<span>'. Advancing from index 0 on the prior html string would
+ // return 4, since < maps to one source code character ('<').
+ GrDiffBuilder.prototype._advanceChar = function(html, index) {
+ // TODO(andybons): Unicode is all kinds of messed up in JS. Account for it.
+ // https://mathiasbynens.be/notes/javascript-unicode
+
+ // Tags don't count as characters
+ while (index < html.length &&
+ html.charCodeAt(index) == GrDiffBuilder.LESS_THAN_CODE) {
+ while (index < html.length &&
+ html.charCodeAt(index) != GrDiffBuilder.GREATER_THAN_CODE) {
+ index++;
+ }
+ index++; // skip the ">" itself
+ }
+ // An HTML entity (e.g., <) counts as one character.
+ if (index < html.length &&
+ html.charCodeAt(index) == GrDiffBuilder.AMPERSAND_CODE) {
+ while (index < html.length &&
+ html.charCodeAt(index) != GrDiffBuilder.SEMICOLON_CODE) {
+ index++;
+ }
+ }
+ return index + 1;
+ };
+
+ GrDiffBuilder.prototype._addNewlines = function(text, html) {
+ var htmlIndex = 0;
+ var indices = [];
+ var numChars = 0;
+ for (var i = 0; i < text.length; i++) {
+ if (numChars > 0 && numChars % this._prefs.line_length === 0) {
+ indices.push(htmlIndex);
+ }
+ htmlIndex = this._advanceChar(html, htmlIndex);
+ if (text[i] === '\t') {
+ numChars += this._prefs.tab_size;
+ } else {
+ numChars++;
+ }
+ }
+ var result = html;
+ // Since the result string is being altered in place, start from the end
+ // of the string so that the insertion indices are not affected as the
+ // result string changes.
+ for (var i = indices.length - 1; i >= 0; i--) {
+ result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML +
+ result.slice(indices[i]);
+ }
+ return result;
+ };
+
+ GrDiffBuilder.prototype._addTabWrappers = function(html) {
+ var htmlStr = this._getTabWrapper(this._prefs.tab_size,
+ this._prefs.show_tabs);
+ return html.replace(GrDiffBuilder.TAB_REGEX, htmlStr);
+ };
+
+ GrDiffBuilder.prototype._addIntralineHighlights = function(content, html,
+ highlights) {
+ var START_TAG = '<hl class="style-scope gr-diff">';
+ var END_TAG = '</hl>';
+
+ for (var i = 0; i < highlights.length; i++) {
+ var hl = highlights[i];
+
+ var htmlStartIndex = 0;
+ // Find the index of the HTML string to insert the start tag.
+ for (var j = 0; j < hl.startIndex; j++) {
+ htmlStartIndex = this._advanceChar(html, htmlStartIndex);
+ }
+
+ var htmlEndIndex = 0;
+ if (hl.endIndex !== undefined) {
+ for (var j = 0; j < hl.endIndex; j++) {
+ htmlEndIndex = this._advanceChar(html, htmlEndIndex);
+ }
+ } else {
+ // If endIndex isn't present, continue to the end of the line.
+ htmlEndIndex = html.length;
+ }
+ // 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 (htmlStartIndex !== htmlEndIndex) {
+ html = html.slice(0, htmlStartIndex) + START_TAG +
+ html.slice(htmlStartIndex, htmlEndIndex) + END_TAG +
+ html.slice(htmlEndIndex);
+ }
+ }
+ return html;
+ };
+
+ GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) {
+ // Force this to be a number to prevent arbitrary injection.
+ tabSize = +tabSize;
+ if (isNaN(tabSize)) {
+ throw Error('Invalid tab size from preferences.');
+ }
+
+ var str = '<span class="style-scope gr-diff tab ';
+ if (showTabs) {
+ str += 'withIndicator';
+ }
+ str += '" ';
+ // TODO(andybons): CSS tab-size is not supported in IE.
+ str += 'style="tab-size:' + tabSize + ';';
+ str += 'style="-moz-tab-size:' + tabSize + ';';
+ str += '">\t</span>';
+ return str;
+ };
+
+ GrDiffBuilder.prototype._createElement = function(tagName, className) {
+ var el = document.createElement(tagName);
+ // When Shady DOM is being used, these classes are added to account for
+ // Polymer's polyfill behavior. In order to guarantee sufficient
+ // specificity within the CSS rules, these are added to every element.
+ // Since the Polymer DOM utility functions (which would do this
+ // automatically) are not being used for performance reasons, this is
+ // done manually.
+ el.classList.add('style-scope', 'gr-diff');
+ if (!!className) {
+ el.classList.add(className);
+ }
+ return el;
+ };
+
+ window.GrDiffBuilder = GrDiffBuilder;
+})(window, GrDiffGroup, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
new file mode 100644
index 0000000..2e30999
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-builder_test.html
@@ -0,0 +1,516 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-builder</title>
+
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="gr-diff-line.js"></script>
+<script src="gr-diff-group.js"></script>
+<script src="gr-diff-builder.js"></script>
+
+<script>
+ suite('gr-diff-builder tests', function() {
+ var builder;
+
+ setup(function() {
+ var prefs = {
+ line_length: 10,
+ show_tabs: true,
+ tab_size: 4,
+ };
+ builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs);
+ });
+
+ test('process loaded content', function() {
+ var content = [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ];
+ var groups = [];
+
+ builder._processContent(content, groups, -1);
+
+ assert.equal(groups.length, 4);
+
+ var group = groups[0];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 1);
+ assert.equal(group.lines[0].text, '');
+ assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE);
+
+ group = groups[1];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 2);
+ assert.equal(group.lines.length, 2);
+
+ function beforeNumberFn(l) { return l.beforeNumber; }
+ function afterNumberFn(l) { return l.afterNumber; }
+ function textFn(l) { return l.text; }
+
+ assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(textFn), [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]);
+
+ group = groups[2];
+ assert.equal(group.type, GrDiffGroup.Type.DELTA);
+ assert.equal(group.lines.length, 3);
+ assert.equal(group.adds.length, 1);
+ assert.equal(group.removes.length, 2);
+ assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
+ assert.deepEqual(group.adds.map(afterNumberFn), [3]);
+ assert.deepEqual(group.removes.map(textFn), [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ]);
+ assert.deepEqual(group.adds.map(textFn), [
+ ' Hello, world!',
+ ]);
+
+ group = groups[3];
+ assert.equal(group.type, GrDiffGroup.Type.BOTH);
+ assert.equal(group.lines.length, 3);
+ assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
+ assert.deepEqual(group.lines.map(textFn), [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]);
+ });
+
+ test('insert context groups', function() {
+ var content = [
+ {ab: []},
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: []},
+ {b: ['elgoog elgoog elgoog']},
+ {ab: []},
+ ];
+ for (var i = 0; i < 100; i++) {
+ content[0].ab.push('all work and no play make jack a dull boy');
+ content[4].ab.push('all work and no play make jill a dull girl');
+ }
+ for (var i = 0; i < 5; i++) {
+ content[2].ab.push('no tv and no beer make homer go crazy');
+ }
+ var groups = [];
+ var context = 10;
+
+ builder._processContent(content, groups, context);
+
+ assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[0].lines.length, 1);
+ assert.equal(groups[0].lines[0].text, '');
+ assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(groups[1].lines[0].contextLines.length, 90);
+ groups[1].lines[0].contextLines.forEach(function(l) {
+ assert.equal(l.text, content[0].ab[0]);
+ });
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, context);
+ groups[2].lines.forEach(function(l) {
+ assert.equal(l.text, content[0].ab[0]);
+ });
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[3].lines.length, 1);
+ assert.equal(groups[3].removes.length, 1);
+ assert.equal(groups[3].removes[0].text,
+ 'all work and no play make andybons a dull boy');
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, 5);
+ groups[4].lines.forEach(function(l) {
+ assert.equal(l.text, content[2].ab[0]);
+ });
+
+ assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[5].lines.length, 1);
+ assert.equal(groups[5].adds.length, 1);
+ assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
+
+ assert.equal(groups[6].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[6].lines.length, context);
+ groups[6].lines.forEach(function(l) {
+ assert.equal(l.text, content[4].ab[0]);
+ });
+
+ assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(groups[7].lines[0].contextLines.length, 90);
+ groups[7].lines[0].contextLines.forEach(function(l) {
+ assert.equal(l.text, content[4].ab[0]);
+ });
+
+ content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {ab: []},
+ {b: ['elgoog elgoog elgoog']},
+ ];
+ for (var i = 0; i < 50; i++) {
+ content[1].ab.push('no tv and no beer make homer go crazy');
+ }
+ groups = [];
+
+ builder._processContent(content, groups, 10);
+
+ assert.equal(groups[0].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[0].lines.length, 1);
+ assert.equal(groups[0].lines[0].text, '');
+ assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE);
+ assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE);
+
+ assert.equal(groups[1].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[1].lines.length, 1);
+ assert.equal(groups[1].removes.length, 1);
+ assert.equal(groups[1].removes[0].text,
+ 'all work and no play make andybons a dull boy');
+
+ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[2].lines.length, context);
+ groups[2].lines.forEach(function(l) {
+ assert.equal(l.text, content[1].ab[0]);
+ });
+
+ assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.equal(groups[3].lines[0].contextLines.length, 30);
+ groups[3].lines[0].contextLines.forEach(function(l) {
+ assert.equal(l.text, content[1].ab[0]);
+ });
+
+ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
+ assert.equal(groups[4].lines.length, context);
+ groups[4].lines.forEach(function(l) {
+ assert.equal(l.text, content[1].ab[0]);
+ });
+
+ assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
+ assert.equal(groups[5].lines.length, 1);
+ assert.equal(groups[5].adds.length, 1);
+ assert.equal(groups[5].adds[0].text, 'elgoog elgoog elgoog');
+ });
+
+ test('newlines', function() {
+ var text = 'abcdef';
+ assert.equal(builder._addNewlines(text, text), text);
+ text = 'a'.repeat(20);
+ assert.equal(builder._addNewlines(text, text),
+ 'a'.repeat(10) +
+ GrDiffBuilder.LINE_FEED_HTML +
+ 'a'.repeat(10));
+
+ text = '<span class="thumbsup">👍</span>';
+ var html = '<span class="thumbsup">👍</span>';
+ assert.equal(builder._addNewlines(text, html),
+ '<span clas' +
+ GrDiffBuilder.LINE_FEED_HTML +
+ 's="thumbsu' +
+ GrDiffBuilder.LINE_FEED_HTML +
+ 'p">👍</spa' +
+ GrDiffBuilder.LINE_FEED_HTML +
+ 'n>');
+
+ text = '01234\t56789';
+ assert.equal(builder._addNewlines(text, text),
+ '01234\t5' +
+ GrDiffBuilder.LINE_FEED_HTML +
+ '6789');
+ });
+
+ test('tab wrapper insertion', function() {
+ var html = 'abc\tdef';
+ var wrapper = builder._getTabWrapper(
+ builder._prefs.tab_size,
+ builder._prefs.show_tabs);
+ assert.ok(wrapper);
+ assert.isAbove(wrapper.length, 0);
+ assert.equal(builder._addTabWrappers(html), 'abc' + wrapper + 'def');
+ assert.throws(builder._getTabWrapper.bind(
+ builder,
+ '"><img src="/" onerror="alert(1);"><span class="',
+ true));
+ });
+
+ test('comments', function() {
+ var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+
+ var comments = {left: [], right:[]};
+ assert.deepEqual(builder._getCommentsForLine(comments, line), []);
+ assert.deepEqual(builder._getCommentsForLine(comments, line,
+ GrDiffBuilder.Side.LEFT), []);
+ assert.deepEqual(builder._getCommentsForLine(comments, line,
+ GrDiffBuilder.Side.RIGHT), []);
+
+ comments = {
+ left: [
+ {id: 'l3', line: 3},
+ {id: 'l5', line: 5},
+ ],
+ right: [
+ {id: 'r3', line: 3},
+ {id: 'r5', line: 5},
+ ],
+ };
+ assert.deepEqual(builder._getCommentsForLine(comments, line),
+ [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
+ assert.deepEqual(builder._getCommentsForLine(comments, line,
+ GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3}]);
+ assert.deepEqual(builder._getCommentsForLine(comments, line,
+ GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5}]);
+ });
+
+ test('comment thread creation', function() {
+ builder._comments = {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'l3', line: 3},
+ {id: 'l5', line: 5},
+ ],
+ right: [
+ {id: 'r5', line: 5},
+ ],
+ };
+
+ function checkThreadProps(patchNum, side, comments) {
+ assert.equal(threadEl.changeNum, '42');
+ assert.equal(threadEl.patchNum, patchNum);
+ assert.equal(threadEl.path, '/path/to/foo');
+ assert.equal(threadEl.side, side);
+ assert.deepEqual(threadEl.projectConfig, {foo: 'bar'});
+ assert.deepEqual(threadEl.comments, comments);
+ }
+
+ var line = new GrDiffLine(GrDiffLine.Type.BOTH);
+ line.beforeNumber = 5;
+ line.afterNumber = 5;
+ threadEl = builder._commentThreadForLine(line);
+ checkThreadProps('3', 'REVISION',
+ [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+ threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
+ checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+
+ threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
+ checkThreadProps('3', 'PARENT', [{id: 'l5', line: 5}]);
+
+ builder._comments.meta.patchRange.basePatchNum = '1';
+
+ threadEl = builder._commentThreadForLine(line);
+ checkThreadProps('3', 'REVISION',
+ [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+ threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.LEFT);
+ checkThreadProps('1', 'REVISION', [{id: 'l5', line: 5}]);
+
+ threadEl = builder._commentThreadForLine(line, GrDiffBuilder.Side.RIGHT);
+ checkThreadProps('3', 'REVISION', [{id: 'r5', line: 5}]);
+
+ builder._comments.meta.patchRange.basePatchNum = 'PARENT';
+
+ line = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ line.beforeNumber = 5;
+ line.afterNumber = 5;
+ threadEl = builder._commentThreadForLine(line);
+ checkThreadProps('3', 'PARENT',
+ [{id: 'l5', line: 5}, {id: 'r5', line: 5}]);
+
+ line = new GrDiffLine(GrDiffLine.Type.ADD);
+ line.beforeNumber = 3;
+ line.afterNumber = 5;
+ threadEl = builder._commentThreadForLine(line);
+ checkThreadProps('3', 'REVISION',
+ [{id: 'l3', line: 3}, {id: 'r5', line: 5}]);
+ });
+
+ test('break up common diff chunks', function() {
+ builder._commentLocations = {
+ left: {1: true},
+ right: {10: true},
+ };
+ var lineNums = {
+ left: 0,
+ right: 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 = builder._splitCommonGroupsWithComments(content, lineNums);
+ assert.deepEqual(result, [
+ {
+ ab: ['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, ',
+ ]
+ },
+ {
+ ab: ['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.',
+ ]
+ }
+ ]);
+ });
+
+ 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 = GrDiffBuilder.prototype._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 = GrDiffBuilder.prototype._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,
+ }
+ ]);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
new file mode 100644
index 0000000..750f7da
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -0,0 +1,77 @@
+// 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(window, GrDiffLine) {
+ 'use strict';
+
+ function GrDiffGroup(type, opt_lines) {
+ this.type = type;
+ this.lines = [];
+ this.adds = [];
+ this.removes = [];
+
+ if (opt_lines) {
+ opt_lines.forEach(this.addLine, this);
+ }
+ }
+
+ GrDiffGroup.Type = {
+ BOTH: 'both',
+ CONTEXT_CONTROL: 'contextControl',
+ DELTA: 'delta',
+ };
+
+ GrDiffGroup.prototype.addLine = function(line) {
+ this.lines.push(line);
+
+ var notDelta = (this.type === GrDiffGroup.Type.BOTH ||
+ this.type === GrDiffGroup.Type.CONTEXT_CONTROL);
+ if (notDelta && (line.type === GrDiffLine.Type.ADD ||
+ line.type === GrDiffLine.Type.REMOVE)) {
+ throw Error('Cannot add delta line to a non-delta group.');
+ }
+
+ if (line.type === GrDiffLine.Type.ADD) {
+ this.adds.push(line);
+ } else if (line.type === GrDiffLine.Type.REMOVE) {
+ this.removes.push(line);
+ }
+ };
+
+ GrDiffGroup.prototype.getSideBySidePairs = function() {
+ if (this.type === GrDiffGroup.Type.BOTH ||
+ this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {
+ return this.lines.map(function(line) {
+ return {
+ left: line,
+ right: line,
+ };
+ });
+ }
+
+ var pairs = [];
+ var i = 0;
+ var j = 0;
+ while (i < this.removes.length || j < this.adds.length) {
+ pairs.push({
+ left: this.removes[i] || GrDiffLine.BLANK_LINE,
+ right: this.adds[j] || GrDiffLine.BLANK_LINE,
+ });
+ i++;
+ j++;
+ }
+ return pairs;
+ };
+
+ window.GrDiffGroup = GrDiffGroup;
+})(window, GrDiffLine);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
new file mode 100644
index 0000000..d3063e7
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-group</title>
+
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<script src="gr-diff-line.js"></script>
+<script src="gr-diff-group.js"></script>
+
+<script>
+ suite('gr-diff-group tests', function() {
+
+ test('delta line pairs', function() {
+ var group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
+ var l1 = new GrDiffLine(GrDiffLine.Type.ADD);
+ var l2 = new GrDiffLine(GrDiffLine.Type.ADD);
+ var l3 = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ group.addLine(l1);
+ group.addLine(l2);
+ group.addLine(l3);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
+
+ var pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
+
+ group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, [l1, l2]);
+ assert.deepEqual(group.removes, [l3]);
+
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l3, right: l1},
+ {left: GrDiffLine.BLANK_LINE, right: l2},
+ ]);
+ });
+
+ test('group/header line pairs', function() {
+ var l1 = new GrDiffLine(GrDiffLine.Type.BOTH);
+ var l2 = new GrDiffLine(GrDiffLine.Type.BOTH);
+ var l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+ var group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
+
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ var pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]);
+ assert.deepEqual(group.lines, [l1, l2, l3]);
+ assert.deepEqual(group.adds, []);
+ assert.deepEqual(group.removes, []);
+
+ pairs = group.getSideBySidePairs();
+ assert.deepEqual(pairs, [
+ {left: l1, right: l1},
+ {left: l2, right: l2},
+ {left: l3, right: l3},
+ ]);
+ });
+
+ test('adding delta lines to non-delta group', function() {
+ var l1 = new GrDiffLine(GrDiffLine.Type.ADD);
+ var l2 = new GrDiffLine(GrDiffLine.Type.REMOVE);
+ var l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
+
+ var group = new GrDiffGroup(GrDiffGroup.Type.BOTH);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+
+ group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL);
+ assert.throws(group.addLine.bind(group, l1));
+ assert.throws(group.addLine.bind(group, l2));
+ assert.doesNotThrow(group.addLine.bind(group, l3));
+ });
+ });
+
+</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
new file mode 100644
index 0000000..ea00a3d
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -0,0 +1,43 @@
+// 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(window) {
+ 'use strict';
+
+ function GrDiffLine(type) {
+ this.type = type;
+ this.contextLines = [];
+ this.highlights = [];
+ }
+
+ GrDiffLine.prototype.beforeNumber = 0;
+
+ GrDiffLine.prototype.afterNumber = 0;
+
+ GrDiffLine.prototype.text = '';
+
+ GrDiffLine.Type = {
+ ADD: 'add',
+ BOTH: 'both',
+ BLANK: 'blank',
+ CONTEXT_CONTROL: 'contextControl',
+ REMOVE: 'remove',
+ };
+
+ GrDiffLine.FILE = 'FILE';
+
+ GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK);
+
+ window.GrDiffLine = GrDiffLine;
+
+})(window);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index a6d92a2..e0306ad 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -20,13 +20,19 @@
<link rel="import" href="../../shared/gr-request/gr-request.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.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>
+ :host {
+ --light-remove-highlight-color: #fee;
+ --dark-remove-highlight-color: #ffd4d4;
+ --light-add-highlight-color: #efe;
+ --dark-add-highlight-color: #d4ffd4;
+ }
.loading {
padding: 0 var(--default-horizontal-margin) 1em;
color: #666;
@@ -45,16 +51,99 @@
display: flex;
font: 12px var(--monospace-font-family);
overflow-x: auto;
+ will-change: transform;
}
- 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;
+ table {
+ border-collapse: collapse;
border-right: 1px solid #ddd;
}
+ .section {
+ background-color: #eee;
+ }
+ .blank,
+ .content {
+ background-color: #fff;
+ }
+ .lineNum,
+ .content {
+ vertical-align: top;
+ white-space: pre;
+ }
+ .contextLineNum:before,
+ .lineNum:before {
+ display: inline-block;
+ color: #666;
+ content: attr(data-value);
+ padding: 0 .75em;
+ text-align: right;
+ width: 100%;
+ }
+ .canComment .lineNum[data-value] {
+ cursor: pointer;
+ }
+ .canComment .lineNum[data-value]:before {
+ text-decoration: underline;
+ }
+ .canComment .lineNum[data-value]:hover:before {
+ background-color: #ccc;
+ }
+ .canComment .lineNum[data-value="FILE"]:before {
+ content: 'File';
+ }
+ .content {
+ overflow: hidden;
+ min-width: var(--content-width, 80ch);
+ }
+ .content.left {
+ -webkit-user-select: var(--left-user-select, text);
+ -moz-user-select: var(--left-user-select, text);
+ -ms-user-select: var(--left-user-select, text);
+ user-select: var(--left-user-select, text);
+ }
+ .content.right {
+ -webkit-user-select: var(--right-user-select, text);
+ -moz-user-select: var(--right-user-select, text);
+ -ms-user-select: var(--right-user-select, text);
+ user-select: var(--right-user-select, text);
+ }
+ .content.add hl,
+ .content.add.darkHighlight {
+ background-color: var(--dark-add-highlight-color);
+ }
+ .content.add.lightHighlight {
+ background-color: var(--light-add-highlight-color);
+ }
+ .content.remove hl,
+ .content.remove.darkHighlight {
+ background-color: var(--dark-remove-highlight-color);
+ }
+ .content.remove.lightHighlight {
+ background-color: var(--light-remove-highlight-color);
+ }
+ .contextControl {
+ color: #849;
+ background-color: #fef;
+ }
+ .contextControl gr-button {
+ display: block;
+ font-family: var(--monospace-font-family);
+ text-decoration: none;
+ }
+ .contextControl td:not(.lineNum) {
+ text-align: center;
+ }
+ .br:after {
+ /* Line feed */
+ content: '\A';
+ }
+ .tab {
+ display: inline-block;
+ }
+ .tab.withIndicator:before {
+ color: #C62828;
+ /* >> character */
+ content: '\00BB';
+ }
</style>
<div class="loading" hidden$="[[!_loading]]">Loading...</div>
<div hidden$="[[_loading]]" hidden>
@@ -67,44 +156,29 @@
<gr-button link
class="prefsButton"
on-tap="_handlePrefsTap"
- hidden$="[[!prefs]]"
+ hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"
hidden>Diff View Preferences</gr-button>
</div>
<gr-overlay id="prefsOverlay" with-backdrop>
<gr-diff-preferences
- prefs="{{prefs}}"
+ 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 class$="[[_computeContainerClass(_loggedIn, _viewMode)]]"
+ on-tap="_handleTap"
+ on-mousedown="_handleMouseDown"
+ on-copy="_handleCopy">
+ <table id="diffTable"></table>
</div>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
+ <script src="gr-diff-line.js"></script>
+ <script src="gr-diff-group.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.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
index cac1f3e..12ceacb 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,6 +14,16 @@
(function() {
'use strict';
+ var DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ };
+
+ var DiffSide = {
+ LEFT: 'left',
+ RIGHT: 'right',
+ };
+
Polymer({
is: 'gr-diff',
@@ -26,148 +36,379 @@
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: {
+ projectConfig: {
type: Object,
- readOnly: true,
- value: function() {
- return new Promise(function(resolve) {
- this._resolvePrefsReady = resolve;
- }.bind(this));
- },
+ observer: '_projectConfigChanged',
},
- _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,
-
- _diffPreferencesPromise: Object, // Used for testing.
+ _viewMode: {
+ type: String,
+ value: DiffViewMode.SIDE_BY_SIDE,
+ },
+ _diff: Object,
+ _diffBuilder: Object,
+ _prefs: Object,
+ _selectionSide: {
+ type: String,
+ observer: '_selectionSideChanged',
+ },
+ _comments: Object,
+ _focusedSection: {
+ type: Number,
+ value: -1,
+ },
+ _focusedThread: {
+ type: Number,
+ value: -1,
+ },
},
observers: [
- '_prefsChanged(prefs.*)',
+ '_prefsChanged(_prefs.*)',
],
- ready: function() {
- app.accountReady.then(function() {
- this._loggedIn = app.loggedIn;
+ attached: function() {
+ this._getLoggedIn().then(function(loggedIn) {
+ this._loggedIn = 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();
+ this.addEventListener('thread-discard',
+ this._handleThreadDiscard.bind(this));
+ this.addEventListener('comment-discard',
+ this._handleCommentDiscard.bind(this));
},
reload: function() {
+ this._clearDiffContent();
this._loading = true;
- // 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 diffLoaded = this._getDiff().then(function(diff) {
- this._diffResponse = diff;
+ var promises = [];
+
+ promises.push(this._getDiff().then(function(diff) {
+ this._diff = diff;
+ this._loading = false;
+ }.bind(this)));
+
+ promises.push(this._getDiffCommentsAndDrafts().then(function(comments) {
+ this._comments = comments;
+ }.bind(this)));
+
+ promises.push(this._getDiffPreferences().then(function(prefs) {
+ this._prefs = prefs;
+ }.bind(this)));
+
+ return Promise.all(promises).then(function() {
+ this._render();
}.bind(this));
+ },
- var promises = [
- this._prefsReady,
- diffLoaded,
- ];
+ showDiffPreferences: function() {
+ this.$.prefsOverlay.open();
+ },
- return app.accountReady.then(function() {
- promises.push(this._getDiffComments().then(function(res) {
- this._baseComments = res.baseComments;
- this._comments = res.comments;
- }.bind(this)));
+ scrollToLine: function(lineNum) {
+ if (isNaN(lineNum) || lineNum < 1) { return; }
- if (!app.loggedIn) {
- this._baseDrafts = [];
- this._drafts = [];
- } else {
- promises.push(this._getDiffDrafts().then(function(res) {
- this._baseDrafts = res.baseComments;
- this._drafts = res.comments;
- }.bind(this)));
- }
+ var lineEls = Polymer.dom(this.root).querySelectorAll(
+ '.lineNum[data-value="' + lineNum + '"]');
- return Promise.all(promises).then(function() {
- this._render();
- this._loading = false;
- }.bind(this)).catch(function(err) {
- this._loading = false;
- alert('Oops. Something went wrong. Check the console and bug the ' +
+ // Always choose the right side.
+ var el = lineEls.length === 2 ? lineEls[1] : lineEls[0];
+ this._scrollToElement(el);
+ },
+
+ scrollToNextDiffChunk: function() {
+ this._focusedSection = this._advanceElementWithinNodeList(
+ this._getDeltaSections(), this._focusedSection, 1);
+ },
+
+ scrollToPreviousDiffChunk: function() {
+ this._focusedSection = this._advanceElementWithinNodeList(
+ this._getDeltaSections(), this._focusedSection, -1);
+ },
+
+ scrollToNextCommentThread: function() {
+ this._focusedThread = this._advanceElementWithinNodeList(
+ this._getCommentThreads(), this._focusedThread, 1);
+ },
+
+ scrollToPreviousCommentThread: function() {
+ this._focusedThread = this._advanceElementWithinNodeList(
+ this._getCommentThreads(), this._focusedThread, -1);
+ },
+
+ _advanceElementWithinNodeList: function(els, curIndex, direction) {
+ var idx = Math.max(0, Math.min(els.length - 1, curIndex + direction));
+ if (curIndex !== idx) {
+ this._scrollToElement(els[idx]);
+ return idx;
+ }
+ return curIndex;
+ },
+
+ _getCommentThreads: function() {
+ return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
+ },
+
+ _getDeltaSections: function() {
+ return Polymer.dom(this.root).querySelectorAll('.section.delta');
+ },
+
+ _scrollToElement: function(el) {
+ if (!el) { return; }
+
+ // Calculate where the element is relative to the window.
+ var top = el.offsetTop;
+ for (var offsetParent = el.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+
+ // Scroll the element to the middle of the window. Dividing by a third
+ // instead of half the inner height feels a bit better otherwise the
+ // element appears to be below the center of the window even when it
+ // isn't.
+ window.scrollTo(0, top - (window.innerHeight / 3) +
+ (el.offsetHeight / 2));
+ },
+
+ _computeContainerClass: function(loggedIn, viewMode) {
+ var classes = ['diffContainer'];
+ switch (viewMode) {
+ case DiffViewMode.UNIFIED:
+ classes.push('unified');
+ break;
+ case DiffViewMode.SIDE_BY_SIDE:
+ classes.push('sideBySide');
+ break
+ default:
+ throw Error('Invalid view mode: ', viewMode);
+ }
+ if (loggedIn) {
+ classes.push('canComment');
+ }
+ return classes.join(' ');
+ },
+
+ _computePrefsButtonHidden: function(prefs, loggedIn) {
+ return !loggedIn || !prefs;
+ },
+
+ _handlePrefsTap: function(e) {
+ e.preventDefault();
+ this.$.prefsOverlay.open();
+ },
+
+ _handlePrefsSave: function(e) {
+ e.stopPropagation();
+ var el = Polymer.dom(e).rootTarget;
+ el.disabled = true;
+ 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));
+ throw err;
+ });
+ },
+
+ _saveDiffPreferences: function() {
+ return this.$.restAPI.saveDiffPreferences(this._prefs);
+ },
+
+ _handlePrefsCancel: function(e) {
+ e.stopPropagation();
+ this.$.prefsOverlay.close();
+ },
+
+ _handleTap: function(e) {
+ var el = Polymer.dom(e).rootTarget;
+
+ if (el.classList.contains('showContext')) {
+ this._showContext(e.detail.group, e.detail.section);
+ } else if (el.classList.contains('lineNum')) {
+ this._handleLineTap(el);
+ }
+ },
+
+ _handleLineTap: function(el) {
+ this._getLoggedIn().then(function(loggedIn) {
+ if (!loggedIn) { return; }
+
+ var value = el.getAttribute('data-value');
+ if (value === GrDiffLine.FILE) {
+ this._addDraft(el);
+ return;
+ }
+ var lineNum = parseInt(value, 10);
+ if (isNaN(lineNum)) {
+ throw Error('Invalid line number: ' + value);
+ }
+ this._addDraft(el, lineNum);
}.bind(this));
},
+ _addDraft: function(lineEl, opt_lineNum) {
+ var threadEl;
+
+ // Does a thread already exist at this line?
+ var contentEl = lineEl.nextSibling;
+ while (contentEl && !contentEl.classList.contains('content')) {
+ contentEl = contentEl.nextSibling;
+ }
+ if (contentEl.childNodes.length > 0 &&
+ contentEl.lastChild.nodeName === 'GR-DIFF-COMMENT-THREAD') {
+ threadEl = contentEl.lastChild;
+ } else {
+ var patchNum = this.patchRange.patchNum;
+ var side = 'REVISION';
+ if (contentEl.classList.contains(DiffSide.LEFT) ||
+ contentEl.classList.contains('remove')) {
+ if (this.patchRange.basePatchNum === 'PARENT') {
+ side = 'PARENT';
+ } else {
+ patchNum = this.patchRange.basePatchNum;
+ }
+ }
+ threadEl = this._builder.createCommentThread(this.changeNum, patchNum,
+ this.path, side, this.projectConfig);
+ contentEl.appendChild(threadEl);
+ }
+ threadEl.addDraft(opt_lineNum);
+ },
+
+ _handleThreadDiscard: function(e) {
+ var el = Polymer.dom(e).rootTarget;
+ el.parentNode.removeChild(el);
+ },
+
+ _handleCommentDiscard: function(e) {
+ var comment = Polymer.dom(e).rootTarget.comment;
+ this._removeComment(comment);
+ },
+
+ _removeComment: function(comment) {
+ if (!comment.id) { return; }
+ this._removeCommentFromSide(comment, DiffSide.LEFT) ||
+ this._removeCommentFromSide(comment, DiffSide.RIGHT);
+ },
+
+ _removeCommentFromSide: function(comment, side) {
+ var idx = -1;
+ for (var i = 0; i < this._comments[side].length; i++) {
+ if (this._comments[side][i].id === comment.id) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx !== -1) {
+ this.splice('_comments.' + side, idx, 1);
+ return true;
+ }
+ return false;
+ },
+
+ _handleMouseDown: function(e) {
+ var el = Polymer.dom(e).rootTarget;
+ var side;
+ for (var node = el; node != null; node = node.parentNode) {
+ if (!node.classList) { continue; }
+
+ if (node.classList.contains(DiffSide.LEFT)) {
+ side = DiffSide.LEFT;
+ break;
+ } else if (node.classList.contains(DiffSide.RIGHT)) {
+ side = DiffSide.RIGHT;
+ break;
+ }
+ }
+ this._selectionSide = side;
+ },
+
+ _selectionSideChanged: function(side) {
+ if (side) {
+ var oppositeSide = side === DiffSide.RIGHT ?
+ DiffSide.LEFT : DiffSide.RIGHT;
+ this.customStyle['--' + side + '-user-select'] = 'text';
+ this.customStyle['--' + oppositeSide + '-user-select'] = 'none';
+ } else {
+ this.customStyle['--left-user-select'] = 'text';
+ this.customStyle['--right-user-select'] = 'text';
+ }
+ this.updateStyles();
+ },
+
+ _handleCopy: function(e) {
+ var text = this._getSelectedText(this._selectionSide);
+ e.clipboardData.setData('Text', text);
+ e.preventDefault();
+ },
+
+ _getSelectedText: function(opt_side) {
+ var sel = window.getSelection();
+ var range = sel.getRangeAt(0);
+ var doc = range.cloneContents();
+ var selector = '.content';
+ if (opt_side) {
+ selector += '.' + opt_side;
+ }
+ var contentEls = Polymer.dom(doc).querySelectorAll(selector);
+
+ if (contentEls.length === 0) {
+ return doc.textContent;
+ }
+
+ var text = '';
+ for (var i = 0; i < contentEls.length; i++) {
+ text += contentEls[i].textContent + '\n';
+ }
+ return text;
+ },
+
+ _showContext: function(group, sectionEl) {
+ this._builder.emitGroup(group, sectionEl);
+ sectionEl.parentNode.removeChild(sectionEl);
+ },
+
+ _prefsChanged: function(prefsChangeRecord) {
+ var prefs = prefsChangeRecord.base;
+ this.customStyle['--content-width'] = prefs.line_length + 'ch';
+ this.updateStyles();
+
+ if (this._diff && this._comments) {
+ this._render();
+ }
+ },
+
+ _render: function() {
+ this._clearDiffContent();
+ this._builder = this._getDiffBuilder(this._diff, this._comments,
+ this._prefs);
+ this._builder.emitDiff(this._diff.content);
+
+ this.async(function() {
+ this.fire('render', null, {bubbles: false});
+ }.bind(this), 1);
+ },
+
+ _clearDiffContent: function() {
+ this.$.diffTable.innerHTML = null;
+ },
+
_getDiff: function() {
return this.$.restAPI.getDiff(
this.changeNum,
@@ -185,528 +426,95 @@
},
_getDiffDrafts: function() {
- return this.$.restAPI.getDiffDrafts(
- this.changeNum,
- this.patchRange.basePatchNum,
- this.patchRange.patchNum,
- this.path);
- },
-
- 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;
- },
-
- _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;
+ return this._getLoggedIn().then(function(loggedIn) {
+ if (!loggedIn) {
+ return Promise.resolve({baseComments: [], comments: []});
}
- 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;
- });
+ return this.$.restAPI.getDiffDrafts(
+ this.changeNum,
+ this.patchRange.basePatchNum,
+ this.patchRange.patchNum,
+ this.path);
}.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;
+ _getDiffCommentsAndDrafts: function() {
+ var promises = [];
+ promises.push(this._getDiffComments());
+ promises.push(this._getDiffDrafts());
+ return Promise.all(promises).then(function(results) {
+ return Promise.resolve({
+ comments: results[0],
+ drafts: results[1],
+ });
+ }).then(this._normalizeDiffCommentsAndDrafts.bind(this));
},
- _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;
+ _getDiffPreferences: function() {
+ return this._getLoggedIn().then(function(loggedIn) {
+ if (!loggedIn) {
+ // These defaults should match the defaults in
+ // gerrit-extension-api/src/main/jcg/gerrit/extensions/client/DiffPreferencesInfo.java
+ // NOTE: There are some settings that don't apply to PolyGerrit
+ // (Render mode being at least one of them).
+ return Promise.resolve({
+ auto_hide_diff_table_header: true,
+ context: 10,
+ cursor_blink_rate: 0,
+ ignore_whitespace: 'IGNORE_NONE',
+ intraline_difference: true,
+ line_length: 100,
+ show_line_endings: true,
+ show_tabs: true,
+ show_whitespace_errors: true,
+ syntax_highlighting: true,
+ tab_size: 8,
+ theme: 'DEFAULT',
+ });
}
- }
- 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);
+ return this.$.restAPI.getDiffPreferences();
+ }.bind(this));
},
- _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);
+ _normalizeDiffCommentsAndDrafts: function(results) {
+ function markAsDraft(d) {
+ d.__draft = true;
+ return d;
}
- 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,
+ var baseDrafts = results.drafts.baseComments.map(markAsDraft);
+ var drafts = results.drafts.comments.map(markAsDraft);
+ return Promise.resolve({
+ meta: {
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,
+ changeNum: this.changeNum,
+ patchRange: this.patchRange,
+ projectConfig: this.projectConfig,
},
- 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,
- };
+ left: results.comments.baseComments.concat(baseDrafts),
+ right: results.comments.comments.concat(drafts),
+ });
},
- // 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;
+ _getLoggedIn: function() {
+ return this.$.restAPI.getLoggedIn();
},
- _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;
+ _getDiffBuilder: function(diff, comments, prefs) {
+ if (this._viewMode === DiffViewMode.SIDE_BY_SIDE) {
+ return new GrDiffBuilderSideBySide(diff, comments, prefs,
+ this.$.diffTable);
+ } else if (this._viewMode === DiffViewMode.UNIFIED) {
+ return new GrDiffBuilderUnified(diff, comments, prefs,
+ this.$.diffTable);
+ }
+ throw Error('Unsupported diff view mode: ' + this._viewMode);
},
- _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 + ']');
+ _projectConfigChanged: function(projectConfig) {
+ var threadEls = this._getCommentThreads();
+ for (var i = 0; i < threadEls.length; i++) {
+ threadEls[i].projectConfig = projectConfig;
}
- 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
index e46bbb8..579957e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -20,8 +20,6 @@
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/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">
@@ -35,458 +33,214 @@
<script>
suite('gr-diff tests', function() {
var element;
- var server;
- var getDiffStub;
- var getCommentsStub;
setup(function() {
+ stub('gr-rest-api-interface', {
+ getLoggedIn: function() { return Promise.resolve(false); },
+ })
element = fixture('basic');
- element.changeNum = 42;
- element.path = 'sieve.go';
- element.prefs = {
- context: 10,
- tab_size: 8,
- };
-
- getDiffStub = sinon.stub(element.$.restAPI, 'getDiff', function() {
- return Promise.resolve({
- 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.',
- ]
- },
- ]
- });
- });
-
- getCommentsStub = sinon.stub(element.$.restAPI, 'getDiffComments',
- function() {
- return Promise.resolve({
- baseComments: [
- {
- 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',
- }
- ],
- comments: [
- {
- 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: 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 = sinon.fakeServer.create();
- server.respondWith(
- 'PUT',
- '/accounts/self/preferences.diff',
- [
- 200,
- {'Content-Type': 'application/json'},
- ')]}\'\n' +
- JSON.stringify({context: 25}),
- ]
- );
-
});
- teardown(function() {
- getDiffStub.restore();
- getCommentsStub.restore();
- server.restore();
- });
+ test('get drafts logged out', function(done) {
+ element.patchRange = {basePatchNum: 0, patchNum: 0};
- test('comment rendering', function(done) {
- element.prefs.context = -1;
- element._loggedIn = true;
- element.patchRange = {
- basePatchNum: 1,
- patchNum: 2,
- };
-
- element.reload().then(function() {
- flush(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);
- flush(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('thread-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('comment-discard');
- });
- });
- });
- server.respond();
- });
-
- 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));
+ var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts');
+ var loggedInStub = sinon.stub(element, '_getLoggedIn',
+ function() { return Promise.resolve(false); });
+ element._getDiffDrafts().then(function(result) {
+ assert.deepEqual(result, {baseComments: [], comments: []});
+ sinon.assert.notCalled(getDraftsStub);
+ loggedInStub.restore();
+ getDraftsStub.restore();
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('get drafts logged in', function(done) {
+ element.patchRange = {basePatchNum: 0, patchNum: 0};
+ var draftsResponse = {
+ baseComments: [{id: 'foo'}],
+ comments: [{id: 'bar'}],
+ };
+ var getDraftsStub = sinon.stub(element.$.restAPI, 'getDiffDrafts',
+ function() { return Promise.resolve(draftsResponse); });
+ var loggedInStub = sinon.stub(element, '_getLoggedIn',
+ function() { return Promise.resolve(true); });
+ element._getDiffDrafts().then(function(result) {
+ assert.deepEqual(result, draftsResponse);
+ loggedInStub.restore();
+ getDraftsStub.restore();
+ done();
+ });
});
- test('break up common diff chunks', function() {
- element._groupedBaseComments = {
- 1: {},
+ test('get comments and drafts', function(done) {
+ var loggedInStub = sinon.stub(element, '_getLoggedIn',
+ function() { return Promise.resolve(true); });
+ var comments = {
+ baseComments: [
+ {id: 'bc1'},
+ {id: 'bc2'},
+ ],
+ comments: [
+ {id: 'c1'},
+ {id: 'c2'},
+ ],
};
- element._groupedComments = {
- 10: {},
+ var diffCommentsStub = sinon.stub(element, '_getDiffComments',
+ function() { return Promise.resolve(comments); });
+
+ var drafts = {
+ baseComments: [
+ {id: 'bd1'},
+ {id: 'bd2'},
+ ],
+ comments: [
+ {id: 'd1'},
+ {id: 'd2'},
+ ],
};
- var ctx = {
- left: {lineNum: 0},
- right: {lineNum: 0},
+ var diffDraftsStub = sinon.stub(element, '_getDiffDrafts',
+ function() { return Promise.resolve(drafts); });
+
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
};
- 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'],
+ element.path = '/path/to/foo';
+ element.projectConfig = {foo: 'bar'};
+
+ element._getDiffCommentsAndDrafts().then(function(result) {
+ assert.deepEqual(result, {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1'},
+ {id: 'bc2'},
+ {id: 'bd1', __draft: true},
+ {id: 'bd2', __draft: true},
+ ],
+ right: [
+ {id: 'c1'},
+ {id: 'c2'},
+ {id: 'd1', __draft: true},
+ {id: 'd2', __draft: true},
+ ],
+ });
+
+ diffCommentsStub.restore();
+ diffDraftsStub.restore();
+ loggedInStub.restore();
+ done();
+ });
+ });
+
+ test('remove comment', function() {
+ element._comments = {
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
},
- {
- 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, ',
- ]
+ left: [
+ {id: 'bc1'},
+ {id: 'bc2'},
+ {id: 'bd1', __draft: true},
+ {id: 'bd2', __draft: true},
+ ],
+ right: [
+ {id: 'c1'},
+ {id: 'c2'},
+ {id: 'd1', __draft: true},
+ {id: 'd2', __draft: true},
+ ],
+ };
+
+ element._removeComment({});
+ // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem to
+ // believe that one object deepEquals another even when they do :-/.
+ assert.equal(JSON.stringify(element._comments), JSON.stringify({
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
},
- {
- __noHighlight: true,
- a: ['software distributed under the License is distributed on an '],
- b: ['software distributed under the License is distributed on an ']
+ left: [
+ {id: 'bc1'},
+ {id: 'bc2'},
+ {id: 'bd1', __draft: true},
+ {id: 'bd2', __draft: true},
+ ],
+ right: [
+ {id: 'c1'},
+ {id: 'c2'},
+ {id: 'd1', __draft: true},
+ {id: 'd2', __draft: true},
+ ],
+ }));
+
+ element._removeComment({id: 'bc2'});
+ assert.equal(JSON.stringify(element._comments), JSON.stringify({
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
},
- {
- 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.',
- ]
- }
- ]);
+ left: [
+ {id: 'bc1'},
+ {id: 'bd1', __draft: true},
+ {id: 'bd2', __draft: true},
+ ],
+ right: [
+ {id: 'c1'},
+ {id: 'c2'},
+ {id: 'd1', __draft: true},
+ {id: 'd2', __draft: true},
+ ],
+ }));
+
+ element._removeComment({id: 'd2'});
+ assert.deepEqual(JSON.stringify(element._comments), JSON.stringify({
+ meta: {
+ changeNum: '42',
+ patchRange: {
+ basePatchNum: 'PARENT',
+ patchNum: 3,
+ },
+ path: '/path/to/foo',
+ projectConfig: {foo: 'bar'},
+ },
+ left: [
+ {id: 'bc1'},
+ {id: 'bd1', __draft: true},
+ {id: 'bd2', __draft: true},
+ ],
+ right: [
+ {id: 'c1'},
+ {id: 'c2'},
+ {id: 'd1', __draft: true},
+ ],
+ }));
});
});
-
</script>