Improve Tooltip component & memoization cleanup

This commit is contained in:
Luke Street
2025-07-13 13:31:25 -06:00
parent 7cf261f364
commit 640ebb46a6
4 changed files with 99 additions and 144 deletions
+34 -58
View File
@@ -1,81 +1,57 @@
import clsx from 'clsx';
import styles from './TooltipShared.module.css';
import type { display } from 'objdiff-wasm';
import React, { useCallback, useMemo } from 'react';
import React, { useMemo } from 'react';
import { Tooltip } from 'react-tooltip';
type TooltipProps = {
export type TooltipTriggerProps = {
'data-tooltip-id': string;
'data-tooltip-content': string;
};
export type TooltipCallback<T> = (content: T) => display.HoverItem[] | null;
export type TooltipProps<T> = Omit<
React.ComponentProps<typeof Tooltip>,
'id' | 'render'
> & {
callback: TooltipCallback<T>;
};
export function createTooltip<T>(): {
Tooltip: React.FC<{
callback: TooltipCallback<T>;
}>;
useTooltip: (content: T) => TooltipProps;
Tooltip: React.FC<TooltipProps<T>>;
useTooltip: (content: T) => TooltipTriggerProps;
} {
const id = generateRandomString(10);
return {
Tooltip: ({ callback }) => {
const callbackMemo = useCallback(
(content: string) => {
if (!content) {
Tooltip: ({ callback, className, ...props }) => (
<Tooltip
{...props}
id={id}
className={clsx(styles.tooltip, className)}
render={({ content }) => {
const items = useMemo(() => {
if (!content) {
return null;
}
const parsedContent = JSON.parse(content) as T;
return callback(parsedContent);
}, [callback, content]);
if (!items) {
return null;
}
const parsedContent = JSON.parse(content) as T;
return callback(parsedContent);
},
[callback],
);
return <TooltipShared id={id} callback={callbackMemo} />;
},
useTooltip: (content: T) =>
// useMemo(
// () => ({
// 'data-tooltip-id': id,
// 'data-tooltip-content': JSON.stringify(content),
// }),
// [content],
// ),
({
'data-tooltip-id': id,
'data-tooltip-content': JSON.stringify(content),
}),
return <TooltipContentMemo items={items} />;
}}
/>
),
useTooltip: (content: T) => ({
'data-tooltip-id': id,
'data-tooltip-content': JSON.stringify(content),
}),
};
}
const TooltipShared = ({
id,
callback,
}: {
id: string;
callback: (content: string) => display.HoverItem[] | null;
}) => {
return (
<Tooltip
id={id}
place="bottom"
className={styles.tooltip}
delayShow={500}
render={({ content }) => {
const items = useMemo(() => {
if (!content) {
return null;
}
return callback(content);
}, [callback, content]);
if (!items) {
return null;
}
return <TooltipContentMemo items={items} />;
}}
/>
);
};
const TooltipContent = ({ items }: { items: display.HoverItem[] }) => {
const out = [];
for (const [i, item] of items.entries()) {
+12 -14
View File
@@ -1,7 +1,7 @@
import memoizeOne from 'memoize-one';
import { diff } from 'objdiff-wasm';
import { subscribeWithSelector } from 'zustand/middleware';
import { create } from 'zustand/react';
import { shallow } from 'zustand/shallow';
import {
type ConfigProperties,
type ConfigPropertyValue,
@@ -325,20 +325,18 @@ export function openSettings(): void {
vsCode.postMessage({ type: 'openSettings' });
}
export function buildDiffConfig(
configProperties: ConfigProperties | null | undefined,
): diff.DiffConfig {
const config = new diff.DiffConfig();
const props = getModifiedConfigProperties(
configProperties ?? useExtensionStore.getState().configProperties,
);
for (const key in props) {
if (props[key] != null) {
config.setProperty(key, props[key].toString());
export const buildDiffConfig = memoizeOne(
(configProperties: ConfigProperties): diff.DiffConfig => {
const config = new diff.DiffConfig();
const props = getModifiedConfigProperties(configProperties);
for (const key in props) {
if (props[key] != null) {
config.setProperty(key, props[key].toString());
}
}
}
return config;
}
return config;
},
);
const handleMessage = (event: MessageEvent) => {
const message = event.data as InboundMessage;
+12 -6
View File
@@ -101,10 +101,8 @@ const DiffView = ({
() => resolveSymbol(result.diff?.right, rightSymbolRef),
[result.diff?.right, rightSymbolRef],
);
const diffConfig = useMemo(
() => buildDiffConfig(configProperties),
[configProperties],
);
// Already memoized
const diffConfig = buildDiffConfig(configProperties);
let leftColumnView: ColumnView = {
type: 'symbols',
@@ -317,8 +315,16 @@ const DiffView = ({
</InstructionContextMenuProvider>
</SymbolContextMenuProvider>
</div>
<SymbolTooltip callback={symbolTooltipCallback} />
<InstructionTooltip callback={instructionTooltipCallback} />
<SymbolTooltip
place="bottom"
delayShow={500}
callback={symbolTooltipCallback}
/>
<InstructionTooltip
place="bottom"
delayShow={500}
callback={instructionTooltipCallback}
/>
</>
);
};
+41 -66
View File
@@ -1,14 +1,12 @@
import headerStyles from '../common/Header.module.css';
import styles from './FunctionView.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 { FixedSizeList, areEqual } from 'react-window';
import type { ListChildComponentProps } from 'react-window';
import type { ListChildComponentProps, ListOnScrollProps } from 'react-window';
import { useShallow } from 'zustand/react/shallow';
import { createContextMenu, renderContextItems } from '../common/ContextMenu';
import { createContextMenu } from '../common/ContextMenu';
import { createTooltip } from '../common/TooltipShared';
import { buildDiffConfig, useAppStore, useExtensionStore } from '../state';
import {
@@ -283,58 +281,6 @@ const AsmRow = memo(
areEqual,
);
const createItemData = memoizeOne(
(
result: diff.DiffResult,
leftSymbol: display.SymbolDisplay | null,
rightSymbol: display.SymbolDisplay | null,
highlight: HighlightState,
setHighlight: (highlight: HighlightState) => void,
): ItemData => {
const itemCount = Math.max(
leftSymbol?.rowCount || 0,
rightSymbol?.rowCount || 0,
);
const symbolName = leftSymbol?.info.name || rightSymbol?.info.name || '';
const config = buildDiffConfig(null);
const matchPercent = rightSymbol?.matchPercent;
return {
itemCount,
symbolName,
result,
config,
matchPercent,
leftSymbol,
rightSymbol,
highlight,
setHighlight,
};
},
);
const SymbolLabel = ({
symbol,
}: {
symbol: display.SymbolDisplay | null;
}) => {
if (!symbol) {
return (
<span className={clsx(headerStyles.label, headerStyles.missing)}>
Missing
</span>
);
}
const displayName = symbol.info.demangledName || symbol.info.name;
return (
<span
className={clsx(headerStyles.label, headerStyles.emphasized)}
title={displayName}
>
{displayName}
</span>
);
};
export const InstructionList = ({
height,
width,
@@ -348,7 +294,12 @@ export const InstructionList = ({
leftSymbol: display.SymbolDisplay | null;
rightSymbol: display.SymbolDisplay | null;
}) => {
const currentUnit = useExtensionStore((state) => state.currentUnit);
const { configProperties, currentUnit } = useExtensionStore(
useShallow((state) => ({
configProperties: state.configProperties,
currentUnit: state.currentUnit,
})),
);
const { highlight, setSymbolScrollOffset, setHighlight } = useAppStore(
useShallow((state) => ({
highlight: state.highlight,
@@ -356,13 +307,33 @@ export const InstructionList = ({
setHighlight: state.setHighlight,
})),
);
const itemData = createItemData(
const itemData = useMemo(() => {
const itemCount = Math.max(
leftSymbol?.rowCount || 0,
rightSymbol?.rowCount || 0,
);
const symbolName = leftSymbol?.info.name || rightSymbol?.info.name || '';
const config = buildDiffConfig(configProperties);
const matchPercent = leftSymbol?.matchPercent;
return {
itemCount,
symbolName,
result: diff,
config,
matchPercent,
leftSymbol,
rightSymbol,
highlight,
setHighlight,
};
}, [
diff,
leftSymbol,
rightSymbol,
configProperties,
highlight,
setHighlight,
);
]);
const currentUnitName = currentUnit?.name || '';
const initialScrollOffset = useMemo(
() =>
@@ -372,6 +343,16 @@ export const InstructionList = ({
[currentUnitName, itemData.symbolName],
);
const itemSize = useFontSize() * 1.33;
const onScrollMemo = useCallback(
(e: ListOnScrollProps) => {
setSymbolScrollOffset(
currentUnitName,
itemData.symbolName,
e.scrollOffset,
);
},
[currentUnitName, itemData.symbolName, setSymbolScrollOffset],
);
return (
<FixedSizeList
height={height}
@@ -380,13 +361,7 @@ export const InstructionList = ({
width={width}
itemData={itemData}
overscanCount={20}
onScroll={(e) => {
setSymbolScrollOffset(
currentUnitName,
itemData.symbolName,
e.scrollOffset,
);
}}
onScroll={onScrollMemo}
initialScrollOffset={initialScrollOffset}
>
{AsmRow}