Skip to content

Commit 2fee04a

Browse files
Merge pull request #751 from lukecotter/feat-highlights-api
perf: migrate find highlights to CSS Highlight API
2 parents e0f3108 + b5dc58f commit 2fee04a

14 files changed

Lines changed: 667 additions & 343 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7979

8080
### Changed
8181

82-
- 🎯 **Call Tree Go To**: Go-to links in call tree now navigate to method definition instead of where method was called from ([#632] [#200])
82+
- 🎯 **Call Tree Go To**: Go-to links in call tree now navigate to method definition instead of where method was called from. ([#632] [#200])
8383
- 🔍 **Search Navigation**: `Shift+Enter` navigates to the previous search result; hold `Enter` to continuously navigate.
84-
-**Log Parsing**: Improved performance ([#552])
85-
-**Duration Formatting**: Human-readable duration formatting in tooltips (30000 ms -> 30s and 0.01 ms -> 10 µs) ([#671])
86-
- 🎯 **Number Precision**: Total and Self Time column precision changed to 2 decimal places for improved readability ([#671])
87-
- 🎨 **Navigation Bar**: Redesigned to better match VS Code’s look and feel ([#694])
84+
-**Search Performance**: Up to 10x faster search on large logs. ([#627])
85+
-**Log Parsing**: Improved performance. ([#552])
86+
-**Duration Formatting**: Human-readable duration formatting in tooltips (30000 ms -> 30s and 0.01 ms -> 10 µs). ([#671])
87+
- 🎯 **Number Precision**: Total and Self Time column precision changed to 2 decimal places for improved readability. ([#671])
88+
- 🎨 **Navigation Bar**: Redesigned to better match VS Code’s look and feel. ([#694])
8889

8990
## [1.18.1] 2025-07-09
9091

@@ -476,6 +477,7 @@ Skipped due to adopting odd numbering for pre releases and even number for relea
476477

477478
<!-- Unreleased -->
478479

480+
[#627]: https://github.com/certinia/debug-log-analyzer/issues/627
479481
[#685]: https://github.com/certinia/debug-log-analyzer/issues/685
480482
[#98]: https://github.com/certinia/debug-log-analyzer/issues/98
481483
[#204]: https://github.com/certinia/debug-log-analyzer/issues/204

log-viewer/src/features/analysis/components/AnalysisView.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { progressFormatterMS } from '../../../tabulator/format/ProgressMS.js';
2424
import { GroupCalcs } from '../../../tabulator/groups/GroupCalcs.js';
2525
import { GroupSort } from '../../../tabulator/groups/GroupSort.js';
2626
import * as CommonModules from '../../../tabulator/module/CommonModules.js';
27-
import { Find, formatter } from '../../../tabulator/module/Find.js';
27+
import { Find } from '../../../tabulator/module/Find.js';
2828
import { RowKeyboardNavigation } from '../../../tabulator/module/RowKeyboardNavigation.js';
2929
import { RowNavigation } from '../../../tabulator/module/RowNavigation.js';
3030
import dataGridStyles from '../../../tabulator/style/DataGrid.scss';
@@ -126,6 +126,13 @@ export class AnalysisView extends LitElement {
126126
document.addEventListener('lv-find-close', this._findEvt);
127127
}
128128

129+
disconnectedCallback(): void {
130+
super.disconnectedCallback();
131+
document.removeEventListener('lv-find', this._findEvt);
132+
document.removeEventListener('lv-find-match', this._findEvt);
133+
document.removeEventListener('lv-find-close', this._findEvt);
134+
}
135+
129136
updated(changedProperties: PropertyValues): void {
130137
if (
131138
this.timelineRoot &&
@@ -240,30 +247,23 @@ export class AnalysisView extends LitElement {
240247
this.totalMatches = result.totalMatches;
241248
this.findMap = result.matchIndexes;
242249

243-
if (!clearHighlights) {
250+
if (!clearHighlights && isTableVisible) {
244251
document.dispatchEvent(
245252
new CustomEvent('lv-find-results', { detail: { totalMatches: result.totalMatches } }),
246253
);
247254
}
248255
}
249256

250-
if (this.totalMatches <= 0) {
257+
if (this.totalMatches <= 0 || !isTableVisible) {
251258
return;
252259
}
253260
this.blockClearHighlights = true;
254-
this.analysisTable?.blockRedraw();
255261
const currentRow = this.findMap[this.findArgs.count];
256-
const rows = [
257-
currentRow,
258-
this.findMap[this.findArgs.count + 1],
259-
this.findMap[this.findArgs.count - 1],
260-
];
261-
rows.forEach((row) => {
262-
row?.reformat();
262+
//@ts-expect-error This is a custom function added in by Find custom module
263+
await this.analysisTable.setCurrentMatch(this.findArgs.count, currentRow, {
264+
scrollIfVisible: false,
265+
focusRow: false,
263266
});
264-
//@ts-expect-error This is a custom function added in by RowNavigation custom module
265-
this.analysisTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false });
266-
this.analysisTable?.restoreRedraw();
267267
this.blockClearHighlights = false;
268268
}
269269

@@ -321,9 +321,6 @@ export class AnalysisView extends LitElement {
321321
groupClosedShowCalcs: true,
322322
groupStartOpen: false,
323323
groupToggleElement: 'header',
324-
rowFormatter: (row: RowComponent) => {
325-
formatter(row, this.findArgs);
326-
},
327324
columnDefaults: {
328325
title: 'default',
329326
resizable: true,
@@ -426,7 +423,21 @@ export class AnalysisView extends LitElement {
426423
],
427424
});
428425

429-
this.analysisTable.on('renderStarted', () => {
426+
this.analysisTable.on('dataSorted', () => {
427+
if (!this.blockClearHighlights && this.totalMatches > 0) {
428+
this._resetFindWidget();
429+
this._clearSearchHighlights();
430+
}
431+
});
432+
433+
this.analysisTable.on('dataFiltered', () => {
434+
if (!this.blockClearHighlights && this.totalMatches > 0) {
435+
this._resetFindWidget();
436+
this._clearSearchHighlights();
437+
}
438+
});
439+
440+
this.analysisTable.on('dataGrouped', () => {
430441
if (!this.blockClearHighlights && this.totalMatches > 0) {
431442
this._resetFindWidget();
432443
this._clearSearchHighlights();
@@ -442,7 +453,7 @@ export class AnalysisView extends LitElement {
442453
this.findArgs.text = '';
443454
this.findArgs.count = 0;
444455
//@ts-expect-error This is a custom function added in by Find custom module
445-
this.analysisTable.clearFindHighlights(Object.values(this.findMap));
456+
this.analysisTable.clearFindHighlights();
446457
this.findMap = {};
447458
this.totalMatches = 0;
448459
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2022 Certinia Inc. All rights reserved.
3+
*/
4+
import type { LogEvent, LogEventType } from 'apex-log-parser';
5+
import type { CellComponent, EmptyCallback } from 'tabulator-tables';
6+
7+
export function createCalltreeNameFormatter(excludedTypes: Set<LogEventType>) {
8+
let childIndent: number;
9+
10+
return function calltreeNameFormatter(
11+
cell: CellComponent,
12+
_formatterParams: object,
13+
_onRendered: EmptyCallback,
14+
): string | HTMLElement {
15+
const data = cell.getData() as { originalData: LogEvent; treeLevel: number };
16+
const { originalData: node, treeLevel } = data;
17+
// @ts-expect-error this.table is added by tabulator when the formatter is called, but isn't in the types for some reason
18+
childIndent ??= this.table.options.dataTreeChildIndent ?? 9;
19+
const levelIndent = treeLevel * childIndent;
20+
21+
const cellElem = cell.getElement();
22+
cellElem.style.paddingLeft = `${levelIndent + 4}px`;
23+
cellElem.style.textIndent = `-${levelIndent}px`;
24+
25+
if (node.hasValidSymbols) {
26+
const link = document.createElement('a');
27+
link.setAttribute('href', '#!');
28+
link.textContent = node.text;
29+
return link;
30+
}
31+
32+
let text = node.text;
33+
if (node.type && node.type !== text && !excludedTypes.has(node.type)) {
34+
text = node.type + ': ' + text;
35+
}
36+
37+
return document.createTextNode(text) as unknown as HTMLElement;
38+
};
39+
}

log-viewer/src/features/call-tree/components/CalltreeView.ts

Lines changed: 36 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import MinMaxFilter from '../../../tabulator/filters/MinMax.js';
2424
import { progressFormatter } from '../../../tabulator/format/Progress.js';
2525
import { progressFormatterMS } from '../../../tabulator/format/ProgressMS.js';
2626
import * as CommonModules from '../../../tabulator/module/CommonModules.js';
27-
import { Find, formatter } from '../../../tabulator/module/Find.js';
27+
import { Find } from '../../../tabulator/module/Find.js';
2828
import { MiddleRowFocus } from '../../../tabulator/module/MiddleRowFocus.js';
2929
import { RowKeyboardNavigation } from '../../../tabulator/module/RowKeyboardNavigation.js';
3030
import { RowNavigation } from '../../../tabulator/module/RowNavigation.js';
3131
import dataGridStyles from '../../../tabulator/style/DataGrid.scss';
32+
import { createCalltreeNameFormatter } from './CalltreeNameFormatter.js';
3233

3334
// styles
3435
import { globalStyles } from '../../../styles/global.styles.js';
@@ -79,18 +80,27 @@ export class CalltreeView extends LitElement {
7980
return (this.tableContainer = this.renderRoot?.querySelector('#call-tree-table') ?? null);
8081
}
8182

83+
private _goToRowEvt = ((e: CustomEvent) => {
84+
this._goToRow(e.detail.timestamp);
85+
}) as EventListener;
86+
8287
constructor() {
8388
super();
8489

85-
document.addEventListener('calltree-go-to-row', ((e: CustomEvent) => {
86-
this._goToRow(e.detail.timestamp);
87-
}) as EventListener);
88-
90+
document.addEventListener('calltree-go-to-row', this._goToRowEvt);
8991
document.addEventListener('lv-find', this._findEvt);
9092
document.addEventListener('lv-find-match', this._findEvt);
9193
document.addEventListener('lv-find-close', this._findEvt);
9294
}
9395

96+
disconnectedCallback(): void {
97+
super.disconnectedCallback();
98+
document.removeEventListener('calltree-go-to-row', this._goToRowEvt);
99+
document.removeEventListener('lv-find', this._findEvt);
100+
document.removeEventListener('lv-find-match', this._findEvt);
101+
document.removeEventListener('lv-find-close', this._findEvt);
102+
}
103+
94104
updated(changedProperties: PropertyValues): void {
95105
if (
96106
this.timelineRoot &&
@@ -385,34 +395,24 @@ export class CalltreeView extends LitElement {
385395
this.totalMatches = result.totalMatches;
386396
this.findMap = result.matchIndexes;
387397

388-
if (!clearHighlights) {
398+
if (!clearHighlights && isTableVisible) {
389399
document.dispatchEvent(
390400
new CustomEvent('lv-find-results', { detail: { totalMatches: result.totalMatches } }),
391401
);
392402
}
393403
}
394404

395405
// Highlight the current row and reset the previous or next depending on whether we are stepping forward or back.
396-
if (this.totalMatches <= 0) {
406+
if (this.totalMatches <= 0 || !isTableVisible) {
397407
return;
398408
}
399409
this.blockClearHighlights = true;
400-
this.calltreeTable?.blockRedraw();
401410
const currentRow = this.findMap[this.findArgs.count];
402-
const rows = [
403-
currentRow,
404-
this.findMap[this.findArgs.count + 1],
405-
this.findMap[this.findArgs.count - 1],
406-
];
407-
rows.forEach((row) => {
408-
row?.reformat();
411+
//@ts-expect-error This is a custom function added in by Find custom module
412+
await this.calltreeTable.setCurrentMatch(this.findArgs.count, currentRow, {
413+
scrollIfVisible: false,
414+
focusRow: false,
409415
});
410-
411-
if (currentRow) {
412-
//@ts-expect-error This is a custom function added in by RowNavigation custom module
413-
this.calltreeTable.goToRow(currentRow, { scrollIfVisible: false, focusRow: false });
414-
}
415-
this.calltreeTable?.restoreRedraw();
416416
this.blockClearHighlights = false;
417417
}
418418

@@ -580,7 +580,7 @@ export class CalltreeView extends LitElement {
580580
const excludedTypes = new Set<LogEventType>(['SOQL_EXECUTE_BEGIN', 'DML_BEGIN']);
581581
const governorLimits = rootMethod.governorLimits;
582582

583-
let childIndent;
583+
const nameFormatter = createCalltreeNameFormatter(excludedTypes);
584584
this.calltreeTable = new Tabulator(callTreeTableContainer, {
585585
data: this._toCallTree(rootMethod.children),
586586
layout: 'fitColumns',
@@ -610,9 +610,6 @@ export class CalltreeView extends LitElement {
610610
return "<div class='sort-by'><div class='sort-by--top'></div><div class='sort-by--bottom'></div></div>";
611611
}
612612
},
613-
rowFormatter: (row: RowComponent) => {
614-
formatter(row, this.findArgs);
615-
},
616613
columnCalcs: 'both',
617614
columnDefaults: {
618615
title: 'default',
@@ -630,34 +627,7 @@ export class CalltreeView extends LitElement {
630627
return 'Total';
631628
},
632629
cssClass: 'datagrid-textarea datagrid-code-text',
633-
formatter: (cell, _formatterParams, _onRendered) => {
634-
const cellElem = cell.getElement();
635-
const row = cell.getRow();
636-
// @ts-expect-error: _row is private. This is temporary and I will patch the text wrap behaviour in the library.
637-
const dataTree = row._row.modules.dataTree;
638-
const treeLevel = dataTree?.index ?? 0;
639-
childIndent ??= row.getTable().options.dataTreeChildIndent || 0;
640-
const levelIndent = treeLevel * childIndent;
641-
cellElem.style.paddingLeft = `${levelIndent + 4}px`;
642-
cellElem.style.textIndent = `-${levelIndent}px`;
643-
644-
const node = (cell.getData() as CalltreeRow).originalData;
645-
let text = node.text;
646-
if (node.hasValidSymbols) {
647-
const link = document.createElement('a');
648-
link.setAttribute('href', '#!');
649-
link.textContent = text;
650-
return link;
651-
}
652-
653-
if (node.type && !excludedTypes.has(node.type) && node.type !== text) {
654-
text = node.type + ': ' + text;
655-
}
656-
657-
const textSpan = document.createElement('span');
658-
textSpan.textContent = text;
659-
return textSpan;
660-
},
630+
formatter: nameFormatter,
661631
variableHeight: true,
662632
cellClick: (e, cell) => {
663633
const { type } = window.getSelection() ?? {};
@@ -867,7 +837,14 @@ export class CalltreeView extends LitElement {
867837
this.typeFilterCache.clear();
868838
});
869839

870-
this.calltreeTable.on('renderStarted', () => {
840+
this.calltreeTable.on('dataSorted', () => {
841+
if (!this.blockClearHighlights && this.totalMatches > 0) {
842+
this._resetFindWidget();
843+
this._clearSearchHighlights();
844+
}
845+
});
846+
847+
this.calltreeTable.on('dataFiltered', () => {
871848
if (!this.blockClearHighlights && this.totalMatches > 0) {
872849
this._resetFindWidget();
873850
this._clearSearchHighlights();
@@ -899,7 +876,7 @@ export class CalltreeView extends LitElement {
899876
this.findArgs.text = '';
900877
this.findArgs.count = 0;
901878
//@ts-expect-error This is a custom function added in by Find custom module
902-
this.calltreeTable.clearFindHighlights(Object.values(this.findMap));
879+
this.calltreeTable.clearFindHighlights();
903880
this.findMap = {};
904881
this.totalMatches = 0;
905882
}
@@ -983,7 +960,7 @@ export class CalltreeView extends LitElement {
983960
}
984961
}
985962

986-
private _toCallTree(nodes: LogEvent[]): CalltreeRow[] | undefined {
963+
private _toCallTree(nodes: LogEvent[], treeLevel = 0): CalltreeRow[] | undefined {
987964
const len = nodes.length;
988965
if (!len) {
989966
return undefined;
@@ -995,12 +972,13 @@ export class CalltreeView extends LitElement {
995972
if (!node) {
996973
continue;
997974
}
998-
const children = node.children.length ? this._toCallTree(node.children) : null;
975+
const children = node.children.length ? this._toCallTree(node.children, treeLevel + 1) : null;
999976
results.push({
1000977
id: node.timestamp + '-' + i,
1001978
originalData: node,
1002979
_children: children,
1003980
text: node.text,
981+
treeLevel,
1004982
namespace: node.namespace,
1005983
duration: node.duration,
1006984
dmlCount: node.dmlCount,
@@ -1076,6 +1054,7 @@ interface CalltreeRow {
10761054
originalData: LogEvent;
10771055
_children: CalltreeRow[] | undefined | null;
10781056
text: string;
1057+
treeLevel: number;
10791058
duration: CountTotals;
10801059
namespace: string;
10811060
dmlCount: CountTotals;

0 commit comments

Comments
 (0)