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(); }); }); }