import styles from './DataView.module.css'; import clsx from 'clsx'; import { type diff, display } from 'objdiff-wasm'; import { memo, useCallback, useMemo } from 'react'; import { FixedSizeList, areEqual } from 'react-window'; import type { ListChildComponentProps, ListOnScrollProps } from 'react-window'; import { useShallow } from 'zustand/react/shallow'; import { createContextMenu } from '../common/ContextMenu'; import { createTooltip } from '../common/TooltipShared'; import { useAppStore, useExtensionStore } from '../state'; import { useFontSize } from '../util/util'; const BYTES_PER_ROW = 16; export type DataTooltipContent = { column: number; row: number; }; export const { Tooltip: DataTooltip, useTooltip: useDataTooltip } = createTooltip(); export const { ContextMenuProvider: DataContextMenuProvider, useContextMenu: useDataContextMenu, } = createContextMenu(); const DataCell = ({ obj, symbol, row, column, }: { obj: diff.ObjectDiff | undefined; symbol: display.SymbolDisplay | null; row: number; column: number; }) => { const onContextMenu = useDataContextMenu(); const tooltipContent: DataTooltipContent = useMemo( () => ({ column, row, }), [column, row], ); const tooltipProps = useDataTooltip(tooltipContent); const onContextMenuMemo = useCallback( (e: React.MouseEvent) => onContextMenu(e, tooltipContent), [onContextMenu, tooltipContent], ); if (!obj || !symbol) { return null; } const dataRow = display.displayDataRow(obj, symbol.info.id, row); const anyDiff = dataRow.segments.some((s) => s.kind !== 'none') || dataRow.relocations.some((r) => r.kind !== 'none'); const out: React.ReactNode[] = []; // Display address out.push( {dataRow.address.toString(16).padStart(8, '0')}: , ); // Display hex bytes let byteIndex = 0; const hexBytes: React.ReactNode[] = []; const asciiChars: React.ReactNode[] = []; for (const diff of dataRow.segments) { let className: string | undefined; switch (diff.kind) { case 'none': break; case 'replace': className = styles.replace; break; case 'delete': className = styles.delete; break; case 'insert': className = styles.insert; break; } // Check for relocations that overlap with this diff segment const diffStartAddr = dataRow.address + BigInt(byteIndex); const diffEndAddr = diffStartAddr + BigInt(diff.size); const relocsForDiff = dataRow.relocations.filter((r) => { const relocStart = r.address; const relocEnd = r.address + BigInt(r.size); // Check if relocation overlaps with this diff segment return relocStart < diffEndAddr && relocEnd > diffStartAddr; }); if (diff.data.length === 0) { // Empty data (deletion on other side - show as blank spaces) for (let i = 0; i < diff.size; i++) { hexBytes.push({' '}); asciiChars.push( ); byteIndex++; if (byteIndex % 8 === 0 && byteIndex < BYTES_PER_ROW) { hexBytes.push( ); } } } else { // Display actual bytes for (let i = 0; i < diff.data.length; i++) { const byte = diff.data[i]; const currentAddress = dataRow.address + BigInt(byteIndex); const reloc = relocsForDiff.find( (r) => r.address <= currentAddress && currentAddress < r.address + BigInt(r.size), ); let byteClassName = className; let byteText = byte.toString(16).padStart(2, '0'); if (reloc) { if (byte === 0) { byteText = '??'; } if (reloc.kind !== 'none') { switch (reloc.kind) { case 'replace': byteClassName = styles.replace; break; case 'delete': byteClassName = styles.delete; break; case 'insert': byteClassName = styles.insert; break; } } } hexBytes.push( {byteText}{' '} , ); // ASCII representation const c = String.fromCharCode(byte); const asciiChar = c.charCodeAt(0) >= 32 && c.charCodeAt(0) < 127 ? c : '.'; asciiChars.push( {asciiChar} , ); byteIndex++; if (byteIndex % 8 === 0 && byteIndex < BYTES_PER_ROW) { hexBytes.push( ); } } } } // Pad to full row width if needed while (byteIndex < BYTES_PER_ROW) { hexBytes.push({' '}); asciiChars.push( ); byteIndex++; if (byteIndex % 8 === 0 && byteIndex < BYTES_PER_ROW) { hexBytes.push( ); } } out.push({hexBytes}); out.push( ); out.push({asciiChars}); return (
{out}
); }; const DataRow = memo( ({ index, style, data, }: ListChildComponentProps<{ leftObj: diff.ObjectDiff | undefined; leftSymbol: display.SymbolDisplay | null; rightObj: diff.ObjectDiff | undefined; rightSymbol: display.SymbolDisplay | null; }>) => { const { leftObj, leftSymbol, rightObj, rightSymbol } = data; return (
); }, areEqual, ); export const DataList = ({ height, width, diff, leftSymbol, rightSymbol, }: { height: number; width: number; diff: diff.DiffResult; leftSymbol: display.SymbolDisplay | null; rightSymbol: display.SymbolDisplay | null; }) => { const { currentUnit } = useExtensionStore( useShallow((state) => ({ currentUnit: state.currentUnit, })), ); const { setSymbolScrollOffset } = useAppStore( useShallow((state) => ({ setSymbolScrollOffset: state.setSymbolScrollOffset, })), ); const fontSize = useFontSize(); const leftCount = leftSymbol?.rowCount ?? 0; const rightCount = rightSymbol?.rowCount ?? 0; const rowCount = Math.max(leftCount, rightCount); // Get symbol name for scroll persistence const symbolName = useMemo(() => { if (leftSymbol && diff.left) { return leftSymbol.info.name; } if (rightSymbol && diff.right) { return rightSymbol.info.name; } return ''; }, [diff, leftSymbol, rightSymbol]); const itemData = useMemo( () => ({ leftObj: diff.left, leftSymbol, rightObj: diff.right, rightSymbol, }), [diff, leftSymbol, rightSymbol], ); const currentUnitName = currentUnit?.name || ''; // Get initial scroll offset const initialScrollOffset = useMemo( () => useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[ symbolName ] || 0, [currentUnitName, symbolName], ); // Handle scroll events to persist position const onScrollMemo = useCallback( (e: ListOnScrollProps) => { setSymbolScrollOffset(currentUnitName, symbolName, e.scrollOffset); }, [currentUnitName, symbolName, setSymbolScrollOffset], ); if ((!diff.left || !leftSymbol) && (!diff.right || !rightSymbol)) { return null; } return ( {DataRow} ); }; export default DataList;