diff --git a/frontend/src/base.html b/frontend/src/base.html index f123ac7ac..0a261404e 100755 --- a/frontend/src/base.html +++ b/frontend/src/base.html @@ -62,8 +62,8 @@

{{#lines}} - - + + diff --git a/frontend/src/common.js b/frontend/src/common.js index 4c8bc8d29..753a6a00e 100644 --- a/frontend/src/common.js +++ b/frontend/src/common.js @@ -1,5 +1,5 @@ import Mustache from "mustache"; -import { buildRoute, readRoute, updateRoute } from "./route.js"; +import { buildRoute, readRoute } from "./route.js"; import { ZERO_COVERAGE_FILTERS } from "./zero_coverage_report.js"; export const REV_LATEST = "latest"; @@ -18,14 +18,12 @@ export async function main(load, display) { // Wait for DOM to be ready before displaying await DOM_READY; await display(data); - monitorOptions(); // Full workflow, loading then displaying data // used for following updates const full = async function() { const data = await load(); await display(data); - monitorOptions(); }; // React to url changes @@ -176,34 +174,6 @@ export function isEnabled(opt) { return value === "on"; } -function monitorOptions() { - // Monitor input & select changes - const fields = document.querySelectorAll("input, select"); - for (const field of fields) { - if (field.type === "text") { - // React on enter - field.onkeydown = async evt => { - if (evt.keyCode === 13) { - const params = {}; - params[evt.target.name] = evt.target.value; - updateRoute(params); - } - }; - } else { - // React on change - field.onchange = async evt => { - let value = evt.target.value; - if (evt.target.type === "checkbox") { - value = evt.target.checked ? "on" : "off"; - } - const params = {}; - params[evt.target.name] = value; - updateRoute(params); - }; - } - } -} - // hgmo. const sourceCache = {}; export async function getSource(file, revision) { diff --git a/frontend/src/index.js b/frontend/src/index.js index 1ffa8751a..1c06bee32 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -13,7 +13,7 @@ import { getSource, getFilters } from "./common.js"; -import { buildRoute, readRoute, updateRoute } from "./route.js"; +import { buildRoute, monitorOptions, readRoute, updateRoute } from "./route.js"; import { zeroCoverageDisplay, zeroCoverageMenu @@ -25,7 +25,8 @@ import Chartist from "chartist"; import "chartist/dist/chartist.css"; const VIEW_ZERO_COVERAGE = "zero"; -const VIEW_BROWSER = "browser"; +const VIEW_DIRECTORY = "directory"; +const VIEW_FILE = "file"; function browserMenu(revision, filters, route) { const context = { @@ -111,7 +112,8 @@ async function showDirectory(dir, revision, files) { navbar: buildNavbar(dir, revision), files: files.map(file => { file.route = buildRoute({ - path: file.path + path: file.path, + view: file.type }); // Calc decimal range to make a nice coloration @@ -128,8 +130,8 @@ async function showDirectory(dir, revision, files) { render("file_browser", context, "output"); } -async function showFile(file, revision) { - const source = await getSource(file.path, revision); +async function showFile(source, file, revision, selectedLine) { + selectedLine = selectedLine !== undefined ? parseInt(selectedLine) : -1; let language; if (file.path.endsWith("cpp") || file.path.endsWith("h")) { @@ -148,7 +150,6 @@ async function showFile(file, revision) { const context = { navbar: buildNavbar(file.path, revision), - revision: revision || REV_LATEST, language, lines: source.map((line, nb) => { const coverage = file.coverage[nb]; @@ -175,12 +176,18 @@ async function showFile(file, revision) { }; } } + + // Override css class when selected + if (nb === selectedLine) { + cssClass = "selected"; + } return { nb, hits, coverage, line: line || " ", - covered: cssClass + css_class: cssClass, + route: buildRoute({ line: nb }) }; }) }; @@ -189,6 +196,15 @@ async function showFile(file, revision) { hide("history"); const output = render("file_coverage", context, "output"); + // Scroll to line + if (selectedLine > 0) { + const line = output.querySelector("#l" + selectedLine); + line.scrollIntoView({ + behavior: "smooth", + block: "center" + }); + } + // Highlight source code once displayed Prism.highlightAll(output); } @@ -218,11 +234,20 @@ async function load() { }; } + // Default to directory view on home + if (!route.view) { + route.view = VIEW_DIRECTORY; + } + try { - var [coverage, history, filters] = await Promise.all([ + const viewContent = + route.view === VIEW_DIRECTORY + ? getHistory(route.path, route.platform, route.suite) + : getSource(route.path, route.revision); + var [coverage, filters, viewData] = await Promise.all([ getPathCoverage(route.path, route.revision, route.platform, route.suite), - getHistory(route.path, route.platform, route.suite), - getFilters() + getFilters(), + viewContent ]); } catch (err) { console.warn("Failed to load coverage", err); @@ -230,37 +255,40 @@ async function load() { message("error", "Failed to load coverage: " + err.message); throw err; } - return { - view: VIEW_BROWSER, + view: route.view, path: route.path, revision: route.revision, route, coverage, - history, - filters + filters, + viewData }; } -async function display(data) { +export async function display(data) { if (data.view === VIEW_ZERO_COVERAGE) { await zeroCoverageMenu(data.route); await zeroCoverageDisplay(data.zeroCoverage, data.path); - } else if (data.view === VIEW_BROWSER) { + } else if (data.view === VIEW_DIRECTORY) { + hide("message"); browserMenu(data.revision, data.filters, data.route); - - if (data.coverage.type === "directory") { - hide("message"); - await graphHistory(data.history, data.path); - await showDirectory(data.path, data.revision, data.coverage.children); - } else if (data.coverage.type === "file") { - await showFile(data.coverage, data.revision); - } else { - message("error", "Invalid file type: " + data.coverate.type); - } + await graphHistory(data.viewData, data.path); + await showDirectory(data.path, data.revision, data.coverage.children); + } else if (data.view === VIEW_FILE) { + browserMenu(data.revision, data.filters, data.route); + await showFile( + data.viewData, + data.coverage, + data.revision, + data.route.line + ); } else { message("error", "Invalid view : " + data.view); } + + // Always monitor options on newly rendered output + monitorOptions(data); } main(load, display); diff --git a/frontend/src/route.js b/frontend/src/route.js index 58eb2de0b..81328afa2 100644 --- a/frontend/src/route.js +++ b/frontend/src/route.js @@ -1,4 +1,5 @@ import { REV_LATEST } from "./common.js"; +import { display } from "./index.js"; export function readRoute() { // Reads all filters from current URL hash @@ -42,5 +43,51 @@ export function buildRoute(params) { export function updateRoute(params) { // Update full hash with an updated url + // Will trigger full load + display update window.location.hash = buildRoute(params); } + +export async function updateRouteImmediate(hash, data) { + // Will trigger only a display update, no remote data will be fetched + + // Update route without reloading content + history.pushState(null, null, hash); + + // Update the route stored in data + data.route = readRoute(); + await display(data); +} + +export function monitorOptions(currentData) { + // Monitor input & select changes + const fields = document.querySelectorAll("input, select, a.scroll"); + for (const field of fields) { + if (field.classList.contains("scroll")) { + // On a scroll event, update display without any data loading + field.onclick = async evt => { + evt.preventDefault(); + updateRouteImmediate(evt.target.hash, currentData); + }; + } else if (field.type === "text") { + // React on enter + field.onkeydown = async evt => { + if (evt.keyCode === 13) { + const params = {}; + params[evt.target.name] = evt.target.value; + updateRoute(params); + } + }; + } else { + // React on change + field.onchange = async evt => { + let value = evt.target.value; + if (evt.target.type === "checkbox") { + value = evt.target.checked ? "on" : "off"; + } + const params = {}; + params[evt.target.name] = value; + updateRoute(params); + }; + } + } +} diff --git a/frontend/src/style.scss b/frontend/src/style.scss index 58fa85449..2f5449ad4 100644 --- a/frontend/src/style.scss +++ b/frontend/src/style.scss @@ -8,6 +8,7 @@ $footer_height: 60px; $coverage_low: #d91a47; $coverage_warn: #ff9a36; $coverage_good: #438718; +$highlighted: #f7f448; $small_screen: 1900px; body { @@ -352,6 +353,17 @@ $samp_size: 20px; background: $uncovered_color; } } + + &.selected { + font-weight: bold; + td { + background: $highlighted; + } + + pre { + background: $highlighted; + } + } } } }
{{ nb }}
{{ nb }}
{{ line }}