
Modifies the `process` method of gr-diff-processor to traverse diff content using an asynchronous recursive loop rather than a blocking for-loop. The async version maintains the promise interface already established. Refactored to constrain side-effects to the `process` method. Whereas, formerly, helper methods in gr-diff-processor both read and wrote the component's internal state, they are rewritten to be more pure, making them simpler to understand, test, and invoke asynchronously. Documentation is added throughout as well as tests for helper functions. Note that this change only makes the processing step asynchronous. Upgrading the diff-rendering stage to be non-blocking will come in a later change. Change-Id: Ifd50eeb75797b173937890caacc92cad5675fc20
491 lines
16 KiB
JavaScript
491 lines
16 KiB
JavaScript
// 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';
|
|
|
|
// Prevent redefinition.
|
|
if (window.GrDiffBuilder) { return; }
|
|
|
|
function GrDiffBuilder(diff, comments, prefs, outputEl) {
|
|
this._diff = diff;
|
|
this._comments = comments;
|
|
this._prefs = prefs;
|
|
this._outputEl = outputEl;
|
|
this.groups = [];
|
|
}
|
|
|
|
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.ContextButtonType = {
|
|
ABOVE: 'above',
|
|
BELOW: 'below',
|
|
ALL: 'all',
|
|
};
|
|
|
|
var PARTIAL_CONTEXT_AMOUNT = 10;
|
|
|
|
GrDiffBuilder.prototype.emitDiff = function() {
|
|
for (var i = 0; i < this.groups.length; i++) {
|
|
this.emitGroup(this.groups[i]);
|
|
}
|
|
};
|
|
|
|
GrDiffBuilder.prototype.buildSectionElement = function(group) {
|
|
throw Error('Subclasses must implement buildGroupElement');
|
|
};
|
|
|
|
GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
|
|
var element = this.buildSectionElement(group);
|
|
this._outputEl.insertBefore(element, opt_beforeSection);
|
|
group.element = element;
|
|
};
|
|
|
|
GrDiffBuilder.prototype.renderSection = function(element) {
|
|
for (var i = 0; i < this.groups.length; i++) {
|
|
var group = this.groups[i];
|
|
if (group.element === element) {
|
|
var newElement = this.buildSectionElement(group);
|
|
group.element.parentElement.replaceChild(newElement, group.element);
|
|
group.element = newElement;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
GrDiffBuilder.prototype.getGroupsByLineRange = function(
|
|
startLine, endLine, opt_side) {
|
|
var groups = [];
|
|
for (var i = 0; i < this.groups.length; i++) {
|
|
var group = this.groups[i];
|
|
if (group.lines.length === 0) {
|
|
continue;
|
|
}
|
|
var groupStartLine = 0;
|
|
var groupEndLine = 0;
|
|
switch (group.type) {
|
|
case GrDiffGroup.Type.BOTH:
|
|
if (opt_side === GrDiffBuilder.Side.LEFT) {
|
|
groupStartLine = group.lines[0].beforeNumber;
|
|
groupEndLine = group.lines[group.lines.length - 1].beforeNumber;
|
|
} else if (opt_side === GrDiffBuilder.Side.RIGHT) {
|
|
groupStartLine = group.lines[0].afterNumber;
|
|
groupEndLine = group.lines[group.lines.length - 1].afterNumber;
|
|
}
|
|
break;
|
|
case GrDiffGroup.Type.DELTA:
|
|
if (opt_side === GrDiffBuilder.Side.LEFT && group.removes.length) {
|
|
groupStartLine = group.removes[0].beforeNumber;
|
|
groupEndLine = group.removes[group.removes.length - 1].beforeNumber;
|
|
} else if (group.adds.length) {
|
|
groupStartLine = group.adds[0].afterNumber;
|
|
groupEndLine = group.adds[group.adds.length - 1].afterNumber;
|
|
}
|
|
break;
|
|
}
|
|
if (groupStartLine === 0) { // Line was removed or added.
|
|
groupStartLine = groupEndLine;
|
|
}
|
|
if (groupEndLine === 0) { // Line was removed or added.
|
|
groupEndLine = groupStartLine;
|
|
}
|
|
if (startLine <= groupEndLine && endLine >= groupStartLine) {
|
|
groups.push(group);
|
|
}
|
|
}
|
|
return groups;
|
|
};
|
|
|
|
GrDiffBuilder.prototype.getSectionsByLineRange = function(
|
|
startLine, endLine, opt_side) {
|
|
return this.getGroupsByLineRange(startLine, endLine, opt_side).map(
|
|
function(group) { return group.element; });
|
|
};
|
|
|
|
GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) {
|
|
return this._commentLocations[side][lineNum] === true;
|
|
};
|
|
|
|
// TODO(wyatta): Move this completely into the processor.
|
|
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.contextGroup =
|
|
new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines);
|
|
groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
|
|
[ctxLine]));
|
|
|
|
if (linesAfterCtx.length > 0) {
|
|
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
|
|
}
|
|
};
|
|
|
|
GrDiffBuilder.prototype._createContextControl = function(section, line) {
|
|
if (!line.contextGroup || !line.contextGroup.lines.length) {
|
|
return null;
|
|
}
|
|
|
|
var td = this._createElement('td');
|
|
var showPartialLinks =
|
|
line.contextGroup.lines.length > PARTIAL_CONTEXT_AMOUNT;
|
|
|
|
if (showPartialLinks) {
|
|
td.appendChild(this._createContextButton(
|
|
GrDiffBuilder.ContextButtonType.ABOVE, section, line));
|
|
td.appendChild(document.createTextNode(' - '));
|
|
}
|
|
|
|
td.appendChild(this._createContextButton(
|
|
GrDiffBuilder.ContextButtonType.ALL, section, line));
|
|
|
|
if (showPartialLinks) {
|
|
td.appendChild(document.createTextNode(' - '));
|
|
td.appendChild(this._createContextButton(
|
|
GrDiffBuilder.ContextButtonType.BELOW, section, line));
|
|
}
|
|
|
|
return td;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._createContextButton = function(type, section, line) {
|
|
var contextLines = line.contextGroup.lines;
|
|
var context = PARTIAL_CONTEXT_AMOUNT;
|
|
|
|
var button = this._createElement('gr-button', 'showContext');
|
|
button.setAttribute('link', true);
|
|
|
|
var text;
|
|
var groups = []; // The groups that replace this one if tapped.
|
|
|
|
if (type === GrDiffBuilder.ContextButtonType.ALL) {
|
|
text = 'Show ' + contextLines.length + ' common line';
|
|
if (contextLines.length > 1) { text += 's'; }
|
|
groups.push(line.contextGroup);
|
|
} else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
|
|
text = '+' + context + '↑';
|
|
this._insertContextGroups(groups, contextLines,
|
|
[context, contextLines.length]);
|
|
} else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
|
|
text = '+' + context + '↓';
|
|
this._insertContextGroups(groups, contextLines,
|
|
[0, contextLines.length - context]);
|
|
}
|
|
|
|
button.textContent = text;
|
|
|
|
button.addEventListener('tap', function(e) {
|
|
e.detail = {
|
|
groups: groups,
|
|
section: section,
|
|
};
|
|
// Let it bubble up the DOM tree.
|
|
});
|
|
|
|
return button;
|
|
};
|
|
|
|
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 = comments[0].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,
|
|
opt_class) {
|
|
var td = this._createElement('td');
|
|
if (opt_class) {
|
|
td.classList.add(opt_class);
|
|
}
|
|
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 (this._textLength(text, this._prefs.tab_size) >
|
|
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;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._textLength = function(text, tabSize) {
|
|
// TODO(andybons): Unicode support.
|
|
var numChars = 0;
|
|
for (var i = 0; i < text.length; i++) {
|
|
if (text[i] === '\t') {
|
|
numChars += tabSize;
|
|
} else {
|
|
numChars++;
|
|
}
|
|
}
|
|
return numChars;
|
|
};
|
|
|
|
// 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 += '" style="';
|
|
// TODO(andybons): CSS tab-size is not supported in IE.
|
|
str += 'tab-size:' + tabSize + ';';
|
|
str += '-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);
|