
As per UX instructions, adds a border between the images and replaces the image outline with a box shadow. Change-Id: Iec157ce7e346cab217a49d6fcee5977c1702478c
697 lines
23 KiB
JavaScript
697 lines
23 KiB
JavaScript
/**
|
|
* @license
|
|
* 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; }
|
|
|
|
/**
|
|
* In JS, unicode code points above 0xFFFF occupy two elements of a string.
|
|
* For example '𐀏'.length is 2. An occurence of such a code point is called a
|
|
* surrogate pair.
|
|
*
|
|
* This regex segments a string along tabs ('\t') and surrogate pairs, since
|
|
* these are two cases where '1 char' does not automatically imply '1 column'.
|
|
*
|
|
* TODO: For human languages whose orthographies use combining marks, this
|
|
* approach won't correctly identify the grapheme boundaries. In those cases,
|
|
* a grapheme consists of multiple code points that should count as only one
|
|
* character against the column limit. Getting that correct (if it's desired)
|
|
* is probably beyond the limits of a regex, but there are nonstandard APIs to
|
|
* do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs.
|
|
*
|
|
* Further reading:
|
|
* On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode
|
|
* Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
|
|
* A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
|
|
*/
|
|
const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
|
|
|
|
function GrDiffBuilder(diff, comments, prefs, projectName, outputEl, layers) {
|
|
this._diff = diff;
|
|
this._comments = comments;
|
|
this._prefs = prefs;
|
|
this._projectName = projectName;
|
|
this._outputEl = outputEl;
|
|
this.groups = [];
|
|
this._blameInfo = null;
|
|
this._parentIndex = undefined;
|
|
|
|
this.layers = layers || [];
|
|
|
|
if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) {
|
|
throw Error('Invalid tab size from preferences.');
|
|
}
|
|
|
|
if (isNaN(prefs.line_length) || prefs.line_length <= 0) {
|
|
throw Error('Invalid line length from preferences.');
|
|
}
|
|
|
|
|
|
for (const layer of this.layers) {
|
|
if (layer.addListener) {
|
|
layer.addListener(this._handleLayerUpdate.bind(this));
|
|
}
|
|
}
|
|
}
|
|
|
|
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',
|
|
};
|
|
|
|
const 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 buildSectionElement');
|
|
};
|
|
|
|
GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) {
|
|
const element = this.buildSectionElement(group);
|
|
this._outputEl.insertBefore(element, opt_beforeSection);
|
|
group.element = element;
|
|
};
|
|
|
|
GrDiffBuilder.prototype.renderSection = function(element) {
|
|
for (let i = 0; i < this.groups.length; i++) {
|
|
const group = this.groups[i];
|
|
if (group.element === element) {
|
|
const newElement = this.buildSectionElement(group);
|
|
group.element.parentElement.replaceChild(newElement, group.element);
|
|
group.element = newElement;
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
GrDiffBuilder.prototype.getGroupsByLineRange = function(
|
|
startLine, endLine, opt_side) {
|
|
const groups = [];
|
|
for (let i = 0; i < this.groups.length; i++) {
|
|
const group = this.groups[i];
|
|
if (group.lines.length === 0) {
|
|
continue;
|
|
}
|
|
let groupStartLine = 0;
|
|
let 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) {
|
|
const root = Polymer.dom(opt_root || this._outputEl);
|
|
const 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) {
|
|
const groups = this.getGroupsByLineRange(start, end, opt_side);
|
|
for (const group of groups) {
|
|
let content = null;
|
|
for (const line of group.lines) {
|
|
if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) ||
|
|
(opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) {
|
|
continue;
|
|
}
|
|
const lineNumber = opt_side === 'left' ?
|
|
line.beforeNumber : line.afterNumber;
|
|
if (lineNumber < start || lineNumber > end) { continue; }
|
|
|
|
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); }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Re-renders the DIV.contentText elements for the given side and range of
|
|
* diff content.
|
|
*/
|
|
GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) {
|
|
const lines = [];
|
|
const elements = [];
|
|
let line;
|
|
let el;
|
|
this.findLinesByRange(start, end, side, lines, elements);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
line = lines[i];
|
|
el = elements[i];
|
|
if (!el) {
|
|
// Cannot re-render an element if it does not exist. This can happen
|
|
// if lines are collapsed and not visible on the page yet.
|
|
continue;
|
|
}
|
|
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(
|
|
group => { return group.element; });
|
|
};
|
|
|
|
// TODO(wyatta): Move this completely into the processor.
|
|
GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
|
|
hiddenRange) {
|
|
const linesBeforeCtx = lines.slice(0, hiddenRange[0]);
|
|
const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
|
|
const linesAfterCtx = lines.slice(hiddenRange[1]);
|
|
|
|
if (linesBeforeCtx.length > 0) {
|
|
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
|
|
}
|
|
|
|
const 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;
|
|
}
|
|
|
|
const td = this._createElement('td');
|
|
const 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) {
|
|
const contextLines = line.contextGroup.lines;
|
|
const context = PARTIAL_CONTEXT_AMOUNT;
|
|
|
|
const button = this._createElement('gr-button', 'showContext');
|
|
button.setAttribute('link', true);
|
|
button.setAttribute('no-uppercase', true);
|
|
|
|
let text;
|
|
const 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]);
|
|
}
|
|
|
|
Polymer.dom(button).textContent = text;
|
|
|
|
button.addEventListener('tap', e => {
|
|
e.detail = {
|
|
groups,
|
|
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);
|
|
};
|
|
}
|
|
const leftComments =
|
|
comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber));
|
|
const rightComments =
|
|
comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber));
|
|
|
|
leftComments.forEach(c => { c.__commentSide = 'left'; });
|
|
rightComments.forEach(c => { c.__commentSide = 'right'; });
|
|
|
|
let 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;
|
|
};
|
|
|
|
/**
|
|
* @param {number} changeNum
|
|
* @param {number|string} patchNum
|
|
* @param {string} path
|
|
* @param {boolean} isOnParent
|
|
* @param {string} commentSide
|
|
* @return {!Object}
|
|
*/
|
|
GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum,
|
|
patchNum, path, isOnParent, commentSide) {
|
|
const threadGroupEl =
|
|
document.createElement('gr-diff-comment-thread-group');
|
|
threadGroupEl.changeNum = changeNum;
|
|
threadGroupEl.commentSide = commentSide;
|
|
threadGroupEl.patchForNewThreads = patchNum;
|
|
threadGroupEl.path = path;
|
|
threadGroupEl.isOnParent = isOnParent;
|
|
threadGroupEl.projectName = this._projectName;
|
|
threadGroupEl.parentIndex = this._parentIndex;
|
|
return threadGroupEl;
|
|
};
|
|
|
|
/**
|
|
* @param {number} line
|
|
* @param {string=} opt_side
|
|
* @return {!Object}
|
|
*/
|
|
GrDiffBuilder.prototype._commentThreadGroupForLine = function(
|
|
line, opt_side) {
|
|
const comments =
|
|
this._getCommentsForLine(this._comments, line, opt_side);
|
|
if (!comments || comments.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let patchNum = this._comments.meta.patchRange.patchNum;
|
|
let isOnParent = comments[0].side === 'PARENT' || false;
|
|
if (line.type === GrDiffLine.Type.REMOVE ||
|
|
opt_side === GrDiffBuilder.Side.LEFT) {
|
|
if (this._comments.meta.patchRange.basePatchNum === 'PARENT' ||
|
|
Gerrit.PatchSetBehavior.isMergeParent(
|
|
this._comments.meta.patchRange.basePatchNum)) {
|
|
isOnParent = true;
|
|
} else {
|
|
patchNum = this._comments.meta.patchRange.basePatchNum;
|
|
}
|
|
}
|
|
const threadGroupEl = this.createCommentThreadGroup(
|
|
this._comments.meta.changeNum, patchNum, this._comments.meta.path,
|
|
isOnParent, opt_side);
|
|
threadGroupEl.comments = comments;
|
|
if (opt_side) {
|
|
threadGroupEl.setAttribute('data-side', opt_side);
|
|
}
|
|
return threadGroupEl;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._createLineEl = function(
|
|
line, number, type, opt_class) {
|
|
const td = this._createElement('td');
|
|
if (opt_class) {
|
|
td.classList.add(opt_class);
|
|
}
|
|
|
|
if (line.type === GrDiffLine.Type.REMOVE) {
|
|
td.setAttribute('aria-label', `${number} removed`);
|
|
} else if (line.type === GrDiffLine.Type.ADD) {
|
|
td.setAttribute('aria-label', `${number} added`);
|
|
}
|
|
|
|
if (line.type === GrDiffLine.Type.BLANK) {
|
|
return td;
|
|
} else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
|
|
td.classList.add('contextLineNum');
|
|
td.setAttribute('data-value', '@@');
|
|
td.textContent = '@@';
|
|
} else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
|
|
td.classList.add('lineNum');
|
|
td.setAttribute('data-value', number);
|
|
td.textContent = number === 'FILE' ? 'File' : number;
|
|
}
|
|
return td;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._createTextEl = function(line, opt_side) {
|
|
const td = this._createElement('td');
|
|
if (line.type !== GrDiffLine.Type.BLANK) {
|
|
td.classList.add('content');
|
|
}
|
|
td.classList.add(line.type);
|
|
|
|
const lineLimit =
|
|
!this._prefs.line_wrapping ? this._prefs.line_length : Infinity;
|
|
|
|
const contentText =
|
|
this._formatText(line.text, this._prefs.tab_size, lineLimit);
|
|
if (opt_side) {
|
|
contentText.setAttribute('data-side', opt_side);
|
|
}
|
|
|
|
for (const layer of this.layers) {
|
|
layer.annotate(contentText, line);
|
|
}
|
|
|
|
td.appendChild(contentText);
|
|
|
|
return td;
|
|
};
|
|
|
|
/**
|
|
* Returns a 'div' element containing the supplied |text| as its innerText,
|
|
* with '\t' characters expanded to a width determined by |tabSize|, and the
|
|
* text wrapped at column |lineLimit|, which may be Infinity if no wrapping is
|
|
* desired.
|
|
*
|
|
* @param {string} text The text to be formatted.
|
|
* @param {number} tabSize The width of each tab stop.
|
|
* @param {number} lineLimit The column after which to wrap lines.
|
|
* @return {HTMLElement}
|
|
*/
|
|
GrDiffBuilder.prototype._formatText = function(text, tabSize, lineLimit) {
|
|
const contentText = this._createElement('div', 'contentText');
|
|
|
|
let columnPos = 0;
|
|
let textOffset = 0;
|
|
for (const segment of text.split(REGEX_TAB_OR_SURROGATE_PAIR)) {
|
|
if (segment) {
|
|
// |segment| contains only normal characters. If |segment| doesn't fit
|
|
// entirely on the current line, append chunks of |segment| followed by
|
|
// line breaks.
|
|
let rowStart = 0;
|
|
let rowEnd = lineLimit - columnPos;
|
|
while (rowEnd < segment.length) {
|
|
contentText.appendChild(
|
|
document.createTextNode(segment.substring(rowStart, rowEnd)));
|
|
contentText.appendChild(this._createElement('span', 'br'));
|
|
columnPos = 0;
|
|
rowStart = rowEnd;
|
|
rowEnd += lineLimit;
|
|
}
|
|
// Append the last part of |segment|, which fits on the current line.
|
|
contentText.appendChild(
|
|
document.createTextNode(segment.substring(rowStart)));
|
|
columnPos += (segment.length - rowStart);
|
|
textOffset += segment.length;
|
|
}
|
|
if (textOffset < text.length) {
|
|
// Handle the special character at |textOffset|.
|
|
if (text.startsWith('\t', textOffset)) {
|
|
// Append a single '\t' character.
|
|
let effectiveTabSize = tabSize - (columnPos % tabSize);
|
|
if (columnPos + effectiveTabSize > lineLimit) {
|
|
contentText.appendChild(this._createElement('span', 'br'));
|
|
columnPos = 0;
|
|
effectiveTabSize = tabSize;
|
|
}
|
|
contentText.appendChild(this._getTabWrapper(effectiveTabSize));
|
|
columnPos += effectiveTabSize;
|
|
textOffset++;
|
|
} else {
|
|
// Append a single surrogate pair.
|
|
if (columnPos >= lineLimit) {
|
|
contentText.appendChild(this._createElement('span', 'br'));
|
|
columnPos = 0;
|
|
}
|
|
contentText.appendChild(document.createTextNode(
|
|
text.substring(textOffset, textOffset + 2)));
|
|
textOffset += 2;
|
|
columnPos += 1;
|
|
}
|
|
}
|
|
}
|
|
return contentText;
|
|
};
|
|
|
|
/**
|
|
* Returns a <span> element holding a '\t' character, that will visually
|
|
* occupy |tabSize| many columns.
|
|
*
|
|
* @param {number} tabSize The effective size of this tab stop.
|
|
* @return {HTMLElement}
|
|
*/
|
|
GrDiffBuilder.prototype._getTabWrapper = function(tabSize) {
|
|
// Force this to be a number to prevent arbitrary injection.
|
|
const result = this._createElement('span', 'tab');
|
|
result.style['tab-size'] = tabSize;
|
|
result.style['-moz-tab-size'] = tabSize;
|
|
result.innerText = '\t';
|
|
return result;
|
|
};
|
|
|
|
GrDiffBuilder.prototype._createElement = function(tagName, classStr) {
|
|
const 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 (classStr) {
|
|
for (const className of classStr.split(' ')) {
|
|
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');
|
|
};
|
|
|
|
/**
|
|
* Determines whether the given group is either totally an addition or totally
|
|
* a removal.
|
|
* @param {!Object} group (GrDiffGroup)
|
|
* @return {boolean}
|
|
*/
|
|
GrDiffBuilder.prototype._isTotal = function(group) {
|
|
return group.type === GrDiffGroup.Type.DELTA &&
|
|
(!group.adds.length || !group.removes.length) &&
|
|
!(!group.adds.length && !group.removes.length);
|
|
};
|
|
|
|
/**
|
|
* Set the blame information for the diff. For any already-rendered line,
|
|
* re-render its blame cell content.
|
|
* @param {Object} blame
|
|
*/
|
|
GrDiffBuilder.prototype.setBlame = function(blame) {
|
|
this._blameInfo = blame;
|
|
|
|
// TODO(wyatta): make this loop asynchronous.
|
|
for (const commit of blame) {
|
|
for (const range of commit.ranges) {
|
|
for (let i = range.start; i <= range.end; i++) {
|
|
// TODO(wyatta): this query is expensive, but, when traversing a
|
|
// range, the lines are consecutive, and given the previous blame
|
|
// cell, the next one can be reached cheaply.
|
|
const el = this._getBlameByLineNum(i);
|
|
if (!el) { continue; }
|
|
// Remove the element's children (if any).
|
|
while (el.hasChildNodes()) {
|
|
el.removeChild(el.lastChild);
|
|
}
|
|
const blame = this._getBlameForBaseLine(i, commit);
|
|
el.appendChild(blame);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
GrDiffBuilder.prototype.setParentIndex = function(index) {
|
|
this._parentIndex = index;
|
|
};
|
|
|
|
/**
|
|
* Find the blame cell for a given line number.
|
|
* @param {number} lineNum
|
|
* @return {HTMLTableDataCellElement}
|
|
*/
|
|
GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) {
|
|
const root = Polymer.dom(this._outputEl);
|
|
return root.querySelector(`td.blame[data-line-number="${lineNum}"]`);
|
|
};
|
|
|
|
/**
|
|
* Given a base line number, return the commit containing that line in the
|
|
* current set of blame information. If no blame information has been
|
|
* provided, null is returned.
|
|
* @param {number} lineNum
|
|
* @return {Object} The commit information.
|
|
*/
|
|
GrDiffBuilder.prototype._getBlameCommitForBaseLine = function(lineNum) {
|
|
if (!this._blameInfo) { return null; }
|
|
|
|
for (const blameCommit of this._blameInfo) {
|
|
for (const range of blameCommit.ranges) {
|
|
if (range.start <= lineNum && range.end >= lineNum) {
|
|
return blameCommit;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Given the number of a base line, get the content for the blame cell of that
|
|
* line. If there is no blame information for that line, returns null.
|
|
* @param {number} lineNum
|
|
* @param {Object=} opt_commit Optionally provide the commit object, so that
|
|
* it does not need to be searched.
|
|
* @return {HTMLSpanElement}
|
|
*/
|
|
GrDiffBuilder.prototype._getBlameForBaseLine = function(lineNum, opt_commit) {
|
|
const commit = opt_commit || this._getBlameCommitForBaseLine(lineNum);
|
|
if (!commit) { return null; }
|
|
|
|
const isStartOfRange = commit.ranges.some(r => r.start === lineNum);
|
|
|
|
const date = (new Date(commit.time * 1000)).toLocaleDateString();
|
|
const blameNode = this._createElement('span',
|
|
isStartOfRange ? 'startOfRange' : '');
|
|
const shaNode = this._createElement('span', 'sha');
|
|
shaNode.innerText = commit.id.substr(0, 7);
|
|
blameNode.appendChild(shaNode);
|
|
blameNode.append(` on ${date} by ${commit.author}`);
|
|
return blameNode;
|
|
};
|
|
|
|
/**
|
|
* Create a blame cell for the given base line. Blame information will be
|
|
* included in the cell if available.
|
|
* @param {GrDiffLine} line
|
|
* @return {HTMLTableDataCellElement}
|
|
*/
|
|
GrDiffBuilder.prototype._createBlameCell = function(line) {
|
|
const blameTd = this._createElement('td', 'blame');
|
|
blameTd.setAttribute('data-line-number', line.beforeNumber);
|
|
if (line.beforeNumber) {
|
|
const content = this._getBlameForBaseLine(line.beforeNumber);
|
|
if (content) {
|
|
blameTd.appendChild(content);
|
|
}
|
|
}
|
|
return blameTd;
|
|
};
|
|
|
|
window.GrDiffBuilder = GrDiffBuilder;
|
|
})(window, GrDiffGroup, GrDiffLine);
|