void,
): ItemData => {
- const leftSymbol = left ? display.displaySymbol(result.left!, left) : null;
- const rightSymbol = right
- ? display.displaySymbol(result.right!, right)
- : null;
const itemCount = Math.max(
leftSymbol?.rowCount || 0,
rightSymbol?.rowCount || 0,
);
- const symbolName = leftSymbol?.name || rightSymbol?.name || '';
+ const symbolName = leftSymbol?.info.name || rightSymbol?.info.name || '';
const config = buildDiffConfig(null);
const matchPercent = rightSymbol?.matchPercent;
return {
@@ -300,9 +304,7 @@ const createItemData = memoizeOne(
result,
config,
matchPercent,
- left,
leftSymbol,
- right,
rightSymbol,
highlight,
setHighlight,
@@ -322,7 +324,7 @@ const SymbolLabel = ({
);
}
- const displayName = symbol.demangledName || symbol.name;
+ const displayName = symbol.info.demangledName || symbol.info.name;
return (
{
- const { buildRunning, currentUnit, lastBuilt, hasProjectConfig } =
- useExtensionStore(
- useShallow((state) => ({
- buildRunning: state.buildRunning,
- currentUnit: state.currentUnit,
- lastBuilt: state.lastBuilt,
- hasProjectConfig: state.projectConfig != null,
- })),
- );
+ const currentUnit = useExtensionStore((state) => state.currentUnit);
+ const { highlight, setSymbolScrollOffset, setHighlight } = useAppStore(
+ useShallow((state) => ({
+ highlight: state.highlight,
+ setSymbolScrollOffset: state.setSymbolScrollOffset,
+ setHighlight: state.setHighlight,
+ })),
+ );
+ const itemData = createItemData(
+ diff,
+ leftSymbol,
+ rightSymbol,
+ highlight,
+ setHighlight,
+ );
const currentUnitName = currentUnit?.name || '';
- const { highlight, setSelectedSymbol, setSymbolScrollOffset, setHighlight } =
- useAppStore(
- useShallow((state) => ({
- highlight: state.highlight,
- setSelectedSymbol: state.setSelectedSymbol,
- setSymbolScrollOffset: state.setSymbolScrollOffset,
- setHighlight: state.setHighlight,
- })),
- );
-
- const itemData = createItemData(diff, left, right, highlight, setHighlight);
const initialScrollOffset = useMemo(
() =>
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
@@ -370,140 +371,25 @@ const FunctionView = ({
] || 0,
[currentUnitName, itemData.symbolName],
);
-
const itemSize = useFontSize() * 1.33;
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
- {hasProjectConfig && (
-
- )}
- {lastBuilt && (
-
- Last built: {new Date(lastBuilt).toLocaleTimeString('en-US')}
-
- )}
-
-
- {itemData.matchPercent !== undefined && (
- <>
-
- {Math.floor(itemData.matchPercent).toFixed(0)}%
-
- {' | '}
- >
- )}
-
-
-
-
-
-
{
- let obj: diff.ObjectDiff | undefined;
- let symbol: display.SectionDisplaySymbol | undefined;
- switch (data.column) {
- case 0:
- obj = diff.left;
- symbol = itemData.left ?? undefined;
- break;
- case 1:
- obj = diff.right;
- symbol = itemData.right ?? undefined;
- break;
- default:
- break;
- }
- if (!obj || !symbol) {
- return null;
- }
- const items = display.instructionContext(
- obj,
- symbol,
- data.row,
- itemData.config,
- );
- return renderContextItems(items, close);
- }}
- >
-
- {({ height, width }) => (
- {
- setSymbolScrollOffset(
- currentUnitName,
- itemData.symbolName,
- e.scrollOffset,
- );
- }}
- initialScrollOffset={initialScrollOffset}
- >
- {AsmRow}
-
- )}
-
-
-
- {
- const data: InstructionTooltipContent = JSON.parse(content);
- let obj: diff.ObjectDiff | undefined;
- let symbol: display.SectionDisplaySymbol | undefined;
- switch (data.column) {
- case 0:
- obj = diff.left;
- symbol = itemData.left ?? undefined;
- break;
- case 1:
- obj = diff.right;
- symbol = itemData.right ?? undefined;
- break;
- default:
- break;
- }
- if (!obj || !symbol) {
- return null;
- }
- return display.instructionHover(
- obj,
- symbol,
- data.row,
- itemData.config,
- );
- }}
- />
- >
+ {
+ setSymbolScrollOffset(
+ currentUnitName,
+ itemData.symbolName,
+ e.scrollOffset,
+ );
+ }}
+ initialScrollOffset={initialScrollOffset}
+ >
+ {AsmRow}
+
);
};
-
-export default FunctionView;
diff --git a/webview/views/SymbolsView.module.css b/webview/views/SymbolsView.module.css
index 76098d8..f596446 100644
--- a/webview/views/SymbolsView.module.css
+++ b/webview/views/SymbolsView.module.css
@@ -11,41 +11,6 @@
padding: 0;
}
-.symbol-list-row {
- cursor: pointer;
- user-select: none;
- height: var(--list-row-height);
-
- font-family: var(--code-font-family);
- font-weight: var(--code-font-weight);
- font-size: var(--code-font-size);
- font-variant-ligatures: var(--code-font-variant-ligatures);
- text-wrap: nowrap;
- white-space: pre;
-
- &:hover {
- background-color: var(--list-row-hover-background);
- }
-}
-
-.section {
- padding-left: 0.5em;
-
- &::before {
- content: "▼ ";
- }
- &.collapsed {
- opacity: 0.75;
- &::before {
- content: "▶ ";
- }
- }
-}
-
-.symbol {
- padding-left: 2em;
-}
-
.flag-local {
color: inherit;
}
@@ -62,6 +27,10 @@
color: var(--color-blue);
}
+.flag-hidden {
+ color: var(--color-muted);
+}
+
.symbol-name {
color: var(--color-bright);
}
@@ -76,3 +45,19 @@
text-wrap: nowrap;
white-space: pre;
}
+
+.selected {
+ background-color: var(--list-row-selection-background) !important;
+}
+
+.highlighted {
+ background-color: var(--list-row-highlight-background) !important;
+}
+
+.selected.highlighted {
+ background-color: color-mix(
+ in srgb,
+ var(--list-row-selection-background) 50%,
+ var(--list-row-highlight-background) 50%
+ ) !important;
+}
diff --git a/webview/views/SymbolsView.tsx b/webview/views/SymbolsView.tsx
index 5a9c4cc..a19788c 100644
--- a/webview/views/SymbolsView.tsx
+++ b/webview/views/SymbolsView.tsx
@@ -1,47 +1,44 @@
-import headerStyles from '../common/Header.module.css';
import styles from './SymbolsView.module.css';
-import clsx from 'clsx';
import memoizeOne from 'memoize-one';
import { type diff, display } from 'objdiff-wasm';
-import { memo, useCallback, useMemo } from 'react';
-import AutoSizer from 'react-virtualized-auto-sizer';
+import { memo, useCallback, useMemo, useState } from 'react';
import {
FixedSizeList,
type ListChildComponentProps,
areEqual,
} from 'react-window';
import { useShallow } from 'zustand/react/shallow';
-import type { BuildStatus } from '../../shared/messages';
-import { createContextMenu, renderContextItems } from '../common/ContextMenu';
-import TooltipShared from '../common/TooltipShared';
+import type { Unit } from '../../shared/config';
+import { createContextMenu } from '../common/ContextMenu';
+import { createTooltip } from '../common/TooltipShared';
+import type { DiffOutput } from '../diff';
import {
- type UnitScrollOffsets,
- runBuild,
- setCurrentUnit,
+ type Side,
+ type SymbolRefByName,
useAppStore,
useExtensionStore,
} from '../state';
import { percentClass, useFontSize } from '../util/util';
+import { type TreeData, TreeRow } from './TreeView';
-type Side = keyof UnitScrollOffsets;
-
-type SymbolTooltipContent = {
- symbolRef: display.SectionDisplaySymbol;
+export type SymbolTooltipContent = {
+ symbolRef: display.SymbolRef;
side: Side;
};
-const { ContextMenuProvider, useContextMenu } =
- createContextMenu();
+export const { Tooltip: SymbolTooltip, useTooltip: useSymbolTooltip } =
+ createTooltip();
+
+export const {
+ ContextMenuProvider: SymbolContextMenuProvider,
+ useContextMenu: useSymbolContextMenu,
+} = createContextMenu();
const SectionRow = ({
section,
- style,
- onClick,
}: {
- section: SectionData;
- style?: React.CSSProperties;
- onClick?: React.MouseEventHandler;
+ section: display.SectionDisplay;
}) => {
let percentElem = null;
if (section.matchPercent != null) {
@@ -56,63 +53,53 @@ const SectionRow = ({
);
}
return (
-
+
{section.name} ({section.size.toString(16)}){percentElem}
-
+
);
};
const SymbolRow = ({
- obj,
- section,
- symbolRef,
- side,
- style,
+ symbol,
}: {
- obj: diff.ObjectDiff;
- section: SectionData;
- symbolRef: display.SectionDisplaySymbol;
- side: Side;
- style?: React.CSSProperties;
+ symbol: display.SymbolDisplay;
}) => {
- const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol);
- const onContextMenu = useContextMenu();
- const symbol = display.displaySymbol(obj, symbolRef);
const flags = [];
- if (symbol.flags.global) {
+ if (symbol.info.flags.global) {
flags.push(
g
,
);
}
- if (symbol.flags.weak) {
+ if (symbol.info.flags.weak) {
flags.push(
w
,
);
}
- if (symbol.flags.local) {
+ if (symbol.info.flags.local) {
flags.push(
l
,
);
}
- if (symbol.flags.common) {
+ if (symbol.info.flags.common) {
flags.push(
c
,
);
}
+ if (symbol.info.flags.hidden) {
+ flags.push(
+
+ h
+ ,
+ );
+ }
let flagsElem = null;
if (flags.length > 0) {
flagsElem = <>[{flags}] >;
@@ -129,172 +116,286 @@ const SymbolRow = ({
>
);
}
- const tooltipContent: SymbolTooltipContent = {
- symbolRef,
- side,
- };
return (
-
{
- setSelectedSymbol(
- {
- symbolName: symbol.name,
- sectionName: section.name,
- },
- {
- symbolName: symbol.name,
- sectionName: section.name,
- },
- );
- }}
- data-tooltip-id="symbol-tooltip"
- data-tooltip-content={JSON.stringify(tooltipContent)}
- onContextMenu={(e) => {
- onContextMenu(e, tooltipContent);
- }}
- >
+ <>
{flagsElem}
{percentElem}
- {symbol.demangledName || symbol.name}
+ {symbol.info.demangledName || symbol.info.name}
-
+ >
);
};
-type SectionData = display.SectionDisplay & { collapsed: boolean };
+type SymbolData = {
+ section: display.SectionDisplay;
+ symbolRef: display.SymbolRef;
+ selected: boolean;
+ highlighted: boolean;
+};
type ItemData = {
- status: BuildStatus | null;
obj: diff.ObjectDiff | undefined;
- itemCount: number;
- sections: SectionData[];
+ otherObj?: diff.ObjectDiff | undefined;
+ sections: display.SectionDisplay[];
+ treeData: TreeData
;
side: Side;
+ isMapping: boolean;
+ currentUnitName: string;
setSectionCollapsed: (section: string, collapsed: boolean) => void;
+ setHoverSymbols: (value: [number | null, number | null]) => void;
};
const SymbolListRow = memo(
({
index,
style,
- data: { obj, sections, side, setSectionCollapsed },
+ data: {
+ obj,
+ otherObj,
+ treeData,
+ side,
+ isMapping,
+ currentUnitName,
+ setSectionCollapsed,
+ setHoverSymbols,
+ },
}: ListChildComponentProps) => {
+ const { setSelectedSymbol, setUnitMapping } = useAppStore(
+ useShallow((state) => ({
+ setSelectedSymbol: state.setSelectedSymbol,
+ setUnitMapping: state.setUnitMapping,
+ })),
+ );
+ const onContextMenu = useSymbolContextMenu();
if (!obj) {
return null;
}
- let currentIndex = 0;
- for (const section of sections) {
- if (currentIndex === index) {
- return (
- setSectionCollapsed(section.id, !section.collapsed)}
- />
- );
- }
- currentIndex++;
- if (section.collapsed) {
- continue;
- }
- if (index < currentIndex + section.symbols.length) {
- const symbolRef = section.symbols[index - currentIndex];
- return (
-
- );
- }
- currentIndex += section.symbols.length;
+ const node = treeData.nodes[index];
+ if (node.type === 'leaf') {
+ const { symbolRef, section, selected, highlighted } = node.data;
+ const symbol = display.displaySymbol(obj, symbolRef);
+ const tooltipContent: SymbolTooltipContent = {
+ symbolRef,
+ side,
+ };
+ const tooltipProps = useSymbolTooltip(tooltipContent);
+ return (
+ }
+ getClasses={() => {
+ const classes = [];
+ if (selected) {
+ classes.push(styles.selected);
+ }
+ if (highlighted) {
+ classes.push(styles.highlighted);
+ }
+ return classes;
+ }}
+ onLeafClick={() => {
+ const symbolRefByName: SymbolRefByName = {
+ symbolName: symbol.info.name,
+ sectionName: section.name,
+ };
+ let otherSymbolRefByName: SymbolRefByName | null = null;
+ if (symbol.targetSymbol !== undefined && otherObj) {
+ const targetSymbol = otherObj.getSymbol(symbol.targetSymbol);
+ if (targetSymbol) {
+ otherSymbolRefByName = {
+ symbolName: targetSymbol.name,
+ sectionName: targetSymbol.sectionName ?? null,
+ };
+ }
+ }
+ if (isMapping) {
+ if (side === 'left') {
+ setUnitMapping(
+ currentUnitName,
+ symbolRefByName.symbolName,
+ otherSymbolRefByName?.symbolName,
+ );
+ } else {
+ setUnitMapping(
+ currentUnitName,
+ otherSymbolRefByName?.symbolName,
+ symbolRefByName.symbolName,
+ );
+ }
+ }
+ if (side === 'left') {
+ setSelectedSymbol(symbolRefByName, otherSymbolRefByName);
+ } else {
+ setSelectedSymbol(otherSymbolRefByName, symbolRefByName);
+ }
+ }}
+ onHover={() => {
+ const targetSymbol = symbol.targetSymbol ?? null;
+ if (side === 'left') {
+ setHoverSymbols([symbolRef, targetSymbol]);
+ } else {
+ setHoverSymbols([targetSymbol, symbolRef]);
+ }
+ }}
+ rowProps={{
+ ...tooltipProps,
+ onContextMenu: (e) => onContextMenu(e, tooltipContent),
+ }}
+ />
+ );
}
- return null;
+ return (
+ }
+ setBranchCollapsed={setSectionCollapsed}
+ onHover={() => setHoverSymbols([null, null])}
+ />
+ );
},
areEqual,
);
const createItemDataFn = (
- status: BuildStatus | null,
obj: diff.ObjectDiff | undefined,
+ otherObj: diff.ObjectDiff | undefined,
collapsedSections: Record,
search: string | null,
side: Side,
+ isMapping: boolean,
setSectionCollapsed: (section: string, collapsed: boolean) => void,
+ highlightedPath: string | null,
+ setHighlightedPath: (id: string | null) => void,
+ hoverSymbol: number | null,
+ setHoverSymbols: (value: [number | null, number | null]) => void,
+ mappingSymbol: number | null,
+ currentUnit: Unit | null,
+ showMappedSymbols: boolean,
+ showHiddenSymbols: boolean,
+ diffLabel: string | null,
): ItemData => {
+ const currentUnitName = currentUnit?.name || '';
if (!obj) {
return {
- status,
obj,
- itemCount: 0,
+ otherObj,
sections: [],
+ treeData: {
+ leafCount: 0,
+ nodes: [],
+ highlightedPath,
+ setHighlightedPath,
+ },
side,
+ isMapping,
+ currentUnitName,
setSectionCollapsed,
+ setHoverSymbols,
};
}
- const displaySections = display.displaySections(
+ const reverseFnOrder =
+ currentUnit?.metadata?.reverse_fn_order ??
+ currentUnit?.reverse_fn_order ??
+ false;
+ const sections = display.displaySections(
obj,
{
- mapping: undefined,
+ mapping: mappingSymbol ?? undefined,
regex: search ?? undefined,
},
{
- showHiddenSymbols: false,
- showMappedSymbols: false,
- reverseFnOrder: false,
+ showHiddenSymbols,
+ showMappedSymbols,
+ reverseFnOrder,
},
);
- let itemCount = 0;
- const sections: SectionData[] = [];
- for (const section of displaySections) {
- itemCount++;
+ const treeData: ItemData['treeData'] = {
+ leafCount: 0,
+ nodes: [],
+ highlightedPath,
+ setHighlightedPath,
+ };
+ for (const section of sections) {
if (search !== null && section.symbols.length === 0) {
continue;
}
- if (collapsedSections[section.id]) {
- sections.push({
- ...section,
- symbols: [],
- collapsed: true,
- });
- continue;
- }
- itemCount += section.symbols.length;
- sections.push({
- ...section,
- collapsed: false,
+ treeData.leafCount += section.symbols.length;
+ const collapsed = collapsedSections[section.id];
+ treeData.nodes.push({
+ type: 'branch',
+ id: section.id,
+ indent: 0,
+ path: [],
+ data: section,
+ collapsed,
});
+ if (!collapsed) {
+ for (const symbolRef of section.symbols) {
+ const symbol = display.displaySymbol(obj, symbolRef);
+ treeData.nodes.push({
+ type: 'leaf',
+ id: `symbol-${symbolRef}`,
+ indent: 1,
+ path: [section.id],
+ data: {
+ section,
+ symbolRef,
+ selected: hoverSymbol === symbolRef,
+ highlighted: diffLabel !== null && diffLabel === symbol.info.name,
+ },
+ });
+ }
+ }
}
return {
- status,
obj,
- itemCount,
+ otherObj,
sections,
+ treeData,
side,
+ isMapping,
+ currentUnitName,
setSectionCollapsed,
+ setHoverSymbols,
};
};
const createItemDataLeft = memoizeOne(createItemDataFn);
const createItemDataRight = memoizeOne(createItemDataFn);
-const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => {
- const {
- buildRunning,
- currentUnit,
- hasProjectConfig,
- leftStatus,
- rightStatus,
- } = useExtensionStore(
+export const SymbolList = ({
+ height,
+ width,
+ side,
+ result,
+ mappingSymbol,
+ showMappedSymbols,
+ showHiddenSymbols,
+ highlightedPath,
+ setHighlightedPath,
+ hoverSymbols,
+ setHoverSymbols,
+}: {
+ height: number;
+ width: number;
+ side: Side;
+ result: DiffOutput;
+ mappingSymbol: number | null;
+ showMappedSymbols: boolean;
+ showHiddenSymbols: boolean;
+ highlightedPath: string | null;
+ setHighlightedPath: (id: string | null) => void;
+ hoverSymbols: [number | null, number | null];
+ setHoverSymbols: (value: [number | null, number | null]) => void;
+}) => {
+ const { currentUnit, diffLabel } = useExtensionStore(
useShallow((state) => ({
- buildRunning: state.buildRunning,
currentUnit: state.currentUnit,
- hasProjectConfig: state.projectConfig != null,
- leftStatus: state.leftStatus,
- rightStatus: state.rightStatus,
+ diffLabel: state.diffLabel,
})),
);
const currentUnitName = currentUnit?.name || '';
@@ -303,8 +404,6 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => {
search,
setUnitSectionCollapsed,
setUnitScrollOffset,
- setUnitSearch,
- setCurrentView,
} = useAppStore(
useShallow((state) => {
const unit = state.getUnitState(currentUnitName);
@@ -313,8 +412,6 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => {
search: unit.search,
setUnitSectionCollapsed: state.setUnitSectionCollapsed,
setUnitScrollOffset: state.setUnitScrollOffset,
- setUnitSearch: state.setUnitSearch,
- setCurrentView: state.setCurrentView,
};
}),
);
@@ -322,220 +419,51 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => {
() => useAppStore.getState().getUnitState(currentUnitName).scrollOffsets,
[currentUnitName],
);
- const setLeftSectionCollapsed = useCallback(
+
+ const setSectionCollapsed = useCallback(
(section: string, collapsed: boolean) => {
- setUnitSectionCollapsed(currentUnitName, section, 'left', collapsed);
+ setUnitSectionCollapsed(currentUnitName, section, side, collapsed);
},
- [currentUnitName, setUnitSectionCollapsed],
- );
- const setRightSectionCollapsed = useCallback(
- (section: string, collapsed: boolean) => {
- setUnitSectionCollapsed(currentUnitName, section, 'right', collapsed);
- },
- [currentUnitName, setUnitSectionCollapsed],
+ [currentUnitName, setUnitSectionCollapsed, side],
);
- const renderList = (
- height: number,
- width: number,
- itemData: ItemData,
- side: Side,
- ) => {
- if (!itemData.obj) {
- if (!itemData.status || itemData.status.success) {
- return (
-
- No object configured
-
- );
- }
- return (
-
-
{itemData.status.cmdline}
-
{itemData.status.stdout}
-
{itemData.status.stderr}
-
- );
- }
- return (
- {
- if (currentUnitName) {
- setUnitScrollOffset(currentUnitName, side, e.scrollOffset);
- }
- }}
- initialScrollOffset={initialScrollOffsets[side]}
- >
- {SymbolListRow}
-
- );
- };
-
+ const itemData = (side === 'left' ? createItemDataLeft : createItemDataRight)(
+ side === 'left' ? result.diff?.left : result.diff?.right,
+ side === 'left' ? result.diff?.right : result.diff?.left,
+ collapsedSections[side],
+ search,
+ side,
+ result.isMapping,
+ setSectionCollapsed,
+ highlightedPath,
+ setHighlightedPath,
+ side === 'left' ? hoverSymbols[0] : hoverSymbols[1],
+ setHoverSymbols,
+ mappingSymbol,
+ currentUnit,
+ showMappedSymbols,
+ showHiddenSymbols,
+ diffLabel,
+ );
const itemSize = useFontSize() * 1.33;
- const leftItemData = createItemDataLeft(
- leftStatus,
- diff.left,
- collapsedSections.left,
- search,
- 'left',
- setLeftSectionCollapsed,
- );
- const rightItemData = createItemDataRight(
- rightStatus,
- diff.right,
- collapsedSections.right,
- search,
- 'right',
- setRightSectionCollapsed,
- );
-
- const setAllSections = (side: Side, value: boolean) => {
- if (side === 'left') {
- for (const section of leftItemData.sections) {
- setUnitSectionCollapsed(currentUnitName, section.id, 'left', value);
- }
- } else {
- for (const section of rightItemData.sections) {
- setUnitSectionCollapsed(currentUnitName, section.id, 'right', value);
- }
- }
- };
-
- const expandCollapse = (side: Side) => (
- <>
-
-
-
- >
- );
-
- const unitNameRow = (
-
- {currentUnitName}
-
- );
-
- const filterRow = (
- setUnitSearch(currentUnitName, e.target.value)}
- />
- );
-
- const settingsRow = (
-
- );
-
return (
- <>
-
-
-
- {hasProjectConfig ? (
-
- ) : null}
- Target object
-
-
- {currentUnitName ? unitNameRow : filterRow}
- {expandCollapse('left')}
-
-
-
-
- {hasProjectConfig && (
-
- )}
- Base object
-
-
- {currentUnitName ? filterRow : settingsRow}
- {expandCollapse('right')}
-
-
-
-
-
{
- let obj: diff.ObjectDiff | undefined;
- switch (data.side) {
- case 'left':
- obj = diff.left;
- break;
- case 'right':
- obj = diff.right;
- break;
- default:
- break;
- }
- if (!obj) {
- return null;
- }
- const items = display.symbolContext(obj, data.symbolRef);
- return renderContextItems(items, close);
- }}
- >
-
- {({ height, width }) => (
- <>
- {renderList(height, width, leftItemData, 'left')}
- {renderList(height, width, rightItemData, 'right')}
- >
- )}
-
-
-
- {
- const data: SymbolTooltipContent = JSON.parse(content);
- let obj: diff.ObjectDiff | undefined;
- switch (data.side) {
- case 'left':
- obj = diff.left;
- break;
- case 'right':
- obj = diff.right;
- break;
- default:
- break;
- }
- if (!obj) {
- return null;
- }
- return display.symbolHover(obj, data.symbolRef);
- }}
- />
- >
+ {
+ if (currentUnitName) {
+ setUnitScrollOffset(currentUnitName, side, e.scrollOffset);
+ }
+ }}
+ initialScrollOffset={initialScrollOffsets[side]}
+ >
+ {SymbolListRow}
+
);
};
-
-export default SymbolsView;
diff --git a/webview/views/TreeView.module.css b/webview/views/TreeView.module.css
new file mode 100644
index 0000000..5ab4bd7
--- /dev/null
+++ b/webview/views/TreeView.module.css
@@ -0,0 +1,42 @@
+.row {
+ display: flex;
+ cursor: pointer;
+ user-select: none;
+ height: var(--list-row-height);
+ padding-left: 0.5em;
+
+ font-family: var(--code-font-family);
+ font-weight: var(--code-font-weight);
+ font-size: var(--code-font-size);
+ font-variant-ligatures: var(--code-font-variant-ligatures);
+ text-wrap: nowrap;
+ white-space: pre;
+
+ &:hover {
+ background-color: var(--list-row-hover-background);
+ }
+}
+
+.indent {
+ display: inline-block;
+ height: var(--list-row-height);
+ width: 0;
+ border-right: 1px solid var(--panel-separator);
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+}
+
+.indent-highlighted {
+ border-right-color: var(--color-muted);
+}
+
+.toggle {
+ display: inline-block;
+ height: var(--list-row-height);
+ width: 1em;
+ margin-right: 0.25em;
+}
+
+.collapsed {
+ color: color-mix(in srgb, var(--foreground) 75%, transparent);
+}
diff --git a/webview/views/TreeView.tsx b/webview/views/TreeView.tsx
new file mode 100644
index 0000000..4973378
--- /dev/null
+++ b/webview/views/TreeView.tsx
@@ -0,0 +1,215 @@
+import clsx from 'clsx';
+import type { ListChildComponentProps } from 'react-window';
+import styles from './TreeView.module.css';
+
+export type NodeInner = {
+ id: string;
+ indent: number;
+ path: string[];
+ data: T;
+};
+
+export type BranchNode = NodeInner & {
+ type: 'branch';
+ collapsed: boolean;
+};
+
+export type LeafNode = NodeInner & {
+ type: 'leaf';
+};
+
+export type Node = BranchNode | LeafNode;
+
+export type TreeData = {
+ leafCount: number;
+ nodes: Node[];
+ highlightedPath: string | null;
+ setHighlightedPath: (id: string | null) => void;
+};
+
+export type TreeRowProps = ListChildComponentProps> & {
+ rowProps?: React.HTMLProps | null;
+ render: (item: Node) => React.ReactNode;
+ getClasses?: (item: Node) => string[];
+ onLeafClick?: (item: LeafNode) => void;
+ onHover?: (item: Node) => void;
+ setBranchCollapsed?: (id: string, collapsed: boolean) => void;
+};
+
+export function TreeRow({
+ index,
+ style,
+ data: { nodes, highlightedPath, setHighlightedPath },
+ rowProps,
+ render,
+ getClasses,
+ onLeafClick,
+ onHover,
+ setBranchCollapsed,
+}: TreeRowProps) {
+ const node = nodes[index];
+ const classes = [styles.row];
+ if (node.type === 'branch' && node.collapsed) {
+ classes.push(styles.collapsed);
+ }
+ classes.push(...(getClasses?.(node) ?? []));
+ const indentItems = [];
+ for (let i = 0; i < node.indent; i++) {
+ indentItems.push(
+ ,
+ );
+ }
+ if (node.type === 'branch') {
+ indentItems.push(
+ ,
+ );
+ }
+ return (
+ {
+ if (node.type === 'leaf') {
+ onLeafClick?.(node);
+ } else {
+ const collapsed = !node.collapsed;
+ setBranchCollapsed?.(node.id, collapsed);
+ setHighlightedPath(
+ collapsed ? node.path[node.path.length - 1] : node.id,
+ );
+ }
+ }}
+ onMouseEnter={() => {
+ onHover?.(node);
+ setHighlightedPath(
+ node.type === 'leaf' || node.collapsed
+ ? node.path[node.path.length - 1]
+ : node.id,
+ );
+ }}
+ >
+ {indentItems}
+ {render(node)}
+
+ );
+}
+
+type NodeWithChildren = BranchNode & {
+ children: (NodeWithChildren | LeafNode)[];
+};
+
+export type SimpleTreeNodeData = { label: string };
+export type SimpleTreeData = TreeData<
+ SimpleTreeNodeData,
+ L & SimpleTreeNodeData
+>;
+
+// Build a simple tree structure by splitting the path of each item
+// into components (splitting on '/') and creating a tree node for
+// each component.
+export function buildSimpleTree(
+ items: L[],
+ getPath: (item: L) => string,
+ collapsed: Record,
+ highlightedPath: string | null,
+ setHighlightedPath: (id: string | null) => void,
+): SimpleTreeData {
+ type SimpleTreeNode = NodeWithChildren<
+ SimpleTreeNodeData,
+ L & SimpleTreeNodeData
+ >;
+ const map = new Map();
+ const rootNodes = [];
+ for (const item of items) {
+ const path = getPath(item);
+ const split = path.split('/');
+ let parent: SimpleTreeNode | null = null;
+ for (let i = 0; i < split.length - 1; i++) {
+ const name = split[i];
+ const dirPath = split.slice(0, i + 1);
+ const key = dirPath.join('/');
+ let node = map.get(key);
+ if (!node) {
+ node = {
+ type: 'branch',
+ id: key,
+ indent: i,
+ collapsed: !!collapsed[key],
+ children: [],
+ path: buildPath(dirPath),
+ data: { label: name },
+ };
+ if (parent) {
+ parent.children.push(node);
+ } else {
+ rootNodes.push(node);
+ }
+ map.set(key, node);
+ }
+ parent = node;
+ }
+ const node: LeafNode = {
+ type: 'leaf',
+ id: path,
+ indent: split.length - 1,
+ path: buildPath(split),
+ data: {
+ ...item,
+ label: split[split.length - 1],
+ },
+ };
+ if (parent) {
+ parent.children.push(node);
+ } else {
+ rootNodes.push(node);
+ }
+ }
+ const nodes: SimpleTreeData['nodes'] = [];
+ for (const node of rootNodes) {
+ pushNodes(node, nodes);
+ }
+ return {
+ leafCount: items.length,
+ nodes,
+ highlightedPath,
+ setHighlightedPath,
+ };
+}
+
+function pushNodes(
+ item: NodeWithChildren | LeafNode,
+ out: Node[],
+) {
+ if (item.type === 'branch') {
+ out.push(item);
+ if (!item.collapsed) {
+ for (const child of item.children) {
+ pushNodes(child, out);
+ }
+ }
+ } else if (item.type === 'leaf') {
+ out.push(item);
+ }
+}
+
+function buildPath(split: string[]) {
+ const path = [];
+ for (let i = 0; i < split.length - 1; i++) {
+ path.push(split.slice(0, i + 1).join('/'));
+ }
+ return path;
+}
diff --git a/webview/views/UnitsView.module.css b/webview/views/UnitsView.module.css
index 180d18c..1849c56 100644
--- a/webview/views/UnitsView.module.css
+++ b/webview/views/UnitsView.module.css
@@ -5,49 +5,8 @@
overflow: hidden;
}
-.row {
- display: flex;
- cursor: pointer;
- user-select: none;
- height: var(--list-row-height);
- padding-left: 0.5em;
-
- font-family: var(--code-font-family);
- font-weight: var(--code-font-weight);
- font-size: var(--code-font-size);
- font-variant-ligatures: var(--code-font-variant-ligatures);
- text-wrap: nowrap;
- white-space: pre;
-
- &:hover {
- background-color: var(--list-row-hover-background);
- }
-}
-
-.indent {
- display: inline-block;
- height: var(--list-row-height);
- width: 0;
- border-right: 1px solid var(--panel-separator);
- margin-left: 0.5em;
- margin-right: 0.5em;
-}
-
-.indent-highlighted {
- border-right-color: var(--color-muted);
-}
-
-.toggle {
- display: inline-block;
- height: var(--list-row-height);
- width: 1em;
- margin-right: 0.25em;
-}
-
-.unit {
- .label {
- margin-left: 0.4em;
- }
+.unit-label {
+ margin-left: 0.4em;
}
.complete {
@@ -57,7 +16,3 @@
.incomplete {
color: var(--color-red);
}
-
-.collapsed {
- color: color-mix(in srgb, var(--foreground) 75%, transparent);
-}
diff --git a/webview/views/UnitsView.tsx b/webview/views/UnitsView.tsx
index ee035ca..8eedbed 100644
--- a/webview/views/UnitsView.tsx
+++ b/webview/views/UnitsView.tsx
@@ -1,6 +1,3 @@
-import styles from './UnitsView.module.css';
-
-import clsx from 'clsx';
import memoizeOne from 'memoize-one';
import { memo, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
@@ -19,135 +16,40 @@ import {
useExtensionStore,
} from '../state';
import { useFontSize } from '../util/util';
+import { type SimpleTreeData, TreeRow, buildSimpleTree } from './TreeView';
+import styles from './UnitsView.module.css';
-type DirectoryItem = {
- type: 'directory';
- indent: number;
- label: string;
- id: string;
- collapsed: boolean;
- path: string[];
-};
-
-type UnitItem = {
- type: 'unit';
- indent: number;
- label: string;
- id: string;
- unit: Unit;
- path: string[];
-};
-
-type Item = DirectoryItem | UnitItem;
-
-type ItemData = {
- unitsCount: number;
- items: Item[];
- highlightedPath: string | null;
- setHighlightedPath: (id: string | null) => void;
-};
-
-const UnitRow = memo(
- ({
- index,
- style,
- data: { items, highlightedPath, setHighlightedPath },
- }: ListChildComponentProps) => {
- const setCollapsedUnit = useAppStore((state) => state.setCollapsedUnit);
- const item = items[index];
- const classes = [styles.row];
- if (item.type === 'unit') {
- classes.push(styles.unit);
- if (item.unit.metadata?.complete !== undefined) {
- if (item.unit.metadata.complete) {
- classes.push(styles.complete);
- } else {
- classes.push(styles.incomplete);
- }
- }
- } else {
- // classes.push(styles.directory);
- if (item.collapsed) {
- classes.push(styles.collapsed);
- }
- }
- const indentItems = [];
- for (let i = 0; i < item.indent; i++) {
- indentItems.push(
- ,
- );
- }
- if (item.type === 'directory') {
- indentItems.push(
- ,
- );
- }
- return (
- {
- if (item.type === 'unit') {
- setCurrentUnit(item.unit);
- } else {
- const collapsed = !item.collapsed;
- setCollapsedUnit(item.id, collapsed);
- setHighlightedPath(
- collapsed ? item.path[item.path.length - 1] : item.id,
- );
+const UnitRow = memo((props: ListChildComponentProps>) => {
+ const setCollapsedUnit = useAppStore((state) => state.setCollapsedUnit);
+ return (
+ {
+ if (
+ item.type === 'leaf' &&
+ item.data.metadata?.complete !== undefined
+ ) {
+ if (item.data.metadata.complete) {
+ return [styles.complete];
}
- }}
- onMouseEnter={() => {
- setHighlightedPath(
- item.type === 'unit' || item.collapsed
- ? item.path[item.path.length - 1]
- : item.id,
- );
- }}
- >
- {indentItems}
- {item.label}
-
- );
- },
- areEqual,
-);
-
-type TreeItem = DirectoryItem & { children: (TreeItem | UnitItem)[] };
-
-function pushTreeItems(item: TreeItem | UnitItem, out: Item[]) {
- if (item.type === 'directory') {
- out.push(item);
- if (!item.collapsed) {
- for (const child of item.children) {
- pushTreeItems(child, out);
- }
- }
- } else if (item.type === 'unit') {
- out.push(item);
- }
-}
-
-const buildPath = (split: string[]) => {
- const path = [];
- for (let i = 0; i < split.length - 1; i++) {
- path.push(split.slice(0, i + 1).join('/'));
- }
- return path;
-};
+ return [styles.incomplete];
+ }
+ return [];
+ }}
+ onLeafClick={(item) => {
+ setCurrentUnit(item.data);
+ }}
+ setBranchCollapsed={setCollapsedUnit}
+ render={(item) => {
+ return (
+
+ {item.data.label}
+
+ );
+ }}
+ />
+ );
+}, areEqual);
const createItemData = memoizeOne(
(
@@ -155,62 +57,14 @@ const createItemData = memoizeOne(
collapsedUnits: Record,
highlightedPath: string | null,
setHighlightedPath: (id: string | null) => void,
- ): ItemData => {
- const units = config?.units ?? [];
- const map = new Map();
- const rootItems = [];
- for (const unit of units) {
- const path = unit.name || '';
- const split = path.split('/');
- let parent: TreeItem | null = null;
- for (let i = 0; i < split.length - 1; i++) {
- const name = split[i];
- const dirPath = split.slice(0, i + 1);
- const key = dirPath.join('/');
- let item = map.get(key);
- if (!item) {
- item = {
- type: 'directory',
- indent: i,
- label: name,
- id: key,
- collapsed: !!collapsedUnits[key],
- children: [],
- path: buildPath(dirPath),
- };
- if (parent) {
- parent.children.push(item);
- } else {
- rootItems.push(item);
- }
- map.set(key, item);
- }
- parent = item;
- }
- const unitItem: UnitItem = {
- type: 'unit',
- indent: split.length - 1,
- label: split[split.length - 1],
- id: path,
- unit,
- path: buildPath(split),
- };
- if (parent) {
- parent.children.push(unitItem);
- } else {
- rootItems.push(unitItem);
- }
- }
- const items: Item[] = [];
- for (const item of rootItems) {
- pushTreeItems(item, items);
- }
- return {
- unitsCount: units.length,
- items,
+ ): SimpleTreeData => {
+ return buildSimpleTree(
+ config?.units ?? [],
+ (unit) => unit.name || '',
+ collapsedUnits,
highlightedPath,
setHighlightedPath,
- };
+ );
},
);
@@ -250,7 +104,7 @@ const UnitsView = () => {