diff --git a/tools/reccmp/reccmp.js b/tools/reccmp/reccmp.js
index 1323ca10..4a8e6595 100644
--- a/tools/reccmp/reccmp.js
+++ b/tools/reccmp/reccmp.js
@@ -61,6 +61,46 @@ function formatAsm(entries, addrOption) {
return output;
}
+// Special internal values to ensure this sort order for matching column:
+// 1. Stub
+// 2. Any match percentage [0.0, 1.0)
+// 3. Effective match
+// 4. Actual 100% match
+function matchingColAdjustment(row) {
+ if ('stub' in row) {
+ return -1;
+ }
+
+ if ('effective' in row) {
+ return 1.0;
+ }
+
+ if (row.matching === 1.0) {
+ return 1000;
+ }
+
+ return row.matching;
+}
+
+function getCppClass(str) {
+ const idx = str.indexOf('::');
+ if (idx !== -1) {
+ return str.slice(0, idx);
+ }
+
+ return str;
+}
+
+// Clamp string length to specified length and pad with ellipsis
+function stringTruncate(str, maxlen = 20) {
+ str = getCppClass(str);
+ if (str.length > maxlen) {
+ return `${str.slice(0, maxlen)}...`;
+ }
+
+ return str;
+}
+
function getMatchPercentText(row) {
if ('stub' in row) {
return 'stub';
@@ -73,6 +113,18 @@ function getMatchPercentText(row) {
return (row.matching * 100).toFixed(2) + '%';
}
+function countDiffs(row) {
+ const { diff = '' } = row;
+ if (diff === '') {
+ return '';
+ }
+
+ const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
+ const diffLength = diffs.filter(d => !('both' in d)).length;
+ const diffWord = diffLength === 1 ? 'diff' : 'diffs';
+ return diffLength === 0 ? '' : `${diffLength} ${diffWord}`;
+}
+
// Helper for this set/remove attribute block
function setBooleanAttribute(element, attribute, value) {
if (value) {
@@ -82,6 +134,12 @@ function setBooleanAttribute(element, attribute, value) {
}
}
+function copyToClipboard(value) {
+ navigator.clipboard.writeText(value);
+}
+
+const PAGE_SIZE = 200;
+
//
// Global state
//
@@ -91,9 +149,184 @@ class ListingState {
this._query = '';
this._sortCol = 'address';
this._filterType = 1;
- this.sortDesc = false;
- this.hidePerfect = false;
- this.hideStub = false;
+ this._sortDesc = false;
+ this._hidePerfect = false;
+ this._hideStub = false;
+ this._showRecomp = false;
+ this._expanded = {};
+ this._page = 0;
+
+ this._listeners = [];
+
+ this._results = [];
+ this.updateResults();
+ }
+
+ addListener(fn) {
+ this._listeners.push(fn);
+ }
+
+ callListeners() {
+ for (const fn of this._listeners) {
+ fn();
+ }
+ }
+
+ isExpanded(addr) {
+ return addr in this._expanded;
+ }
+
+ toggleExpanded(addr) {
+ this.setExpanded(addr, !this.isExpanded(addr));
+ }
+
+ setExpanded(addr, value) {
+ if (value) {
+ this._expanded[addr] = true;
+ } else {
+ delete this._expanded[addr];
+ }
+ }
+
+ updateResults() {
+ const filterFn = this.rowFilterFn.bind(this);
+ const sortFn = this.rowSortFn.bind(this);
+
+ this._results = data.filter(filterFn).sort(sortFn);
+
+ // Set _page directly to avoid double call to listeners.
+ this._page = this.pageClamp(this.page);
+ this.callListeners();
+ }
+
+ pageSlice() {
+ return this._results.slice(this.page * PAGE_SIZE, (this.page + 1) * PAGE_SIZE);
+ }
+
+ resultsCount() {
+ return this._results.length;
+ }
+
+ pageCount() {
+ return Math.ceil(this._results.length / PAGE_SIZE);
+ }
+
+ maxPage() {
+ return Math.max(0, this.pageCount() - 1);
+ }
+
+ // A list showing the range of each page based on the sort column and direction.
+ pageHeadings() {
+ if (this._results.length === 0) {
+ return [];
+ }
+
+ const headings = [];
+
+ for (let i = 0; i < this.pageCount(); i++) {
+ const startIdx = i * PAGE_SIZE;
+ const endIdx = Math.min(this._results.length, ((i + 1) * PAGE_SIZE)) - 1;
+
+ let start = this._results[startIdx][this.sortCol];
+ let end = this._results[endIdx][this.sortCol];
+
+ if (this.sortCol === 'matching') {
+ start = getMatchPercentText(this._results[startIdx]);
+ end = getMatchPercentText(this._results[endIdx]);
+ }
+
+ headings.push([i, stringTruncate(start), stringTruncate(end)]);
+ }
+
+ return headings;
+ }
+
+ rowFilterFn(row) {
+ // Destructuring sets defaults for optional values from this object.
+ const {
+ effective = false,
+ stub = false,
+ diff = '',
+ name,
+ address,
+ matching
+ } = row;
+
+ if (this.hidePerfect && (effective || matching >= 1)) {
+ return false;
+ }
+
+ if (this.hideStub && stub) {
+ return false;
+ }
+
+ if (this.query === '') {
+ return true;
+ }
+
+ // Name/addr search
+ if (this.filterType === 1) {
+ return (
+ address.includes(this.query) ||
+ name.toLowerCase().includes(this.query)
+ );
+ }
+
+ // no diff for review.
+ if (diff === '') {
+ return false;
+ }
+
+ // special matcher for combined diff
+ const anyLineMatch = ([addr, line]) => line.toLowerCase().trim().includes(this.query);
+
+ // Flatten all diff groups for the search
+ const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
+ for (const subgroup of diffs) {
+ const { both = [], orig = [], recomp = [] } = subgroup;
+
+ // If search includes context
+ if (this.filterType === 2 && both.some(anyLineMatch)) {
+ return true;
+ }
+
+ if (orig.some(anyLineMatch) || recomp.some(anyLineMatch)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ rowSortFn(rowA, rowB) {
+ const valA = this.sortCol === 'matching'
+ ? matchingColAdjustment(rowA)
+ : rowA[this.sortCol];
+
+ const valB = this.sortCol === 'matching'
+ ? matchingColAdjustment(rowB)
+ : rowB[this.sortCol];
+
+ if (valA > valB) {
+ return this.sortDesc ? -1 : 1;
+ } else if (valA < valB) {
+ return this.sortDesc ? 1 : -1;
+ }
+
+ return 0;
+ }
+
+ pageClamp(page) {
+ return Math.max(0, Math.min(page, this.maxPage()));
+ }
+
+ get page() {
+ return this._page;
+ }
+
+ set page(page) {
+ this._page = this.pageClamp(page);
+ this.callListeners();
}
get filterType() {
@@ -105,6 +338,7 @@ class ListingState {
if (value >= 1 && value <= 3) {
this._filterType = value;
}
+ this.updateResults();
}
get query() {
@@ -114,6 +348,21 @@ class ListingState {
set query(value) {
// Normalize search string
this._query = value.toLowerCase().trim();
+ this.updateResults();
+ }
+
+ get showRecomp() {
+ return this._showRecomp;
+ }
+
+ set showRecomp(value) {
+ // Don't sort by the recomp column we are about to hide
+ if (!value && this.sortCol === 'recomp') {
+ this._sortCol = 'address';
+ }
+
+ this._showRecomp = value;
+ this.callListeners();
}
get sortCol() {
@@ -122,117 +371,43 @@ class ListingState {
set sortCol(column) {
if (column === this._sortCol) {
- this.sortDesc = !this.sortDesc;
+ this._sortDesc = !this._sortDesc;
} else {
this._sortCol = column;
}
+
+ this.updateResults();
+ }
+
+ get sortDesc() {
+ return this._sortDesc;
+ }
+
+ set sortDesc(value) {
+ this._sortDesc = value;
+ this.updateResults();
+ }
+
+ get hidePerfect() {
+ return this._hidePerfect;
+ }
+
+ set hidePerfect(value) {
+ this._hidePerfect = value;
+ this.updateResults();
+ }
+
+ get hideStub() {
+ return this._hideStub;
+ }
+
+ set hideStub(value) {
+ this._hideStub = value;
+ this.updateResults();
}
}
-const StateProxy = {
- set(obj, prop, value) {
- if (prop === 'onsort') {
- this._onsort = value;
- return true;
- }
-
- if (prop === 'onfilter') {
- this._onfilter = value;
- return true;
- }
-
- obj[prop] = value;
-
- if (prop === 'sortCol' || prop === 'sortDesc') {
- this._onsort();
- } else {
- this._onfilter();
- }
- return true;
- }
-};
-
-const appState = new Proxy(new ListingState(), StateProxy);
-
-//
-// Stateful functions
-//
-
-function addrShouldAppear(addr) {
- // Destructuring sets defaults for optional values from this object.
- const {
- effective = false,
- stub = false,
- diff = '',
- name,
- address,
- matching
- } = getDataByAddr(addr);
-
- if (appState.hidePerfect && (effective || matching >= 1)) {
- return false;
- }
-
- if (appState.hideStub && stub) {
- return false;
- }
-
- if (appState.query === '') {
- return true;
- }
-
- // Name/addr search
- if (appState.filterType === 1) {
- return (
- address.includes(appState.query) ||
- name.toLowerCase().includes(appState.query)
- );
- }
-
- // no diff for review.
- if (diff === '') {
- return false;
- }
-
- // special matcher for combined diff
- const anyLineMatch = ([addr, line]) => line.toLowerCase().trim().includes(appState.query);
-
- // Flatten all diff groups for the search
- const diffs = diff.map(([slug, subgroups]) => subgroups).flat();
- for (const subgroup of diffs) {
- const { both = [], orig = [], recomp = [] } = subgroup;
-
- // If search includes context
- if (appState.filterType === 2 && both.some(anyLineMatch)) {
- return true;
- }
-
- if (orig.some(anyLineMatch) || recomp.some(anyLineMatch)) {
- return true;
- }
- }
-
- return false;
-}
-
-// Row comparator function, using our chosen sort column and direction.
-// -1 (A before B)
-// 1 (B before A)
-// 0 (equal)
-function rowSortOrder(addrA, addrB) {
- const objA = getDataByAddr(addrA);
- const objB = getDataByAddr(addrB);
- const valA = objA[appState.sortCol];
- const valB = objB[appState.sortCol];
-
- if (valA > valB) {
- return appState.sortDesc ? -1 : 1;
- } else if (valA < valB) {
- return appState.sortDesc ? 1 : -1;
- }
-
- return 0;
-}
+const appState = new ListingState();
//
// Custom elements
@@ -244,7 +419,8 @@ class SortIndicator extends window.HTMLElement {
attributeChangedCallback(name, oldValue, newValue) {
if (newValue === null) {
- this.textContent = '';
+ // Reserve space for blank indicator so column width stays the same
+ this.innerHTML = ' ';
} else {
this.innerHTML = newValue === 'asc' ? '▲' : '▼';
}
@@ -252,14 +428,6 @@ class SortIndicator extends window.HTMLElement {
}
class FuncRow extends window.HTMLElement {
- static observedAttributes = ['expanded'];
-
- constructor() {
- super();
-
- this.onclick = evt => (this.expanded = !this.expanded);
- }
-
connectedCallback() {
if (this.shadowRoot !== null) {
return;
@@ -268,29 +436,14 @@ class FuncRow extends window.HTMLElement {
const template = document.querySelector('template#funcrow-template').content;
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.cloneNode(true));
+ shadow.querySelector(':host > div[data-col="name"]').addEventListener('click', evt => {
+ this.dispatchEvent(new Event('name-click'));
+ });
}
get address() {
return this.getAttribute('data-address');
}
-
- get expanded() {
- return this.getAttribute('expanded') !== null;
- }
-
- set expanded(value) {
- setBooleanAttribute(this, 'expanded', value);
- }
-
- attributeChangedCallback(name, oldValue, newValue) {
- if (name !== 'expanded') {
- return;
- }
-
- if (this.onchangeExpand) {
- this.onchangeExpand(this.expanded);
- }
- }
}
class NoDiffMessage extends window.HTMLElement {
@@ -305,6 +458,36 @@ class NoDiffMessage extends window.HTMLElement {
}
}
+class CanCopy extends window.HTMLElement {
+ connectedCallback() {
+ if (this.shadowRoot !== null) {
+ return;
+ }
+
+ const template = document.querySelector('template#can-copy-template').content;
+ const shadow = this.attachShadow({ mode: 'open' });
+ shadow.appendChild(template.cloneNode(true));
+
+ const el = shadow.querySelector('slot').assignedNodes()[0];
+ el.addEventListener('mouseout', evt => { this.copied = false; });
+ el.addEventListener('click', evt => {
+ copyToClipboard(evt.target.textContent);
+ this.copied = true;
+ });
+ }
+
+ get copied() {
+ return this.getAttribute('copied');
+ }
+
+ set copied(value) {
+ if (value) {
+ setTimeout(() => { this.copied = false; }, 2000);
+ }
+ setBooleanAttribute(this, 'copied', value);
+ }
+}
+
// Displays asm diff for the given @data-address value.
class DiffRow extends window.HTMLElement {
connectedCallback() {
@@ -347,6 +530,10 @@ class DiffDisplayOptions extends window.HTMLElement {
margin-right: 10px;
user-select: none;
}
+
+ label, input {
+ cursor: pointer;
+ }