diff --git a/swh/web/assets/src/bundles/revision/diff-utils.js b/swh/web/assets/src/bundles/revision/diff-utils.js
index f5f6ba13..3aeb9a8b 100644
--- a/swh/web/assets/src/bundles/revision/diff-utils.js
+++ b/swh/web/assets/src/bundles/revision/diff-utils.js
@@ -1,546 +1,546 @@
/**
- * Copyright (C) 2018-2019 The Software Heritage developers
+ * Copyright (C) 2018-2020 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
import 'waypoints/lib/jquery.waypoints';
import {swhSpinnerSrc} from 'utils/constants';
// number of changed files in the revision
let changes = null;
let nbChangedFiles = 0;
// to track the number of already computed files diffs
let nbDiffsComputed = 0;
// the no newline at end of file marker from Github
let noNewLineMarker = '' +
'' +
'';
// to track the total number of added lines in files diffs
let nbAdditions = 0;
// to track the total number of deleted lines in files diffs
let nbDeletions = 0;
// to track the already computed diffs by id
let computedDiffs = {};
// map a diff id to its computation url
let diffsUrls = {};
// to check if a DOM element is in the viewport
function isInViewport(elt) {
let elementTop = $(elt).offset().top;
let elementBottom = elementTop + $(elt).outerHeight();
let viewportTop = $(window).scrollTop();
let viewportBottom = viewportTop + $(window).height();
return elementBottom > viewportTop && elementTop < viewportBottom;
}
// to format the diffs line numbers
function formatDiffLineNumbers(fromLine, toLine, maxNumberChars) {
let ret = '';
if (fromLine != null) {
for (let i = 0; i < (maxNumberChars - fromLine.length); ++i) {
ret += ' ';
}
ret += fromLine;
}
if (fromLine != null && toLine != null) {
ret += ' ';
}
if (toLine != null) {
for (let i = 0; i < (maxNumberChars - toLine.length); ++i) {
ret += ' ';
}
ret += toLine;
}
return ret;
}
function parseDiffHunkRangeIfAny(lineText) {
let baseFromLine, baseToLine;
if (lineText.startsWith('@@')) {
let linesInfoRegExp = new RegExp(/^@@ -(\d+),(\d+) \+(\d+),(\d+) @@$/gm);
let linesInfoRegExp2 = new RegExp(/^@@ -(\d+) \+(\d+),(\d+) @@$/gm);
let linesInfoRegExp3 = new RegExp(/^@@ -(\d+),(\d+) \+(\d+) @@$/gm);
let linesInfoRegExp4 = new RegExp(/^@@ -(\d+) \+(\d+) @@$/gm);
let linesInfo = linesInfoRegExp.exec(lineText);
let linesInfo2 = linesInfoRegExp2.exec(lineText);
let linesInfo3 = linesInfoRegExp3.exec(lineText);
let linesInfo4 = linesInfoRegExp4.exec(lineText);
if (linesInfo) {
baseFromLine = parseInt(linesInfo[1]) - 1;
baseToLine = parseInt(linesInfo[3]) - 1;
} else if (linesInfo2) {
baseFromLine = parseInt(linesInfo2[1]) - 1;
baseToLine = parseInt(linesInfo2[2]) - 1;
} else if (linesInfo3) {
baseFromLine = parseInt(linesInfo3[1]) - 1;
baseToLine = parseInt(linesInfo3[3]) - 1;
} else if (linesInfo4) {
baseFromLine = parseInt(linesInfo4[1]) - 1;
baseToLine = parseInt(linesInfo4[2]) - 1;
}
}
if (baseFromLine !== undefined) {
return [baseFromLine, baseToLine];
} else {
return null;
}
}
// to compute diff and process it for display
export function computeDiff(diffUrl, diffId) {
// force diff computation ?
let force = diffUrl.indexOf('force=true') !== -1;
// it no forced computation and diff already computed, do nothing
if (!force && computedDiffs.hasOwnProperty(diffId)) {
return;
}
// mark diff computation as already requested
computedDiffs[diffId] = true;
$(`#${diffId}-loading`).css('visibility', 'visible');
// set spinner visible while requesting diff
$(`#${diffId}-loading`).css('display', 'block');
$(`#${diffId}-highlightjs`).css('display', 'none');
// request diff computation and process it
fetch(diffUrl)
.then(response => response.json())
.then(data => {
// increment number of computed diffs
++nbDiffsComputed;
// toggle the 'Compute all diffs' button if all diffs have been computed
if (nbDiffsComputed === changes.length) {
$('#swh-compute-all-diffs').addClass('active');
}
// Large diff (> threshold) are not automatically computed,
// add a button to force its computation
if (data.diff_str.indexOf('Large diff') === 0) {
$(`#${diffId}`)[0].innerHTML = data.diff_str +
`
';
setDiffVisible(diffId);
} else if (data.diff_str.indexOf('@@') !== 0) {
$(`#${diffId}`).text(data.diff_str);
setDiffVisible(diffId);
} else {
// prepare code highlighting
$(`.${diffId}`).removeClass('nohighlight');
$(`.${diffId}`).addClass(data.language);
// set unified diff text
$(`#${diffId}`).text(data.diff_str);
// code highlighting for unified diff
$(`#${diffId}`).each((i, block) => {
hljs.highlightBlock(block);
hljs.lineNumbersBlock(block);
});
// hljs.lineNumbersBlock is asynchronous so we have to postpone our
// next treatments by adding it at the end of the current js events queue
setTimeout(() => {
// process unified diff lines in order to generate side-by-side diffs text
// but also compute line numbers for unified and side-by-side diffs
let baseFromLine = '';
let baseToLine = '';
let fromToLines = [];
let fromLines = [];
let toLines = [];
let maxNumberChars = 0;
let diffFromStr = '';
let diffToStr = '';
let linesOffset = 0;
$(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
let lnText = lnElt.nextSibling.innerText;
let linesInfo = parseDiffHunkRangeIfAny(lnText);
let fromLine = '';
let toLine = '';
// parsed lines info from the diff output
if (linesInfo) {
baseFromLine = linesInfo[0];
baseToLine = linesInfo[1];
linesOffset = 0;
diffFromStr += (lnText + '\n');
diffToStr += (lnText + '\n');
fromLines.push('');
toLines.push('');
// line removed in the from file
} else if (lnText.length > 0 && lnText[0] === '-') {
baseFromLine = baseFromLine + 1;
fromLine = baseFromLine.toString();
fromLines.push(fromLine);
++nbDeletions;
diffFromStr += (lnText + '\n');
++linesOffset;
// line added in the to file
} else if (lnText.length > 0 && lnText[0] === '+') {
baseToLine = baseToLine + 1;
toLine = baseToLine.toString();
toLines.push(toLine);
++nbAdditions;
diffToStr += (lnText + '\n');
--linesOffset;
// line present in both files
} else {
baseFromLine = baseFromLine + 1;
baseToLine = baseToLine + 1;
fromLine = baseFromLine.toString();
toLine = baseToLine.toString();
for (let j = 0; j < Math.abs(linesOffset); ++j) {
if (linesOffset > 0) {
diffToStr += '\n';
toLines.push('');
} else {
diffFromStr += '\n';
fromLines.push('');
}
}
linesOffset = 0;
diffFromStr += (lnText + '\n');
diffToStr += (lnText + '\n');
toLines.push(toLine);
fromLines.push(fromLine);
}
if (!baseFromLine) {
fromLine = '';
}
if (!baseToLine) {
toLine = '';
}
fromToLines[i] = [fromLine, toLine];
maxNumberChars = Math.max(maxNumberChars, fromLine.length);
maxNumberChars = Math.max(maxNumberChars, toLine.length);
});
// set side-by-side diffs text
$(`#${diffId}-from`).text(diffFromStr);
$(`#${diffId}-to`).text(diffToStr);
// code highlighting for side-by-side diffs
$(`#${diffId}-from, #${diffId}-to`).each((i, block) => {
hljs.highlightBlock(block);
hljs.lineNumbersBlock(block);
});
// hljs.lineNumbersBlock is asynchronous so we have to postpone our
// next treatments by adding it at the end of the current js events queue
setTimeout(() => {
// diff highlighting for added/removed lines on top of code highlighting
$(`.${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
let lnText = lnElt.nextSibling.innerText;
if (lnText.startsWith('@@')) {
$(lnElt).parent().addClass('swh-diff-lines-info');
let linesInfoText = $(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text();
$(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').children().remove();
$(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').text('');
$(lnElt).parent().find('.hljs-ln-code .hljs-ln-line').append(` `);
} else if (lnText.length > 0 && lnText[0] === '-') {
$(lnElt).parent().addClass('swh-diff-removed-line');
} else if (lnText.length > 0 && lnText[0] === '+') {
$(lnElt).parent().addClass('swh-diff-added-line');
}
});
// set line numbers for unified diff
$(`#${diffId} .hljs-ln-numbers`).each((i, lnElt) => {
$(lnElt).children().attr(
'data-line-number',
formatDiffLineNumbers(fromToLines[i][0], fromToLines[i][1],
maxNumberChars));
});
// set line numbers for the from side-by-side diff
$(`#${diffId}-from .hljs-ln-numbers`).each((i, lnElt) => {
$(lnElt).children().attr(
'data-line-number',
formatDiffLineNumbers(fromLines[i], null,
maxNumberChars));
});
// set line numbers for the to side-by-side diff
$(`#${diffId}-to .hljs-ln-numbers`).each((i, lnElt) => {
$(lnElt).children().attr(
'data-line-number',
formatDiffLineNumbers(null, toLines[i],
maxNumberChars));
});
// last processing:
// - remove the '+' and '-' at the beginning of the diff lines
// from code highlighting
// - add the "no new line at end of file marker" if needed
$(`.${diffId} .hljs-ln-code`).each((i, lnElt) => {
if (lnElt.firstChild) {
if (lnElt.firstChild.nodeName !== '#text') {
let lineText = lnElt.firstChild.innerHTML;
if (lineText[0] === '-' || lineText[0] === '+') {
lnElt.firstChild.innerHTML = lineText.substr(1);
let newTextNode = document.createTextNode(lineText[0]);
$(lnElt).prepend(newTextNode);
}
}
$(lnElt).contents().filter((i, elt) => {
return elt.nodeType === 3; // Node.TEXT_NODE
}).each((i, textNode) => {
let swhNoNewLineMarker = '[swh-no-nl-marker]';
if (textNode.textContent.indexOf(swhNoNewLineMarker) !== -1) {
textNode.textContent = textNode.textContent.replace(swhNoNewLineMarker, '');
$(lnElt).append($(noNewLineMarker));
}
});
}
});
// hide the diff mode switch button in case of not generated diffs
if (data.diff_str.indexOf('Diffs are not generated for non textual content') !== 0) {
$(`#panel_${diffId} .diff-styles`).css('visibility', 'visible');
}
setDiffVisible(diffId);
});
});
}
});
}
function setDiffVisible(diffId) {
// set the unified diff visible by default
$(`#${diffId}-loading`).css('display', 'none');
$(`#${diffId}-highlightjs`).css('display', 'block');
// update displayed counters
$('#swh-revision-lines-added').text(`${nbAdditions} additions`);
$('#swh-revision-lines-deleted').text(`${nbDeletions} deletions`);
$('#swh-nb-diffs-computed').text(nbDiffsComputed);
// refresh the waypoints triggering diffs computation as
// the DOM layout has been updated
Waypoint.refreshAll();
}
// to compute all visible diffs in the viewport
function computeVisibleDiffs() {
$('.swh-file-diff-panel').each((i, elt) => {
if (isInViewport(elt)) {
let diffId = elt.id.replace('panel_', '');
computeDiff(diffsUrls[diffId], diffId);
}
});
}
function genDiffPanel(diffData) {
let diffPanelTitle = diffData.path;
if (diffData.type === 'rename') {
diffPanelTitle = `${diffData.from_path} → ${diffData.to_path}`;
}
let diffPanelHtml =
`