Skip to content

Commit 4b85ae7

Browse files
committed
Add function list page
1 parent 504505b commit 4b85ae7

File tree

7 files changed

+590
-59
lines changed

7 files changed

+590
-59
lines changed

gcovr-templates/html/directory_page.summary.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,11 @@ <h3>Branches</h3>
8484
<span class="legend-item coverage-medium">Medium: &ge; {{COVERAGE_MED}}%</span>
8585
{% endif %}
8686
<span class="legend-item coverage-low">Low: &lt; {{COVERAGE_MED}}%</span>
87+
{% if info.link_function_list %}
88+
<span class="legend-separator"></span>
89+
<a href="{{ FUNCTIONS_FNAME }}" class="legend-item legend-functions-link" title="List of all functions">
90+
<span class="fx-icon" aria-hidden="true"><i>f</i>(x)</span>
91+
List of functions
92+
</a>
93+
{% endif %}
8794
</div>
Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,44 @@
11
{# -*- engine: jinja -*- #}
22
<div class="functions-container">
3-
<div class="functions-header">
4-
{% set class_sortable = " sortable" if not info.static_report and function_list | length > 1 else "" %}
5-
<div class="col-function{{class_sortable}}{% if class_sortable %} sorted-ascending{% endif %}" data-sort="name">Function</div>
6-
<div class="col-calls{{class_sortable}}" data-sort="calls">Calls</div>
7-
<div class="col-lines{{class_sortable}}" data-sort="lines">Lines</div>
8-
<div class="col-branches{{class_sortable}}" data-sort="branches">Branches</div>
9-
{% if SHOW_CONDITION_COVERAGE %}
10-
<div class="col-conditions{{class_sortable}}" data-sort="conditions">Conditions</div>
11-
{% endif %}
12-
</div>
13-
143
<div class="functions-body">
15-
{% for entry in function_list %}
16-
<div class="function-row" data-name="{{entry.name}}" data-calls="{{'-' if entry['excluded'] else entry['execution_count']}}" data-lines="{{entry.line_coverage}}" data-branches="{{entry.branch_coverage}}" data-conditions="{{entry.condition_coverage}}">
17-
<div class="col-function">
18-
<a href="{%- if info.single_page %}#{{ entry['html_filename'] }}|
19-
{%- elif (html_filename != entry['html_filename']) %}{{ entry['html_filename'] }}#
20-
{%- else %}#
21-
{%- endif %}l{{ entry['line'] }}">
22-
<span class="function-name">{{ entry["name"] }}</span>
23-
<span class="function-location">{{ entry["filename"] }}:{{ entry["line"] }}</span>
24-
</a>
25-
</div>
26-
<div class="col-calls">
27-
{%- if entry["excluded"] %}<span class="excluded">excluded</span>
28-
{%- else %}
29-
{%- if entry["execution_count"] == 0 %}<span class="not-called">not called</span>
30-
{%- else %}<span class="called">{{ entry["execution_count"] }}x</span>
31-
{%- endif -%}
32-
{%- endif -%}
33-
</div>
34-
<div class="col-lines">{{ entry["line_coverage"] }}%</div>
35-
<div class="col-branches">{{ entry["branch_coverage"] }}%</div>
4+
<div class="functions-header">
5+
{% set class_sortable = " sortable" if not info.static_report and function_list | length > 1 else "" %}
6+
<div class="col-function{{class_sortable}}{% if class_sortable %} sorted-ascending{% endif %}" data-sort="name">Function</div>
7+
<div class="col-calls{{class_sortable}}" data-sort="calls">Calls</div>
8+
<div class="col-lines{{class_sortable}}" data-sort="lines">Lines</div>
369
{% if SHOW_CONDITION_COVERAGE %}
37-
<div class="col-conditions">{{ entry["condition_coverage"] }}%</div>
10+
<div class="col-conditions{{class_sortable}}" data-sort="conditions">Conditions</div>
3811
{% endif %}
3912
</div>
40-
{% endfor %}
13+
<div class="functions-loading" id="functions-loading">
14+
<div class="functions-loading-spinner"></div>
15+
<span class="functions-loading-text">Loading functions…</span>
16+
</div>
4117
</div>
4218
</div>
19+
20+
<script type="application/json" id="functions-data">
21+
[
22+
{% for entry in function_list %}
23+
{% if not loop.first %},{% endif %}
24+
{
25+
"name": {{ entry.name | tojson }},
26+
"filename": {{ entry.filename | tojson }},
27+
"line": {{ entry.line }},
28+
"html_filename": {{ entry.html_filename | tojson }},
29+
"execution_count": {{ entry.execution_count if entry.execution_count is defined and entry.execution_count is not none else 0 }},
30+
"excluded": {{ "true" if entry.excluded else "false" }},
31+
"line_coverage": "{{ entry.line_coverage }}",
32+
"branch_coverage": "{{ entry.branch_coverage }}",
33+
"condition_coverage": "{{ entry.condition_coverage }}"
34+
}
35+
{% endfor %}
36+
]
37+
</script>
38+
<script>
39+
window.__functionsPageConfig = {
40+
singlePage: {{ "true" if info.single_page else "false" }},
41+
htmlFilename: "{{ html_filename }}",
42+
showConditions: {{ "true" if SHOW_CONDITION_COVERAGE else "false" }}
43+
};
44+
</script>

gcovr-templates/html/functions_page.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{# -*- engine: jinja -*- #}
2+
{%- set function_list = all_functions %}
23
{% extends "base.html" %}
34

45
{% block html_title %}Functions - {{info.head}}{% endblock %}

gcovr-templates/html/gcovr.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
initNavOverride();
1414
initBreadcrumbs();
1515
initSearch();
16+
initFunctionRows();
1617
initSorting();
1718
initToggleButtons();
1819
initCoverageNav();
@@ -929,6 +930,227 @@
929930
}
930931
}
931932

933+
// ===========================================
934+
// Progressive Function Row Rendering
935+
// ===========================================
936+
937+
function initFunctionRows() {
938+
var dataEl = document.getElementById('functions-data');
939+
if (!dataEl) return;
940+
941+
var config = window.__functionsPageConfig || {};
942+
var data = JSON.parse(dataEl.textContent);
943+
var container = document.querySelector('.functions-body');
944+
var loadingEl = document.getElementById('functions-loading');
945+
var showConditions = config.showConditions;
946+
var singlePage = config.singlePage;
947+
var currentFile = config.htmlFilename || '';
948+
949+
if (data.length === 0) {
950+
if (loadingEl) loadingEl.remove();
951+
return;
952+
}
953+
954+
// --- Virtual scrolling setup ---
955+
var ROW_HEIGHT = 52;
956+
var BUFFER = 10;
957+
var visibleCount = Math.max(30, Math.ceil(container.clientHeight / ROW_HEIGHT) + BUFFER * 2);
958+
var viewport, visibleEl;
959+
var lastStartIdx = -1;
960+
961+
window.addEventListener('resize', function() {
962+
visibleCount = Math.max(30, Math.ceil(container.clientHeight / ROW_HEIGHT) + BUFFER * 2);
963+
lastStartIdx = -1;
964+
renderVisible();
965+
});
966+
967+
function buildHref(entry) {
968+
if (singlePage) return '#' + entry.html_filename + '|l' + entry.line;
969+
if (currentFile !== entry.html_filename) return entry.html_filename + '#l' + entry.line;
970+
return '#l' + entry.line;
971+
}
972+
973+
function entryKey(entry) {
974+
return entry.name + '|' + entry.filename + ':' + entry.line;
975+
}
976+
977+
function el(tag, cls, text) {
978+
var node = document.createElement(tag);
979+
if (cls) node.className = cls;
980+
if (text !== undefined) node.textContent = text;
981+
return node;
982+
}
983+
984+
function createRow(entry) {
985+
var row = el('div', 'function-row');
986+
if (highlightKey && entryKey(entry) === highlightKey) {
987+
row.classList.add('function-row-visited');
988+
}
989+
990+
// col-function
991+
var colFn = el('div', 'col-function');
992+
var a = document.createElement('a');
993+
a.href = buildHref(entry);
994+
a.appendChild(el('span', 'function-name', entry.name));
995+
a.appendChild(el('span', 'function-location', entry.filename + ':' + entry.line));
996+
colFn.appendChild(a);
997+
row.appendChild(colFn);
998+
999+
// col-calls
1000+
var colCalls = el('div', 'col-calls');
1001+
var callSpan;
1002+
if (entry.excluded) {
1003+
callSpan = el('span', 'excluded', 'excluded');
1004+
} else if (entry.execution_count === 0) {
1005+
callSpan = el('span', 'not-called', 'not called');
1006+
} else {
1007+
callSpan = el('span', 'called', entry.execution_count + 'x');
1008+
}
1009+
colCalls.appendChild(callSpan);
1010+
row.appendChild(colCalls);
1011+
1012+
// col-lines
1013+
row.appendChild(el('div', 'col-lines', entry.line_coverage + '%'));
1014+
1015+
// col-conditions (optional)
1016+
if (showConditions) {
1017+
row.appendChild(el('div', 'col-conditions', entry.condition_coverage + '%'));
1018+
}
1019+
1020+
return row;
1021+
}
1022+
1023+
function setupVirtualScroll() {
1024+
if (loadingEl) loadingEl.remove();
1025+
1026+
viewport = document.createElement('div');
1027+
viewport.className = 'functions-viewport';
1028+
viewport.style.height = (data.length * ROW_HEIGHT) + 'px';
1029+
1030+
visibleEl = document.createElement('div');
1031+
visibleEl.className = 'functions-visible';
1032+
1033+
viewport.appendChild(visibleEl);
1034+
container.appendChild(viewport);
1035+
}
1036+
1037+
function renderVisible() {
1038+
var scrollTop = container.scrollTop;
1039+
var startIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER);
1040+
var endIdx = Math.min(data.length, startIdx + visibleCount + BUFFER);
1041+
1042+
// Skip re-render if the window hasn't shifted
1043+
if (startIdx === lastStartIdx) return;
1044+
lastStartIdx = startIdx;
1045+
1046+
visibleEl.style.top = (startIdx * ROW_HEIGHT) + 'px';
1047+
1048+
var frag = document.createDocumentFragment();
1049+
for (var i = startIdx; i < endIdx; i++) {
1050+
frag.appendChild(createRow(data[i]));
1051+
}
1052+
visibleEl.replaceChildren(frag);
1053+
}
1054+
1055+
// --- Scroll listener (rAF-throttled) ---
1056+
var ticking = false;
1057+
container.addEventListener('scroll', function() {
1058+
if (!ticking) {
1059+
requestAnimationFrame(function() { renderVisible(); ticking = false; });
1060+
ticking = true;
1061+
}
1062+
});
1063+
1064+
// --- Save state on navigation for back-button restore ---
1065+
var highlightKey = null;
1066+
container.addEventListener('click', function(e) {
1067+
var row = e.target.closest('.function-row');
1068+
if (!row) return;
1069+
var link = row.querySelector('a');
1070+
if (!link) return;
1071+
var nameEl = row.querySelector('.function-name');
1072+
var locEl = row.querySelector('.function-location');
1073+
if (nameEl && locEl) {
1074+
sessionStorage.setItem('gcovr-functions-clicked', nameEl.textContent + '|' + locEl.textContent);
1075+
}
1076+
sessionStorage.setItem('gcovr-functions-scrollTop', String(container.scrollTop));
1077+
});
1078+
1079+
// --- Data-level sorting ---
1080+
function sortData(key, ascending) {
1081+
data.sort(function(a, b) {
1082+
var aVal, bVal;
1083+
switch (key) {
1084+
case 'name': aVal = a.name; bVal = b.name; break;
1085+
case 'calls': aVal = a.excluded ? -1 : a.execution_count; bVal = b.excluded ? -1 : b.execution_count; break;
1086+
case 'lines': aVal = parseFloat(a.line_coverage) || 0; bVal = parseFloat(b.line_coverage) || 0; break;
1087+
case 'conditions': aVal = parseFloat(a.condition_coverage) || 0; bVal = parseFloat(b.condition_coverage) || 0; break;
1088+
default: aVal = a.name; bVal = b.name;
1089+
}
1090+
if (typeof aVal === 'string' && typeof bVal === 'string') {
1091+
return ascending ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
1092+
}
1093+
return ascending ? aVal - bVal : bVal - aVal;
1094+
});
1095+
lastStartIdx = -1; // force re-render
1096+
viewport.style.height = (data.length * ROW_HEIGHT) + 'px';
1097+
renderVisible();
1098+
}
1099+
1100+
// Intercept sort clicks on functions-header before initSorting runs
1101+
var funcHeaders = document.querySelectorAll('.functions-header .sortable');
1102+
funcHeaders.forEach(function(header) {
1103+
header.addEventListener('click', function(e) {
1104+
e.stopPropagation();
1105+
var sortKey = this.dataset.sort;
1106+
var isAscending = this.classList.contains('sorted-ascending');
1107+
1108+
// Update header classes
1109+
funcHeaders.forEach(function(h) {
1110+
h.classList.remove('sorted-ascending', 'sorted-descending');
1111+
});
1112+
this.classList.add(isAscending ? 'sorted-descending' : 'sorted-ascending');
1113+
1114+
sortData(sortKey, !isAscending);
1115+
}, true); // capture phase to beat initSorting
1116+
});
1117+
1118+
// --- Restore saved state (scroll + highlight) ---
1119+
function restoreSavedState() {
1120+
var saved = sessionStorage.getItem('gcovr-functions-clicked');
1121+
if (saved !== null) {
1122+
sessionStorage.removeItem('gcovr-functions-clicked');
1123+
highlightKey = saved;
1124+
}
1125+
var scroll = sessionStorage.getItem('gcovr-functions-scrollTop');
1126+
if (scroll !== null) {
1127+
sessionStorage.removeItem('gcovr-functions-scrollTop');
1128+
container.scrollTop = parseInt(scroll, 10);
1129+
}
1130+
if (saved !== null || scroll !== null) {
1131+
lastStartIdx = -1;
1132+
renderVisible();
1133+
}
1134+
}
1135+
1136+
// --- Initialize ---
1137+
data.sort(function(a, b) { return a.name.localeCompare(b.name); });
1138+
1139+
setupVirtualScroll();
1140+
renderVisible();
1141+
restoreSavedState();
1142+
1143+
// Also restore on bfcache navigation (browser Back button)
1144+
window.addEventListener('pageshow', function(e) {
1145+
if (e.persisted) restoreSavedState();
1146+
});
1147+
1148+
// Mark functions page so initSorting can skip it
1149+
container.dataset.virtualScroll = 'true';
1150+
}
1151+
1152+
1153+
9321154
// ===========================================
9331155
// Sorting
9341156
// ===========================================
@@ -961,6 +1183,8 @@
9611183
function sortList(key, ascending) {
9621184
const container = document.getElementById('file-list') || document.querySelector('.functions-body');
9631185
if (!container) return;
1186+
// Virtual scroll handles its own sorting
1187+
if (container.dataset.virtualScroll) return;
9641188

9651189
const rows = Array.from(container.children);
9661190

0 commit comments

Comments
 (0)