diff --git a/cypress/integration/revision-diff.spec.js b/cypress/integration/revision-diff.spec.js --- a/cypress/integration/revision-diff.spec.js +++ b/cypress/integration/revision-diff.spec.js @@ -1,29 +1,57 @@ /** - * Copyright (C) 2019 The Software Heritage developers + * 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 */ -let origin; +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 Diffs View', function() { - before(function() { - origin = this.origin[0]; - const url = this.Urls.browse_revision(origin.revisions[0]) + `?origin=${origin.url}`; - cy.visit(url).window().then(win => { + 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; }); }); - }); - - beforeEach(function() { - const url = this.Urls.browse_revision(origin.revisions[0]) + `?origin=${origin.url}`; - cy.visit(url); cy.get('a[data-toggle="tab"]') .contains('Changes') .click(); @@ -65,7 +93,7 @@ it('should have correct links in diff file names', function() { for (let change of diffData.changes) { - cy.get(`#swh-revision-changes-list a[href="#panel_${change.id}"`) + cy.get(`#swh-revision-changes-list a[href="#diff_${change.id}"`) .should('be.visible'); } }); @@ -76,7 +104,7 @@ for (let change of diffData.changes) { cy.get(`#${change.id}-unified-diff`) .should('be.visible'); - cy.get(`#${change.id}-splitted-diff`) + cy.get(`#${change.id}-split-diff`) .should('not.be.visible'); } }); @@ -84,12 +112,342 @@ 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(`#panel_${id}`) + cy.get(`#diff_${id}`) .contains('label', 'Side-by-side') .click(); - cy.get(`#${id}-splitted-diff`) + 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 mixBlendMode = $(line).css('mix-blend-mode'); + if (inHighlightedRange && parseInt(lnNumber)) { + assert.equal(mixBlendMode, 'multiply'); + } else { + assert.equal(mixBlendMode, 'normal'); + } + 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('body').type(`{shift}`, { + release: false + }); + cy.get(endElt).click(); + + // 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('body').type(`{shift}`, { + release: false + }); + cy.get(endElt).click(); + + 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 fromMixBlendMode = $(fromLn).css('mix-blend-mode'); + if (inHighlightedRange && fromLnNumber) { + assert.equal(fromMixBlendMode, 'multiply'); + } else { + assert.equal(fromMixBlendMode, 'normal'); + } + } + + if (toLn) { + const toMixBlendMode = $(toLn).css('mix-blend-mode'); + if (inHighlightedRange && toLnNumber) { + assert.equal(toMixBlendMode, 'multiply'); + } else { + assert.equal(toMixBlendMode, 'normal'); + } + } + + 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('body').type(`{shift}`, { + release: false + }); + cy.get(endElt).click(); + + 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); + }); }); diff --git a/swh/web/assets/src/bundles/revision/diff-panel.ejs b/swh/web/assets/src/bundles/revision/diff-panel.ejs --- a/swh/web/assets/src/bundles/revision/diff-panel.ejs +++ b/swh/web/assets/src/bundles/revision/diff-panel.ejs @@ -4,9 +4,9 @@ License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information %> -
+
-
+
\ No newline at end of file diff --git a/swh/web/assets/src/bundles/revision/diff-utils.js b/swh/web/assets/src/bundles/revision/diff-utils.js --- a/swh/web/assets/src/bundles/revision/diff-utils.js +++ b/swh/web/assets/src/bundles/revision/diff-utils.js @@ -32,6 +32,17 @@ 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; // to check if a DOM element is in the viewport function isInViewport(elt) { @@ -45,23 +56,20 @@ } // to format the diffs line numbers -function formatDiffLineNumbers(fromLine, toLine, maxNumberChars) { +export function formatDiffLineNumbers(diffId, fromLine, toLine) { + const maxNumberChars = diffMaxNumberChars[diffId]; + const fromLineStr = toLnStr(fromLine); + const toLineStr = toLnStr(toLine); let ret = ''; - if (fromLine != null) { - for (let i = 0; i < (maxNumberChars - fromLine.length); ++i) { - ret += ' '; - } - ret += fromLine; - } - if (fromLine != null && toLine != null) { - ret += ' '; + for (let i = 0; i < (maxNumberChars - fromLineStr.length); ++i) { + ret += ' '; } - if (toLine != null) { - for (let i = 0; i < (maxNumberChars - toLine.length); ++i) { - ret += ' '; - } - ret += toLine; + ret += fromLineStr; + ret += ' '; + for (let i = 0; i < (maxNumberChars - toLineStr.length); ++i) { + ret += ' '; } + ret += toLineStr; return ret; } @@ -97,6 +105,206 @@ } } +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'); +} + +// 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) { @@ -108,6 +316,12 @@ 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; @@ -228,6 +442,8 @@ maxNumberChars = Math.max(maxNumberChars, toLine.length); }); + diffMaxNumberChars[diffId] = maxNumberChars; + // set side-by-side diffs text $(`#${diffId}-from`).text(diffFromStr); $(`#${diffId}-to`).text(diffToStr); @@ -256,26 +472,18 @@ // 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)); + 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) => { - $(lnElt).children().attr( - 'data-line-number', - formatDiffLineNumbers(fromLines[i], null, - maxNumberChars)); + setLineNumbers(lnElt, fromLines[i]); }); // 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)); + setLineNumbers(lnElt, toLines[i]); }); // last processing: @@ -306,10 +514,30 @@ // 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'); + $(`#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 + } + ); + } } }); } @@ -333,7 +561,7 @@ function computeVisibleDiffs() { $('.swh-file-diff-panel').each((i, elt) => { if (isInViewport(elt)) { - let diffId = elt.id.replace('panel_', ''); + let diffId = elt.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); } }); @@ -358,10 +586,10 @@ // create a waypoint that will trigger diff computation when // the top of the diff panel hits the bottom of the viewport - $(`#panel_${diffData.id}`).waypoint({ + $(`#diff_${diffData.id}`).waypoint({ handler: function() { if (isInViewport(this.element)) { - let diffId = this.element.id.replace('panel_', ''); + let diffId = this.element.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); this.destroy(); } @@ -371,10 +599,10 @@ // create a waypoint that will trigger diff computation when // the bottom of the diff panel hits the top of the viewport - $(`#panel_${diffData.id}`).waypoint({ + $(`#diff_${diffData.id}`).waypoint({ handler: function() { if (isInViewport(this.element)) { - let diffId = this.element.id.replace('panel_', ''); + let diffId = this.element.id.replace('diff_', ''); computeDiff(diffsUrls[diffId], diffId); this.destroy(); } @@ -387,16 +615,28 @@ Waypoint.refreshAll(); } -// callback to switch from side-by-side diff to unified one -export function showUnifiedDiff(diffId) { - $(`#${diffId}-splitted-diff`).css('display', 'none'); - $(`#${diffId}-unified-diff`).css('display', 'block'); -} - -// callback to switch from unified diff to side-by-side one -export function showSplittedDiff(diffId) { - $(`#${diffId}-unified-diff`).css('display', 'none'); - $(`#${diffId}-splitted-diff`).css('display', 'block'); +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 @@ -458,6 +698,11 @@ setupWaypoints(); computeVisibleDiffs(); + + if (selectedDiffLinesInfo) { + scrollToDiffPanel(selectedDiffLinesInfo.diffPanelId, false); + } + }); } else if (e.currentTarget.text.trim() === 'Files') { $('#readme-panel').css('display', 'block'); @@ -472,34 +717,59 @@ $('#swh-collapse-revision-message').attr('data-toggle', ''); } - let $root = $('html, body'); - // 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'); - // 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(); - - $root.animate( - { - scrollTop: $(href).offset().top - }, - { - duration: 500, - complete: () => { - window.location.hash = href; - // enable waypoints back after scrolling - Waypoint.enableAll(); - // compute diffs visible in the viewport - computeVisibleDiffs(); + scrollToDiffPanel(href); + return false; + }); + + // click callback for highlighting diff lines + $('body').click(evt => { + 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); } - }); + } - return false; + } 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="#swh-revision-changes"]').tab('show'); + } + } + } + }); } diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py --- a/swh/web/browse/views/revision.py +++ b/swh/web/browse/views/revision.py @@ -118,21 +118,21 @@ hasher.update(diff_id.encode("utf-8")) diff_id = hasher.hexdigest() change["id"] = diff_id - panel_diff_link = "#panel_" + diff_id + diff_link = "#diff_" + diff_id if change["type"] == "modify": change["content_url"] = _gen_content_url( revision, to_query_string, change["to_path"], snapshot_context ) changes_msg.append( - "modified: %s" % _gen_diff_link(i, panel_diff_link, change["to_path"]) + "modified: %s" % _gen_diff_link(i, diff_link, change["to_path"]) ) elif change["type"] == "insert": change["content_url"] = _gen_content_url( revision, to_query_string, change["to_path"], snapshot_context ) changes_msg.append( - "new file: %s" % _gen_diff_link(i, panel_diff_link, change["to_path"]) + "new file: %s" % _gen_diff_link(i, diff_link, change["to_path"]) ) elif change["type"] == "delete": parent = service.lookup_revision(revision["parents"][0]) @@ -140,8 +140,7 @@ parent, from_query_string, change["from_path"], snapshot_context ) changes_msg.append( - "deleted: %s" - % _gen_diff_link(i, panel_diff_link, change["from_path"]) + "deleted: %s" % _gen_diff_link(i, diff_link, change["from_path"]) ) elif change["type"] == "rename": change["content_url"] = _gen_content_url( @@ -149,7 +148,7 @@ ) link_text = change["from_path"] + " → " + change["to_path"] changes_msg.append( - "renamed: %s" % _gen_diff_link(i, panel_diff_link, link_text) + "renamed: %s" % _gen_diff_link(i, diff_link, link_text) ) if not changes: changes_msg.append("No changes")