
Add a simple annotation layer that marks trailing whitespace in diffs (guarded by the `show_whitespace_errors` diff preference). The newly supported diff preference is added to both diff preference controls. The requirement that all annotation layers must implement `addListener` is relaxed as the trailing whitespace layer is the third layer that doesn't use it. Adds tests for the layer and the diff preference. Feature: Issue 4836 Change-Id: Ifba05216bf0bc3c0a8a094f5ef392b983091d59f
576 lines
18 KiB
JavaScript
576 lines
18 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; }
|
|
|
|
var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/;
|
|
|
|
function GrDiffBuilder(diff, comments, prefs, outputEl, layers) {
|
|
this._diff = diff;
|
|
this._comments = comments;
|
|
this._prefs = prefs;
|
|
this._outputEl = outputEl;
|
|
this.groups = [];
|
|
|
|
this.layers = layers || [];
|
|
|
|
this.layers.forEach(function(layer) {
|
|
if (layer.addListener) {
|
|
layer.addListener(this._handleLayerUpdate.bind(this));
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0);
|
|
GrDiffBuilder.GREATER_THAN_CODE = '>'.charCodeAt(0);
|
|
GrDiffBuilder.AMPERSAND_CODE = '&'.charCodeAt(0);
|
|
GrDiffBuilder.SEMICOLON_CODE = ';'.charCodeAt(0);
|
|
|
|
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;
|
|
|
|
/**
|
|
* Abstract method
|
|
* @param {string} outputEl
|
|
* @param {number} fontSize
|
|
*/
|
|
GrDiffBuilder.prototype.addColumns = function() {
|
|
throw Error('Subclasses must implement addColumns');
|
|
};
|
|
|
|
/**
|
|
* Abstract method
|
|
* @param {Object} group
|
|
*/
|
|
GrDiffBuilder.prototype.buildSectionElement = function() {
|
|
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;
|
|
if (opt_side) {
|
|
groupStartLine = group.lineRange[opt_side].start;
|
|
groupEndLine = group.lineRange[opt_side].end;
|
|
}
|
|
|
|
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.getContentByLine = function(lineNumber, opt_side,
|
|
opt_root) {
|
|
var root = Polymer.dom(opt_root || this._outputEl);
|
|
var sideSelector = !!opt_side ? ('.' + opt_side) : '';
|
|
return root.querySelector('td.lineNum[data-value="' + lineNumber +
|
|
'"]' + sideSelector + ' ~ td.content .contentText');
|
|
};
|
|
|
|
/**
|
|
* Find line elements or line objects by a range of line numbers and a side.
|
|
*
|
|
* @param {Number} start The first line number
|
|
* @param {Number} end The last line number
|
|
* @param {String} opt_side The side of the range. Either 'left' or 'right'.
|
|
* @param {Array<GrDiffLine>} out_lines The output list of line objects. Use
|
|
* null if not desired.
|
|
* @param {Array<HTMLElement>} out_elements The output list of line elements.
|
|
* Use null if not desired.
|
|
*/
|
|
GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side,
|
|
out_lines, out_elements) {
|
|
var groups = this.getGroupsByLineRange(start, end, opt_side);
|
|
groups.forEach(function(group) {
|
|
var content = null;
|
|
group.lines.forEach(function(line) {
|
|
if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) ||
|
|
(opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) {
|
|
return;
|
|
}
|
|
var lineNumber = opt_side === 'left' ?
|
|
line.beforeNumber : line.afterNumber;
|
|
if (lineNumber < start || lineNumber > end) { return; }
|
|
|
|
if (out_lines) { out_lines.push(line); }
|
|
if (out_elements) {
|
|
if (content) {
|
|
content = this._getNextContentOnSide(content, opt_side);
|
|
} else {
|
|
content = this.getContentByLine(lineNumber, opt_side,
|
|
group.element);
|
|
}
|
|
if (content) { out_elements.push(content); }
|
|
}
|
|
}.bind(this));
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Re-renders the DIV.contentText elements for the given side and range of
|
|
* diff content.
|
|
*/
|
|
GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
|
|
var lines = [];
|
|
var elements = [];
|
|
var line;
|
|
var el;
|
|
this.findLinesByRange(start, end, side, lines, elements);
|
|
for (var i = 0; i < lines.length; i++) {
|
|
line = lines[i];
|
|
el = elements[i];
|
|
el.parentElement.replaceChild(this._createTextEl(line, side).firstChild,
|
|
el);
|
|
}
|
|
};
|
|
|
|
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;
|
|
if (opt_side) {
|
|
threadEl.setAttribute('data-side', opt_side);
|
|
}
|
|
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, opt_side) {
|
|
var td = this._createElement('td');
|
|
var text = line.text;
|
|
if (line.type !== GrDiffLine.Type.BLANK) {
|
|
td.classList.add('content');
|
|
if (!text) {
|
|
text = '\xa0';
|
|
}
|
|
}
|
|
td.classList.add(line.type);
|
|
var html = util.escapeHTML(text);
|
|
html = this._addTabWrappers(html, this._prefs.tab_size);
|
|
|
|
if (!this._prefs.line_wrapping &&
|
|
this._textLength(text, this._prefs.tab_size) >
|
|
this._prefs.line_length) {
|
|
html = this._addNewlines(text, html);
|
|
}
|
|
|
|
var contentText = this._createElement('div', 'contentText');
|
|
if (opt_side) {
|
|
contentText.setAttribute('data-side', opt_side);
|
|
}
|
|
|
|
// 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) {
|
|
contentText.textContent = text;
|
|
} else {
|
|
contentText.innerHTML = html;
|
|
}
|
|
|
|
td.classList.add(line.highlights.length > 0 ?
|
|
'lightHighlight' : 'darkHighlight');
|
|
|
|
this.layers.forEach(function(layer) {
|
|
layer.annotate(contentText, line);
|
|
});
|
|
|
|
td.appendChild(contentText);
|
|
|
|
return td;
|
|
};
|
|
|
|
/**
|
|
* Returns the text length after normalizing unicode and tabs.
|
|
* @return {Number} The normalized length of the text.
|
|
*/
|
|
GrDiffBuilder.prototype._textLength = function(text, tabSize) {
|
|
text = text.replace(REGEX_ASTRAL_SYMBOL, '_');
|
|
var numChars = 0;
|
|
for (var i = 0; i < text.length; i++) {
|
|
if (text[i] === '\t') {
|
|
numChars += tabSize - (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;
|
|
};
|
|
|
|
/**
|
|
* Takes a string of text (not HTML) and returns a string of HTML with tab
|
|
* elements in place of tab characters. In each case tab elements are given
|
|
* the width needed to reach the next tab-stop.
|
|
*
|
|
* @param {String} A line of text potentially containing tab characters.
|
|
* @param {Number} The width for tabs.
|
|
* @return {String} An HTML string potentially containing tab elements.
|
|
*/
|
|
GrDiffBuilder.prototype._addTabWrappers = function(line, tabSize) {
|
|
if (!line.length) { return ''; }
|
|
|
|
var result = '';
|
|
var offset = 0;
|
|
var split = line.split('\t');
|
|
var width;
|
|
|
|
for (var i = 0; i < split.length - 1; i++) {
|
|
offset += split[i].length;
|
|
width = tabSize - (offset % tabSize);
|
|
result += split[i] + this._getTabWrapper(width);
|
|
offset += width;
|
|
}
|
|
if (split.length) {
|
|
result += split[split.length - 1];
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
|
|
// 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 ';
|
|
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;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._handleLayerUpdate = function(start, end, side) {
|
|
this._renderContentByRange(start, end, side);
|
|
};
|
|
|
|
/**
|
|
* Finds the next DIV.contentText element following the given element, and on
|
|
* the same side. Will only search within a group.
|
|
* @param {HTMLElement} content
|
|
* @param {String} side Either 'left' or 'right'
|
|
* @return {HTMLElement}
|
|
*/
|
|
GrDiffBuilder.prototype._getNextContentOnSide = function(content, side) {
|
|
throw Error('Subclasses must implement _getNextContentOnSide');
|
|
};
|
|
|
|
window.GrDiffBuilder = GrDiffBuilder;
|
|
})(window, GrDiffGroup, GrDiffLine);
|