Skip to content

Commit 58935b9

Browse files
authored
frontend: Support linking to a specific line, fixes #189 (#203)
1 parent 50ab70e commit 58935b9

File tree

5 files changed

+116
-59
lines changed

5 files changed

+116
-59
lines changed

frontend/src/base.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ <h2>
6262
<table>
6363
<tbody>
6464
{{#lines}}
65-
<tr class="{{covered}}">
66-
<td>{{ nb }}</td>
65+
<tr class="{{ css_class }}" id="l{{ nb }}">
66+
<td><a class="scroll" href="{{ route }}">{{ nb }}</a></td>
6767
<td>
6868
<pre class="language-{{ language }}"><code>{{ line }}</code></pre>
6969
</td>

frontend/src/common.js

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Mustache from "mustache";
2-
import { buildRoute, readRoute, updateRoute } from "./route.js";
2+
import { buildRoute, readRoute } from "./route.js";
33
import { ZERO_COVERAGE_FILTERS } from "./zero_coverage_report.js";
44

55
export const REV_LATEST = "latest";
@@ -18,14 +18,12 @@ export async function main(load, display) {
1818
// Wait for DOM to be ready before displaying
1919
await DOM_READY;
2020
await display(data);
21-
monitorOptions();
2221

2322
// Full workflow, loading then displaying data
2423
// used for following updates
2524
const full = async function() {
2625
const data = await load();
2726
await display(data);
28-
monitorOptions();
2927
};
3028

3129
// React to url changes
@@ -176,34 +174,6 @@ export function isEnabled(opt) {
176174
return value === "on";
177175
}
178176

179-
function monitorOptions() {
180-
// Monitor input & select changes
181-
const fields = document.querySelectorAll("input, select");
182-
for (const field of fields) {
183-
if (field.type === "text") {
184-
// React on enter
185-
field.onkeydown = async evt => {
186-
if (evt.keyCode === 13) {
187-
const params = {};
188-
params[evt.target.name] = evt.target.value;
189-
updateRoute(params);
190-
}
191-
};
192-
} else {
193-
// React on change
194-
field.onchange = async evt => {
195-
let value = evt.target.value;
196-
if (evt.target.type === "checkbox") {
197-
value = evt.target.checked ? "on" : "off";
198-
}
199-
const params = {};
200-
params[evt.target.name] = value;
201-
updateRoute(params);
202-
};
203-
}
204-
}
205-
}
206-
207177
// hgmo.
208178
const sourceCache = {};
209179
export async function getSource(file, revision) {

frontend/src/index.js

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
getSource,
1414
getFilters
1515
} from "./common.js";
16-
import { buildRoute, readRoute, updateRoute } from "./route.js";
16+
import { buildRoute, monitorOptions, readRoute, updateRoute } from "./route.js";
1717
import {
1818
zeroCoverageDisplay,
1919
zeroCoverageMenu
@@ -25,7 +25,8 @@ import Chartist from "chartist";
2525
import "chartist/dist/chartist.css";
2626

2727
const VIEW_ZERO_COVERAGE = "zero";
28-
const VIEW_BROWSER = "browser";
28+
const VIEW_DIRECTORY = "directory";
29+
const VIEW_FILE = "file";
2930

3031
function browserMenu(revision, filters, route) {
3132
const context = {
@@ -111,7 +112,8 @@ async function showDirectory(dir, revision, files) {
111112
navbar: buildNavbar(dir, revision),
112113
files: files.map(file => {
113114
file.route = buildRoute({
114-
path: file.path
115+
path: file.path,
116+
view: file.type
115117
});
116118

117119
// Calc decimal range to make a nice coloration
@@ -128,8 +130,8 @@ async function showDirectory(dir, revision, files) {
128130
render("file_browser", context, "output");
129131
}
130132

131-
async function showFile(file, revision) {
132-
const source = await getSource(file.path, revision);
133+
async function showFile(source, file, revision, selectedLine) {
134+
selectedLine = selectedLine !== undefined ? parseInt(selectedLine) : -1;
133135

134136
let language;
135137
if (file.path.endsWith("cpp") || file.path.endsWith("h")) {
@@ -148,7 +150,6 @@ async function showFile(file, revision) {
148150

149151
const context = {
150152
navbar: buildNavbar(file.path, revision),
151-
revision: revision || REV_LATEST,
152153
language,
153154
lines: source.map((line, nb) => {
154155
const coverage = file.coverage[nb];
@@ -175,12 +176,18 @@ async function showFile(file, revision) {
175176
};
176177
}
177178
}
179+
180+
// Override css class when selected
181+
if (nb === selectedLine) {
182+
cssClass = "selected";
183+
}
178184
return {
179185
nb,
180186
hits,
181187
coverage,
182188
line: line || " ",
183-
covered: cssClass
189+
css_class: cssClass,
190+
route: buildRoute({ line: nb })
184191
};
185192
})
186193
};
@@ -189,6 +196,15 @@ async function showFile(file, revision) {
189196
hide("history");
190197
const output = render("file_coverage", context, "output");
191198

199+
// Scroll to line
200+
if (selectedLine > 0) {
201+
const line = output.querySelector("#l" + selectedLine);
202+
line.scrollIntoView({
203+
behavior: "smooth",
204+
block: "center"
205+
});
206+
}
207+
192208
// Highlight source code once displayed
193209
Prism.highlightAll(output);
194210
}
@@ -218,49 +234,61 @@ async function load() {
218234
};
219235
}
220236

237+
// Default to directory view on home
238+
if (!route.view) {
239+
route.view = VIEW_DIRECTORY;
240+
}
241+
221242
try {
222-
var [coverage, history, filters] = await Promise.all([
243+
const viewContent =
244+
route.view === VIEW_DIRECTORY
245+
? getHistory(route.path, route.platform, route.suite)
246+
: getSource(route.path, route.revision);
247+
var [coverage, filters, viewData] = await Promise.all([
223248
getPathCoverage(route.path, route.revision, route.platform, route.suite),
224-
getHistory(route.path, route.platform, route.suite),
225-
getFilters()
249+
getFilters(),
250+
viewContent
226251
]);
227252
} catch (err) {
228253
console.warn("Failed to load coverage", err);
229254
await DOM_READY; // We want to always display this message
230255
message("error", "Failed to load coverage: " + err.message);
231256
throw err;
232257
}
233-
234258
return {
235-
view: VIEW_BROWSER,
259+
view: route.view,
236260
path: route.path,
237261
revision: route.revision,
238262
route,
239263
coverage,
240-
history,
241-
filters
264+
filters,
265+
viewData
242266
};
243267
}
244268

245-
async function display(data) {
269+
export async function display(data) {
246270
if (data.view === VIEW_ZERO_COVERAGE) {
247271
await zeroCoverageMenu(data.route);
248272
await zeroCoverageDisplay(data.zeroCoverage, data.path);
249-
} else if (data.view === VIEW_BROWSER) {
273+
} else if (data.view === VIEW_DIRECTORY) {
274+
hide("message");
250275
browserMenu(data.revision, data.filters, data.route);
251-
252-
if (data.coverage.type === "directory") {
253-
hide("message");
254-
await graphHistory(data.history, data.path);
255-
await showDirectory(data.path, data.revision, data.coverage.children);
256-
} else if (data.coverage.type === "file") {
257-
await showFile(data.coverage, data.revision);
258-
} else {
259-
message("error", "Invalid file type: " + data.coverate.type);
260-
}
276+
await graphHistory(data.viewData, data.path);
277+
await showDirectory(data.path, data.revision, data.coverage.children);
278+
} else if (data.view === VIEW_FILE) {
279+
browserMenu(data.revision, data.filters, data.route);
280+
await showFile(
281+
data.viewData,
282+
data.coverage,
283+
data.revision,
284+
data.route.line
285+
);
261286
} else {
262287
message("error", "Invalid view : " + data.view);
263288
}
289+
290+
// Always monitor options on newly rendered output
291+
monitorOptions(data);
264292
}
265293

266294
main(load, display);

frontend/src/route.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { REV_LATEST } from "./common.js";
2+
import { display } from "./index.js";
23

34
export function readRoute() {
45
// Reads all filters from current URL hash
@@ -42,5 +43,51 @@ export function buildRoute(params) {
4243

4344
export function updateRoute(params) {
4445
// Update full hash with an updated url
46+
// Will trigger full load + display update
4547
window.location.hash = buildRoute(params);
4648
}
49+
50+
export async function updateRouteImmediate(hash, data) {
51+
// Will trigger only a display update, no remote data will be fetched
52+
53+
// Update route without reloading content
54+
history.pushState(null, null, hash);
55+
56+
// Update the route stored in data
57+
data.route = readRoute();
58+
await display(data);
59+
}
60+
61+
export function monitorOptions(currentData) {
62+
// Monitor input & select changes
63+
const fields = document.querySelectorAll("input, select, a.scroll");
64+
for (const field of fields) {
65+
if (field.classList.contains("scroll")) {
66+
// On a scroll event, update display without any data loading
67+
field.onclick = async evt => {
68+
evt.preventDefault();
69+
updateRouteImmediate(evt.target.hash, currentData);
70+
};
71+
} else if (field.type === "text") {
72+
// React on enter
73+
field.onkeydown = async evt => {
74+
if (evt.keyCode === 13) {
75+
const params = {};
76+
params[evt.target.name] = evt.target.value;
77+
updateRoute(params);
78+
}
79+
};
80+
} else {
81+
// React on change
82+
field.onchange = async evt => {
83+
let value = evt.target.value;
84+
if (evt.target.type === "checkbox") {
85+
value = evt.target.checked ? "on" : "off";
86+
}
87+
const params = {};
88+
params[evt.target.name] = value;
89+
updateRoute(params);
90+
};
91+
}
92+
}
93+
}

frontend/src/style.scss

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ $footer_height: 60px;
88
$coverage_low: #d91a47;
99
$coverage_warn: #ff9a36;
1010
$coverage_good: #438718;
11+
$highlighted: #f7f448;
1112
$small_screen: 1900px;
1213

1314
body {
@@ -352,6 +353,17 @@ $samp_size: 20px;
352353
background: $uncovered_color;
353354
}
354355
}
356+
357+
&.selected {
358+
font-weight: bold;
359+
td {
360+
background: $highlighted;
361+
}
362+
363+
pre {
364+
background: $highlighted;
365+
}
366+
}
355367
}
356368
}
357369
}

0 commit comments

Comments
 (0)