
Line numbers in diffs had been specified in data-attributes and CSS to avoid including them in pasteboard selections. However, we have since moved to a different system that avoids including non diff content in selections, and this CSS needlessly complicates style application. With this change, diff line numbers show content, and the relevant CSS is simplified. This provides a small, but measurable diff render performance improvement. Change-Id: Iad062553be533ead1dd29eaaacd5af8867249a16
678 lines
22 KiB
JavaScript
678 lines
22 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; }
|
|
|
|
/**
|
|
* 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);
|
|
|
|
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]);
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum,
|
|
patchNum, path, isOnParent, range) {
|
|
const threadGroupEl =
|
|
document.createElement('gr-diff-comment-thread-group');
|
|
threadGroupEl.changeNum = changeNum;
|
|
threadGroupEl.patchForNewThreads = patchNum;
|
|
threadGroupEl.path = path;
|
|
threadGroupEl.isOnParent = isOnParent;
|
|
threadGroupEl.projectName = this._projectName;
|
|
threadGroupEl.range = range;
|
|
threadGroupEl.parentIndex = this._parentIndex;
|
|
return threadGroupEl;
|
|
};
|
|
|
|
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);
|
|
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, className) {
|
|
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 (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');
|
|
};
|
|
|
|
/**
|
|
* 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);
|