diff --git a/cypress/integration/revision-diff.spec.js b/cypress/integration/revision-diff.spec.js
index 3fe38c79..12f45f0c 100644
--- a/cypress/integration/revision-diff.spec.js
+++ b/cypress/integration/revision-diff.spec.js
@@ -1,467 +1,492 @@
/**
* Copyright (C) 2019-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
*/
const $ = Cypress.$;
const origin = 'https://github.com/memononen/libtess2';
const revision = '98c65dad5e47ad888032b6cdf556f192e0e028d0';
const diffsHighlightingData = {
'unified': {
diffId: '3d4c0797cf0e89430410e088339aac384dfa4d82',
startLines: [913, 915],
endLines: [0, 979]
},
'split-from': {
diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
startLines: [192, 0],
endLines: [198, 0]
},
'split-to': {
diffId: '602cec77c3d3f41d396d9e1083a0bbce0796b505',
startLines: [0, 120],
endLines: [0, 130]
},
'split-from-top-to-bottom': {
diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
startLines: [63, 0],
endLines: [0, 68]
},
'split-to-top-from-bottom': {
diffId: 'a00c33990655a93aa2c821c4008bbddda812a896',
startLines: [0, 63],
endLines: [67, 0]
}
};
let diffData;
let swh;
describe('Test Revision View', function() {
it('should add/remove #swh-revision-changes url fragment when switching tab', function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url);
cy.get('a[data-toggle="tab"]')
.contains('Changes')
.click();
cy.hash().should('be.equal', '#swh-revision-changes');
cy.get('a[data-toggle="tab"]')
.contains('Files')
.click();
cy.hash().should('be.equal', '');
});
it('should display Changes tab by default when url ends with #swh-revision-changes', function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url + '#swh-revision-changes');
cy.get('#swh-revision-changes-list')
.should('be.visible');
});
});
describe('Test Diffs View', function() {
beforeEach(function() {
const url = this.Urls.browse_revision(revision) + `?origin=${origin}`;
cy.visit(url);
cy.window().then(win => {
swh = win.swh;
cy.request(win.diffRevUrl)
.then(res => {
diffData = res.body;
});
});
cy.get('a[data-toggle="tab"]')
.contains('Changes')
.click();
});
it('should list all files with changes', function() {
let files = new Set([]);
for (let change of diffData.changes) {
files.add(change.from_path);
files.add(change.to_path);
}
for (let file of files) {
cy.get('#swh-revision-changes-list a')
.contains(file)
.should('be.visible');
}
});
it('should load diffs when scrolled down', function() {
cy.get('#swh-revision-changes-list a')
.each($el => {
cy.get($el.attr('href'))
.scrollIntoView()
.find('.swh-content')
.should('be.visible');
});
});
it('should compute all diffs when selected', function() {
cy.get('#swh-compute-all-diffs')
.click();
cy.get('#swh-revision-changes-list a')
.each($el => {
cy.get($el.attr('href'))
.find('.swh-content')
.should('be.visible');
});
});
it('should have correct links in diff file names', function() {
for (let change of diffData.changes) {
cy.get(`#swh-revision-changes-list a[href="#diff_${change.id}"`)
.should('be.visible');
}
});
it('should load unified diff by default', function() {
cy.get('#swh-compute-all-diffs')
.click();
for (let change of diffData.changes) {
cy.get(`#${change.id}-unified-diff`)
.should('be.visible');
cy.get(`#${change.id}-split-diff`)
.should('not.be.visible');
}
});
it('should switch between unified and side-by-side diff when selected', function() {
// Test for first diff
const id = diffData.changes[0].id;
cy.get(`#diff_${id}`)
.contains('label', 'Side-by-side')
.click();
cy.get(`#${id}-split-diff`)
.should('be.visible')
.get(`#${id}-unified-diff`)
.should('not.be.visible');
});
function checkDiffHighlighted(diffId, start, end) {
cy.get(`#${diffId} .hljs-ln-line`)
.then(lines => {
let inHighlightedRange = false;
for (let line of lines) {
const lnNumber = $(line).data('line-number');
if (lnNumber === start || lnNumber === end) {
inHighlightedRange = true;
}
+ const backgroundColor = $(line).css('background-color');
const mixBlendMode = $(line).css('mix-blend-mode');
if (inHighlightedRange && parseInt(lnNumber)) {
assert.equal(mixBlendMode, 'multiply');
+ assert.notEqual(backgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(mixBlendMode, 'normal');
+ assert.equal(backgroundColor, 'rgba(0, 0, 0, 0)');
}
if (lnNumber === end) {
inHighlightedRange = false;
}
}
});
}
function unifiedDiffHighlightingTest(diffId, startLines, endLines) {
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.get(`#${diffId}-unified-diff`)
.should('be.visible')
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
let startLinesStr = swh.revision.formatDiffLineNumbers(diffId, startLines[0], startLines[1]);
let endLinesStr = swh.revision.formatDiffLineNumbers(diffId, endLines[0], endLines[1]);
// highlight a range of lines
let startElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${startLinesStr}"]`;
let endElt = `#${diffId}-unified-diff .hljs-ln-numbers[data-line-number="${endLinesStr}"]`;
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
// check URL fragment has been updated
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, true);
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
if ($(endElt).position().top < $(startElt).position().top) {
[startLinesStr, endLinesStr] = [endLinesStr, startLinesStr];
}
// check lines range is highlighted
checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-unified-diff`)
.should('be.visible');
checkDiffHighlighted(`${diffId}-unified-diff`, startLinesStr, endLinesStr);
});
}
it('should highlight unified diff lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['unified'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
unifiedDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight unified diff lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['unified'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
unifiedDiffHighlightingTest(diffId, endLines, startLines);
});
function singleSpitDiffHighlightingTest(diffId, startLines, endLines, to) {
let singleDiffId = `${diffId}-from`;
if (to) {
singleDiffId = `${diffId}-to`;
}
let startLine = startLines[0] || startLines[1];
let endLine = endLines[0] || endLines[1];
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.get(`#${diffId}-unified-diff`)
.should('be.visible');
cy.get(`#diff_${diffId}`)
.contains('label', 'Side-by-side')
.click()
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
// highlight a range of lines
let startElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${startLine}"]`;
let endElt = `#${singleDiffId} .hljs-ln-numbers[data-line-number="${endLine}"]`;
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
// check URL fragment has been updated
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
if ($(endElt).position().top < $(startElt).position().top) {
[startLine, endLine] = [endLine, startLine];
}
// check lines range is highlighted
checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-split-diff`)
.get(`#${singleDiffId}`)
.should('be.visible');
checkDiffHighlighted(`${singleDiffId}`, startLine, endLine);
});
}
it('should highlight split diff from lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['split-from'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, startLines, endLines, false);
});
it('should highlight split diff from lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['split-from'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, endLines, startLines, false);
});
it('should highlight split diff to lines when selecting them from top to bottom', function() {
const diffHighlightingData = diffsHighlightingData['split-to'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, startLines, endLines, true);
});
it('should highlight split diff to lines when selecting them from bottom to top', function() {
const diffHighlightingData = diffsHighlightingData['split-to'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
singleSpitDiffHighlightingTest(diffId, endLines, startLines, true);
});
function checkSplitDiffHighlighted(diffId, startLines, endLines) {
let left, right;
if (startLines[0] && endLines[1]) {
left = startLines[0];
right = endLines[1];
} else {
left = endLines[0];
right = startLines[1];
}
cy.get(`#${diffId}-from .hljs-ln-line`)
.then(fromLines => {
cy.get(`#${diffId}-to .hljs-ln-line`)
.then(toLines => {
const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`);
const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`);
const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top;
let inHighlightedRange = false;
for (let i = 0; i < Math.max(fromLines.length, toLines.length); ++i) {
const fromLn = fromLines[i];
const toLn = toLines[i];
const fromLnNumber = $(fromLn).data('line-number');
const toLnNumber = $(toLn).data('line-number');
if ((leftLineAbove && fromLnNumber === left) ||
(!leftLineAbove && toLnNumber === right) ||
(leftLineAbove && toLnNumber === right) ||
(!leftLineAbove && fromLnNumber === left)) {
inHighlightedRange = true;
}
if (fromLn) {
+ const fromBackgroundColor = $(fromLn).css('background-color');
const fromMixBlendMode = $(fromLn).css('mix-blend-mode');
if (inHighlightedRange && fromLnNumber) {
assert.equal(fromMixBlendMode, 'multiply');
+ assert.notEqual(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(fromMixBlendMode, 'normal');
+ assert.equal(fromBackgroundColor, 'rgba(0, 0, 0, 0)');
}
}
if (toLn) {
+ const toBackgroundColor = $(toLn).css('background-color');
const toMixBlendMode = $(toLn).css('mix-blend-mode');
if (inHighlightedRange && toLnNumber) {
assert.equal(toMixBlendMode, 'multiply');
+ assert.notEqual(toBackgroundColor, 'rgba(0, 0, 0, 0)');
} else {
assert.equal(toMixBlendMode, 'normal');
+ assert.equal(toBackgroundColor, 'rgba(0, 0, 0, 0)');
}
}
if ((leftLineAbove && toLnNumber === right) ||
(!leftLineAbove && fromLnNumber === left)) {
inHighlightedRange = false;
}
}
});
});
}
function splitDiffHighlightingTest(diffId, startLines, endLines) {
// render diff
cy.get(`#diff_${diffId}`)
.scrollIntoView()
.find(`#${diffId}-unified-diff`)
.should('be.visible');
cy.get(`#diff_${diffId}`)
.contains('label', 'Side-by-side')
.click()
// ensure all asynchronous treatments in the page have been performed
// before testing diff highlighting
.then(() => {
// select lines range in diff
let startElt;
if (startLines[0]) {
startElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${startLines[0]}"]`;
} else {
startElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${startLines[1]}"]`;
}
let endElt;
if (endLines[0]) {
endElt = `#${diffId}-from .hljs-ln-numbers[data-line-number="${endLines[0]}"]`;
} else {
endElt = `#${diffId}-to .hljs-ln-numbers[data-line-number="${endLines[1]}"]`;
}
cy.get(startElt).click();
cy.get(endElt).click({shiftKey: true});
const selectedLinesFragment =
swh.revision.selectedDiffLinesToFragment(startLines, endLines, false);
// check URL fragment has been updated
cy.hash().should('be.equal', `#diff_${diffId}+${selectedLinesFragment}`);
// check lines range is highlighted
checkSplitDiffHighlighted(diffId, startLines, endLines);
// check selected diff lines get highlighted when reloading page
// with highlighting info in URL fragment
cy.reload();
cy.get(`#diff_${diffId}`)
.get(`#${diffId}-split-diff`)
.get(`#${diffId}-to`)
.should('be.visible');
checkSplitDiffHighlighted(diffId, startLines, endLines);
});
}
it('should highlight split diff from and to lines when selecting them from top-left to bottom-right', function() {
const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight split diff from and to lines when selecting them from bottom-right to top-left', function() {
const diffHighlightingData = diffsHighlightingData['split-from-top-to-bottom'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, endLines, startLines);
});
it('should highlight split diff from and to lines when selecting them from top-right to bottom-left', function() {
const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, startLines, endLines);
});
it('should highlight split diff from and to lines when selecting them from bottom-left to top-right', function() {
const diffHighlightingData = diffsHighlightingData['split-to-top-from-bottom'];
const diffId = diffHighlightingData.diffId;
let startLines = diffHighlightingData.startLines;
let endLines = diffHighlightingData.endLines;
splitDiffHighlightingTest(diffId, endLines, startLines);
});
+
+ it('should highlight diff lines properly when a content is browsed in the Files tab', function() {
+ const url = this.Urls.browse_revision(revision) + `?origin=${origin}&path=README.md`;
+ cy.visit(url);
+ cy.get('a[data-toggle="tab"]')
+ .contains('Changes')
+ .click();
+ const diffHighlightingData = diffsHighlightingData['unified'];
+ const diffId = diffHighlightingData.diffId;
+ let startLines = diffHighlightingData.startLines;
+ let endLines = diffHighlightingData.endLines;
+
+ unifiedDiffHighlightingTest(diffId, startLines, endLines);
+
+ });
+
});
diff --git a/swh/web/assets/src/bundles/revision/diff-utils.js b/swh/web/assets/src/bundles/revision/diff-utils.js
index 9f63912d..1063313c 100644
--- a/swh/web/assets/src/bundles/revision/diff-utils.js
+++ b/swh/web/assets/src/bundles/revision/diff-utils.js
@@ -1,788 +1,794 @@
/**
* 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';
import {removeUrlFragment} from 'utils/functions';
import diffPanelTemplate from './diff-panel.ejs';
// 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 keep track of diff lines to highlight
let startLines = null;
let endLines = null;
// map max line numbers characters to diff
const diffMaxNumberChars = {};
// focused diff for highlighting
let focusedDiff = null;
// highlighting color
const lineHighlightColor = '#fdf3da';
// might contain diff lines to highlight parsed from URL fragment
let selectedDiffLinesInfo;
// URL fragment to append when switching to 'Changes' tab
const changesUrlFragment = '#swh-revision-changes';
// current displayed tab name
let currentTabName = 'Files';
// 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
export function formatDiffLineNumbers(diffId, fromLine, toLine) {
const maxNumberChars = diffMaxNumberChars[diffId];
const fromLineStr = toLnStr(fromLine);
const toLineStr = toLnStr(toLine);
let ret = '';
for (let i = 0; i < (maxNumberChars - fromLineStr.length); ++i) {
ret += ' ';
}
ret += fromLineStr;
ret += ' ';
for (let i = 0; i < (maxNumberChars - toLineStr.length); ++i) {
ret += ' ';
}
ret += toLineStr;
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;
}
}
function toLnInt(lnStr) {
return lnStr ? parseInt(lnStr) : 0;
};
function toLnStr(lnInt) {
return lnInt ? lnInt.toString() : '';
};
// parse diff line numbers to an int array [from, to]
export function parseDiffLineNumbers(lineNumbersStr, from, to) {
let lines;
if (!from && !to) {
lines = lineNumbersStr.replace(/[ ]+/g, ' ').split(' ');
if (lines.length > 2) {
lines.shift();
}
lines = lines.map(x => toLnInt(x));
} else {
let lineNumber = toLnInt(lineNumbersStr.trim());
if (from) {
lines = [lineNumber, 0];
} else if (to) {
lines = [0, lineNumber];
}
}
return lines;
}
// serialize selected line numbers range to string for URL fragment
export function selectedDiffLinesToFragment(startLines, endLines, unified) {
let selectedLinesFragment = '';
selectedLinesFragment += `F${startLines[0] || 0}`;
selectedLinesFragment += `T${startLines[1] || 0}`;
selectedLinesFragment += `-F${endLines[0] || 0}`;
selectedLinesFragment += `T${endLines[1] || 0}`;
if (unified) {
selectedLinesFragment += '-unified';
} else {
selectedLinesFragment += '-split';
}
return selectedLinesFragment;
}
// parse selected lines from URL fragment
export function fragmentToSelectedDiffLines(fragment) {
const RE_LINES = /F([0-9]+)T([0-9]+)-F([0-9]+)T([0-9]+)-([a-z]+)/;
const matchObj = RE_LINES.exec(fragment);
if (matchObj.length === 6) {
return {
startLines: [parseInt(matchObj[1]), parseInt(matchObj[2])],
endLines: [parseInt(matchObj[3]), parseInt(matchObj[4])],
unified: matchObj[5] === 'unified'
};
} else {
return null;
}
}
// function to highlight a single diff line
function highlightDiffLine(diffId, i) {
let line = $(`#${diffId} .hljs-ln-line[data-line-number="${i}"]`);
let lineNumbers = $(`#${diffId} .hljs-ln-numbers[data-line-number="${i}"]`);
lineNumbers.css('color', 'black');
lineNumbers.css('font-weight', 'bold');
line.css('background-color', lineHighlightColor);
line.css('mix-blend-mode', 'multiply');
return line;
}
// function to reset highlighting
function resetHighlightedDiffLines(resetVars = true) {
if (resetVars) {
focusedDiff = null;
startLines = null;
endLines = null;
}
$('.hljs-ln-line[data-line-number]').css('background-color', 'initial');
$('.hljs-ln-line[data-line-number]').css('mix-blend-mode', 'initial');
$('.hljs-ln-numbers[data-line-number]').css('color', '#aaa');
$('.hljs-ln-numbers[data-line-number]').css('font-weight', 'initial');
if (currentTabName === 'Changes' && window.location.hash !== changesUrlFragment) {
- window.location.hash = changesUrlFragment;
+ window.history.replaceState('', document.title,
+ window.location.pathname + window.location.search + changesUrlFragment);
}
}
// highlight lines in a diff, return first highlighted line numbers element
function highlightDiffLines(diffId, startLines, endLines, unified) {
let firstHighlightedLine;
// unified diff case
if (unified) {
let start = formatDiffLineNumbers(diffId, startLines[0], startLines[1]);
let end = formatDiffLineNumbers(diffId, endLines[0], endLines[1]);
const startLine = $(`#${diffId} .hljs-ln-line[data-line-number="${start}"]`);
const endLine = $(`#${diffId} .hljs-ln-line[data-line-number="${end}"]`);
if ($(endLine).position().top < $(startLine).position().top) {
[start, end] = [end, start];
firstHighlightedLine = endLine;
} else {
firstHighlightedLine = startLine;
}
const lineTd = highlightDiffLine(diffId, start);
let tr = $(lineTd).closest('tr');
let lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString();
while (lineNumbers !== end) {
if (lineNumbers.trim()) {
highlightDiffLine(diffId, lineNumbers);
}
tr = $(tr).next();
lineNumbers = $(tr).children('.hljs-ln-line').data('line-number').toString();
}
highlightDiffLine(diffId, end);
// split diff case
} else {
// highlight only from part of the diff
if (startLines[0] && endLines[0]) {
const start = Math.min(startLines[0], endLines[0]);
const end = Math.max(startLines[0], endLines[0]);
for (let i = start; i <= end; ++i) {
highlightDiffLine(`${diffId}-from`, i);
}
firstHighlightedLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${start}"]`);
// highlight only to part of the diff
} else if (startLines[1] && endLines[1]) {
const start = Math.min(startLines[1], endLines[1]);
const end = Math.max(startLines[1], endLines[1]);
for (let i = start; i <= end; ++i) {
highlightDiffLine(`${diffId}-to`, i);
}
firstHighlightedLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${start}"]`);
// highlight both part of the diff
} else {
let left, right;
if (startLines[0] && endLines[1]) {
left = startLines[0];
right = endLines[1];
} else {
left = endLines[0];
right = startLines[1];
}
const leftLine = $(`#${diffId}-from .hljs-ln-line[data-line-number="${left}"]`);
const rightLine = $(`#${diffId}-to .hljs-ln-line[data-line-number="${right}"]`);
const leftLineAbove = $(leftLine).position().top < $(rightLine).position().top;
if (leftLineAbove) {
firstHighlightedLine = leftLine;
} else {
firstHighlightedLine = rightLine;
}
let fromTr = $(`#${diffId}-from tr`).first();
let fromLn = $(fromTr).children('.hljs-ln-line').data('line-number');
let toTr = $(`#${diffId}-to tr`).first();
let toLn = $(toTr).children('.hljs-ln-line').data('line-number');
let canHighlight = false;
while (true) {
if (leftLineAbove && fromLn === left) {
canHighlight = true;
} else if (!leftLineAbove && toLn === right) {
canHighlight = true;
}
if (canHighlight && fromLn) {
highlightDiffLine(`${diffId}-from`, fromLn);
}
if (canHighlight && toLn) {
highlightDiffLine(`${diffId}-to`, toLn);
}
if ((leftLineAbove && toLn === right) || (!leftLineAbove && fromLn === left)) {
break;
}
fromTr = $(fromTr).next();
fromLn = $(fromTr).children('.hljs-ln-line').data('line-number');
toTr = $(toTr).next();
toLn = $(toTr).children('.hljs-ln-line').data('line-number');
}
}
}
let selectedLinesFragment = selectedDiffLinesToFragment(startLines, endLines, unified);
window.location.hash = `diff_${diffId}+${selectedLinesFragment}`;
return firstHighlightedLine;
}
// callback to switch from side-by-side diff to unified one
export function showUnifiedDiff(diffId) {
$(`#${diffId}-split-diff`).css('display', 'none');
$(`#${diffId}-unified-diff`).css('display', 'block');
}
// callback to switch from unified diff to side-by-side one
export function showSplitDiff(diffId) {
$(`#${diffId}-unified-diff`).css('display', 'none');
$(`#${diffId}-split-diff`).css('display', 'block');
}
// 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;
}
function setLineNumbers(lnElt, lineNumbers) {
$(lnElt).attr('data-line-number', lineNumbers || '');
$(lnElt).children().attr('data-line-number', lineNumbers || '');
$(lnElt).siblings().attr('data-line-number', lineNumbers || '');
}
// 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.lineNumbersBlockSync(block);
});
// 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);
});
diffMaxNumberChars[diffId] = maxNumberChars;
// 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.lineNumbersBlockSync(block);
});
// 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(`${linesInfoText}`);
} 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) => {
const lineNumbers = formatDiffLineNumbers(diffId, fromToLines[i][0], fromToLines[i][1]);
setLineNumbers(lnElt, lineNumbers);
});
// set line numbers for the from side-by-side diff
$(`#${diffId}-from .hljs-ln-numbers`).each((i, lnElt) => {
setLineNumbers(lnElt, fromLines[i]);
});
// set line numbers for the to side-by-side diff
$(`#${diffId}-to .hljs-ln-numbers`).each((i, lnElt) => {
setLineNumbers(lnElt, toLines[i]);
});
// 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) {
$(`#diff_${diffId} .diff-styles`).css('visibility', 'visible');
}
setDiffVisible(diffId);
// highlight diff lines if provided in URL fragment
if (selectedDiffLinesInfo &&
selectedDiffLinesInfo.diffPanelId.indexOf(diffId) !== -1) {
if (!selectedDiffLinesInfo.unified) {
showSplitDiff(diffId);
}
const firstHighlightedLine = highlightDiffLines(
diffId, selectedDiffLinesInfo.startLines,
selectedDiffLinesInfo.endLines, selectedDiffLinesInfo.unified);
$('html, body').animate(
{
scrollTop: firstHighlightedLine.offset().top - 50
},
{
duration: 500
}
);
}
}
});
}
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('diff_', '');
computeDiff(diffsUrls[diffId], diffId);
}
});
}
function genDiffPanel(diffData) {
let diffPanelTitle = diffData.path;
if (diffData.type === 'rename') {
diffPanelTitle = `${diffData.from_path} → ${diffData.to_path}`;
}
return diffPanelTemplate({
diffData: diffData,
diffPanelTitle: diffPanelTitle,
swhSpinnerSrc: swhSpinnerSrc
});
}
// setup waypoints to request diffs computation on the fly while scrolling
function setupWaypoints() {
for (let i = 0; i < changes.length; ++i) {
let diffData = changes[i];
// create a waypoint that will trigger diff computation when
// the top of the diff panel hits the bottom of the viewport
$(`#diff_${diffData.id}`).waypoint({
handler: function() {
if (isInViewport(this.element)) {
let diffId = this.element.id.replace('diff_', '');
computeDiff(diffsUrls[diffId], diffId);
this.destroy();
}
},
offset: '100%'
});
// create a waypoint that will trigger diff computation when
// the bottom of the diff panel hits the top of the viewport
$(`#diff_${diffData.id}`).waypoint({
handler: function() {
if (isInViewport(this.element)) {
let diffId = this.element.id.replace('diff_', '');
computeDiff(diffsUrls[diffId], diffId);
this.destroy();
}
},
offset: function() {
return -$(this.element).height();
}
});
}
Waypoint.refreshAll();
}
function scrollToDiffPanel(diffPanelId, setHash = true) {
// disable waypoints while scrolling as we do not want to
// launch computation of diffs the user is not interested in
// (file changes list can be large)
Waypoint.disableAll();
$('html, body').animate(
{
scrollTop: $(diffPanelId).offset().top
},
{
duration: 500,
complete: () => {
if (setHash) {
window.location.hash = diffPanelId;
}
// enable waypoints back after scrolling
Waypoint.enableAll();
// compute diffs visible in the viewport
computeVisibleDiffs();
}
});
}
// callback when the user clicks on the 'Compute all diffs' button
export function computeAllDiffs(event) {
$(event.currentTarget).addClass('active');
for (let diffId in diffsUrls) {
if (diffsUrls.hasOwnProperty(diffId)) {
computeDiff(diffsUrls[diffId], diffId);
}
}
event.stopPropagation();
}
export async function initRevisionDiff(revisionMessageBody, diffRevisionUrl) {
await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
// callback when the 'Changes' tab is activated
$(document).on('shown.bs.tab', 'a[data-toggle="tab"]', e => {
currentTabName = e.currentTarget.text.trim();
if (currentTabName === 'Changes') {
window.location.hash = changesUrlFragment;
$('#readme-panel').css('display', 'none');
if (changes) {
return;
}
// request computation of revision file changes list
// when navigating to the 'Changes' tab and add diff panels
// to the DOM when receiving the result
fetch(diffRevisionUrl)
.then(response => response.json())
.then(data => {
changes = data.changes;
nbChangedFiles = data.total_nb_changes;
let changedFilesText = `${nbChangedFiles} changed file`;
if (nbChangedFiles !== 1) {
changedFilesText += 's';
}
$('#swh-revision-changed-files').text(changedFilesText);
$('#swh-total-nb-diffs').text(changes.length);
$('#swh-revision-changes-list pre')[0].innerHTML = data.changes_msg;
$('#swh-revision-changes-loading').css('display', 'none');
$('#swh-revision-changes-list pre').css('display', 'block');
$('#swh-compute-all-diffs').css('visibility', 'visible');
$('#swh-revision-changes-list').removeClass('in');
if (nbChangedFiles > changes.length) {
$('#swh-too-large-revision-diff').css('display', 'block');
$('#swh-nb-loaded-diffs').text(changes.length);
}
for (let i = 0; i < changes.length; ++i) {
let diffData = changes[i];
diffsUrls[diffData.id] = diffData.diff_url;
$('#swh-revision-diffs').append(genDiffPanel(diffData));
}
setupWaypoints();
computeVisibleDiffs();
if (selectedDiffLinesInfo) {
scrollToDiffPanel(selectedDiffLinesInfo.diffPanelId, false);
}
});
} else if (currentTabName === 'Files') {
removeUrlFragment();
$('#readme-panel').css('display', 'block');
}
});
$(document).ready(() => {
if (revisionMessageBody.length > 0) {
$('#swh-revision-message').addClass('in');
} else {
$('#swh-collapse-revision-message').attr('data-toggle', '');
}
// callback when the user requests to scroll on a specific diff or back to top
$('#swh-revision-changes-list a[href^="#"], #back-to-top a[href^="#"]').click(e => {
let href = $.attr(e.currentTarget, 'href');
scrollToDiffPanel(href);
return false;
});
// click callback for highlighting diff lines
$('body').click(evt => {
+
+ if (currentTabName !== 'Changes') {
+ return;
+ }
+
if (evt.target.classList.contains('hljs-ln-n')) {
const diffId = $(evt.target).closest('code').prop('id');
const from = diffId.indexOf('-from') !== -1;
const to = diffId.indexOf('-to') !== -1;
const lineNumbers = $(evt.target).data('line-number').toString();
const currentDiff = diffId.replace('-from', '').replace('-to', '');
if (!evt.shiftKey || currentDiff !== focusedDiff || !lineNumbers.trim()) {
resetHighlightedDiffLines();
focusedDiff = currentDiff;
}
if (currentDiff === focusedDiff && lineNumbers.trim()) {
if (!evt.shiftKey) {
startLines = parseDiffLineNumbers(lineNumbers, from, to);
highlightDiffLines(currentDiff, startLines, startLines, !from && !to);
} else if (startLines) {
resetHighlightedDiffLines(false);
endLines = parseDiffLineNumbers(lineNumbers, from, to);
highlightDiffLines(currentDiff, startLines, endLines, !from && !to);
}
}
} else {
resetHighlightedDiffLines();
}
});
// if an URL fragment for highlighting a diff is present
// parse highlighting info and initiate diff loading
const fragment = window.location.hash;
if (fragment) {
const split = fragment.split('+');
if (split.length === 2) {
selectedDiffLinesInfo = fragmentToSelectedDiffLines(split[1]);
if (selectedDiffLinesInfo) {
selectedDiffLinesInfo.diffPanelId = split[0];
$(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show');
}
}
if (fragment === changesUrlFragment) {
$(`.nav-tabs a[href="${changesUrlFragment}"]`).tab('show');
}
}
});
}
diff --git a/swh/web/assets/src/bundles/webapp/code-highlighting.js b/swh/web/assets/src/bundles/webapp/code-highlighting.js
index cbe32515..f386c859 100644
--- a/swh/web/assets/src/bundles/webapp/code-highlighting.js
+++ b/swh/web/assets/src/bundles/webapp/code-highlighting.js
@@ -1,111 +1,114 @@
/**
* Copyright (C) 2018-2019 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 {removeUrlFragment} from 'utils/functions';
export async function highlightCode(showLineNumbers = true) {
await import(/* webpackChunkName: "highlightjs" */ 'utils/highlightjs');
// keep track of the first highlighted line
let firstHighlightedLine = null;
// highlighting color
let lineHighlightColor = 'rgb(193, 255, 193)';
// function to highlight a line
function highlightLine(i) {
let lineTd = $(`.hljs-ln-line[data-line-number="${i}"]`);
lineTd.css('background-color', lineHighlightColor);
return lineTd;
}
// function to reset highlighting
function resetHighlightedLines() {
firstHighlightedLine = null;
$('.hljs-ln-line[data-line-number]').css('background-color', 'inherit');
}
function scrollToLine(lineDomElt) {
if ($(lineDomElt).closest('.swh-content').length > 0) {
$('html, body').animate({
scrollTop: $(lineDomElt).offset().top - 70
}, 500);
}
}
// function to highlight lines based on a url fragment
// in the form '#Lx' or '#Lx-Ly'
function parseUrlFragmentForLinesToHighlight() {
let lines = [];
let linesRegexp = new RegExp(/L(\d+)/g);
let line = linesRegexp.exec(window.location.hash);
+ if (line === null) {
+ return;
+ }
while (line) {
lines.push(parseInt(line[1]));
line = linesRegexp.exec(window.location.hash);
}
resetHighlightedLines();
if (lines.length === 1) {
firstHighlightedLine = parseInt(lines[0]);
scrollToLine(highlightLine(lines[0]));
} else if (lines[0] < lines[lines.length - 1]) {
firstHighlightedLine = parseInt(lines[0]);
scrollToLine(highlightLine(lines[0]));
for (let i = lines[0] + 1; i <= lines[lines.length - 1]; ++i) {
highlightLine(i);
}
}
}
$(document).ready(() => {
// highlight code and add line numbers
$('code').each((i, block) => {
hljs.highlightBlock(block);
if (showLineNumbers) {
hljs.lineNumbersBlock(block, {singleLine: true});
}
});
if (!showLineNumbers) {
return;
}
// click handler to dynamically highlight line(s)
// when the user clicks on a line number (lines range
// can also be highlighted while holding the shift key)
- $('body').click(evt => {
+ $('.swh-content').click(evt => {
if (evt.target.classList.contains('hljs-ln-n')) {
let line = parseInt($(evt.target).data('line-number'));
if (evt.shiftKey && firstHighlightedLine && line > firstHighlightedLine) {
let firstLine = firstHighlightedLine;
resetHighlightedLines();
for (let i = firstLine; i <= line; ++i) {
highlightLine(i);
}
firstHighlightedLine = firstLine;
window.location.hash = `#L${firstLine}-L${line}`;
} else {
resetHighlightedLines();
highlightLine(line);
window.location.hash = `#L${line}`;
scrollToLine(evt.target);
}
} else if ($(evt.target).closest('.hljs-ln').length) {
resetHighlightedLines();
removeUrlFragment();
}
});
// update lines highlighting when the url fragment changes
$(window).on('hashchange', () => parseUrlFragmentForLinesToHighlight());
// schedule lines highlighting if any as hljs.lineNumbersBlock() is async
setTimeout(() => {
parseUrlFragmentForLinesToHighlight();
});
});
}