import headerStyles from '../common/Header.module.css'; import styles from './DiffView.module.css'; import clsx from 'clsx'; import { type diff, display } from 'objdiff-wasm'; import { useCallback, useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { useShallow } from 'zustand/react/shallow'; import type { BuildStatus } from '../../shared/messages'; import { type ContextMenuRender, renderContextItems, } from '../common/ContextMenu'; import type { TooltipCallback } from '../common/TooltipShared'; import type { DiffOutput } from '../diff'; import { type Side, type SymbolRefByName, buildDiffConfig, runBuild, setCurrentUnit, useAppStore, useExtensionStore, } from '../state'; import { percentClass } from '../util/util'; import { DataContextMenuProvider, DataList, DataTooltip, type DataTooltipContent, } from './DataView'; import { InstructionContextMenuProvider, InstructionList, InstructionTooltip, type InstructionTooltipContent, } from './FunctionView'; import { SymbolContextMenuProvider, SymbolList, SymbolTooltip, type SymbolTooltipContent, } from './SymbolsView'; type ColumnViewBuildStatus = { type: 'buildStatus'; status: BuildStatus; }; type ColumnViewAsm = { type: 'asm'; obj: diff.ObjectDiff; symbol: display.SymbolDisplay; }; type ColumnViewData = { type: 'data'; obj: diff.ObjectDiff; symbol: display.SymbolDisplay; }; type ColumnViewSymbols = { type: 'symbols'; mappingSymbol: number | null; }; type ColumnViewNone = { type: 'none'; }; type ColumnView = | ColumnViewNone | ColumnViewBuildStatus | ColumnViewAsm | ColumnViewData | ColumnViewSymbols; export const resolveSymbol = ( obj: diff.ObjectDiff | undefined, symbolRef: SymbolRefByName | null, ): display.SymbolDisplay | null => { if (!symbolRef || !obj) { return null; } const leftSymbol = obj?.findSymbol( symbolRef.symbolName, symbolRef.sectionName ?? undefined, ); if (leftSymbol === undefined) { return null; } return display.displaySymbol(obj, leftSymbol.id); }; const DiffView = ({ result, leftSymbolRef, rightSymbolRef, }: { result: DiffOutput; leftSymbolRef: SymbolRefByName | null; rightSymbolRef: SymbolRefByName | null; }) => { const { configProperties } = useExtensionStore( useShallow((state) => ({ configProperties: state.configProperties, })), ); const leftSymbol = useMemo( () => resolveSymbol(result.diff?.left, leftSymbolRef), [result.diff?.left, leftSymbolRef], ); const rightSymbol = useMemo( () => resolveSymbol(result.diff?.right, rightSymbolRef), [result.diff?.right, rightSymbolRef], ); // Already memoized const diffConfig = buildDiffConfig(configProperties); let leftColumnView: ColumnView = { type: 'symbols', mappingSymbol: null, }; let rightColumnView: ColumnView = { type: 'symbols', mappingSymbol: null, }; const leftSuccess = result.leftStatus?.success ?? false; const rightSuccess = result.rightStatus?.success ?? false; if (!leftSuccess) { if (result.leftStatus) { leftColumnView = { type: 'buildStatus', status: result.leftStatus, }; } else { leftColumnView = { type: 'none', }; } } if (!rightSuccess) { if (result.rightStatus) { rightColumnView = { type: 'buildStatus', status: result.rightStatus, }; } else { rightColumnView = { type: 'none', }; } } let leftColumnType: ColumnView['type'] | null = null; let rightColumnType: ColumnView['type'] | null = null; if (leftSymbol && leftSuccess) { switch (leftSymbol.info.sectionKind) { case 'code': leftColumnType = 'asm'; break; case 'data': leftColumnType = 'data'; break; default: break; } } if (rightSymbol && rightSuccess) { switch (rightSymbol.info.sectionKind) { case 'code': rightColumnType = 'asm'; break; case 'data': rightColumnType = 'data'; break; default: break; } } if (leftSymbol && leftColumnType && rightSymbol && rightColumnType) { // Joint view leftColumnView = { type: leftColumnType, obj: result.diff!.left!, symbol: leftSymbol, }; rightColumnView = { type: rightColumnType, obj: result.diff!.right!, symbol: rightSymbol, }; } else if (leftSymbol && leftColumnType) { // Left view only leftColumnView = { type: leftColumnType, obj: result.diff!.left!, symbol: leftSymbol, }; if (rightSuccess) { // Mapping view rightColumnView = { type: 'symbols', mappingSymbol: leftSymbol.info.id, }; } } else if (rightSymbol && rightColumnType) { // Right view only rightColumnView = { type: rightColumnType, obj: result.diff!.right!, symbol: rightSymbol, }; if (leftSuccess) { // Mapping view leftColumnView = { type: 'symbols', mappingSymbol: rightSymbol.info.id, }; } } const symbolContextMenuRender: ContextMenuRender = useCallback( ({ data }, close) => { let obj: diff.ObjectDiff | undefined; switch (data.side) { case 'left': obj = result.diff?.left; break; case 'right': obj = result.diff?.right; break; default: break; } if (!obj) { return null; } const items = display.symbolContext(obj, data.symbolRef); return renderContextItems(items, close); }, [result.diff], ); const symbolTooltipCallback: TooltipCallback = useCallback( (data) => { let obj: diff.ObjectDiff | undefined; switch (data.side) { case 'left': obj = result.diff?.left; break; case 'right': obj = result.diff?.right; break; default: break; } if (!obj) { return null; } return display.symbolHover(obj, data.symbolRef); }, [result.diff], ); const instructionContextMenuRender: ContextMenuRender = useCallback( ({ data }, close) => { let obj: diff.ObjectDiff | undefined; let symbol: number | undefined; switch (data.column) { case 0: obj = result.diff?.left; symbol = leftSymbol?.info.id; break; case 1: obj = result.diff?.right; symbol = rightSymbol?.info.id; break; default: break; } if (!obj || symbol === undefined) { return null; } const items = display.instructionContext( obj, symbol, data.row, diffConfig, ); return renderContextItems(items, close); }, [result.diff, leftSymbol, rightSymbol, diffConfig], ); const dataContextMenuRender: ContextMenuRender = useCallback( ({ data }, close) => { let obj: diff.ObjectDiff | undefined; let symbol: number | undefined; switch (data.column) { case 0: obj = result.diff?.left; symbol = leftSymbol?.info.id; break; case 1: obj = result.diff?.right; symbol = rightSymbol?.info.id; break; default: break; } if (!obj || symbol === undefined) { return null; } const items = display.dataContext(obj, symbol, data.row); return renderContextItems(items, close); }, [result.diff, leftSymbol, rightSymbol], ); const instructionTooltipCallback: TooltipCallback = useCallback( (data) => { let obj: diff.ObjectDiff | undefined; let symbol: number | undefined; switch (data.column) { case 0: obj = result.diff?.left; symbol = leftSymbol?.info.id; break; case 1: obj = result.diff?.right; symbol = rightSymbol?.info.id; break; default: break; } if (!obj || symbol === undefined) { return null; } return display.instructionHover(obj, symbol, data.row, diffConfig); }, [result.diff, leftSymbol, rightSymbol, diffConfig], ); const dataTooltipCallback: TooltipCallback = useCallback( (data) => { let obj: diff.ObjectDiff | undefined; let symbol: number | undefined; switch (data.column) { case 0: obj = result.diff?.left; symbol = leftSymbol?.info.id; break; case 1: obj = result.diff?.right; symbol = rightSymbol?.info.id; break; default: break; } if (!obj || symbol === undefined) { return null; } return display.dataHover(obj, symbol, data.row); }, [result.diff, leftSymbol, rightSymbol], ); const [showMappedSymbols, setShowMappedSymbols] = useState(false); return ( <>
{({ height, width }) => ( )}
); }; const DiffViewHeader = ({ result, leftSymbolRef, rightSymbolRef, leftColumnView, rightColumnView, showMappedSymbols, setShowMappedSymbols, }: { result: DiffOutput; leftSymbolRef: SymbolRefByName | null; rightSymbolRef: SymbolRefByName | null; leftColumnView: ColumnView; rightColumnView: ColumnView; showMappedSymbols: boolean; setShowMappedSymbols: (value: boolean) => void; }) => { const { buildRunning, currentUnit, hasProjectConfig } = useExtensionStore( useShallow((state) => ({ buildRunning: state.buildRunning, currentUnit: state.currentUnit, hasProjectConfig: state.projectConfig != null, })), ); const currentUnitName = currentUnit?.name || ''; const { search, setUnitSectionCollapsed, setUnitSearch, setCurrentView, setSelectedSymbol, setUnitMapping, } = useAppStore( useShallow((state) => { const unit = state.getUnitState(currentUnitName); return { search: unit.search, setUnitSectionCollapsed: state.setUnitSectionCollapsed, setUnitSearch: state.setUnitSearch, setCurrentView: state.setCurrentView, setSelectedSymbol: state.setSelectedSymbol, setUnitMapping: state.setUnitMapping, }; }), ); const onBackClick = useCallback(() => { if (leftSymbolRef || rightSymbolRef) { setSelectedSymbol(null, null); } else if (hasProjectConfig) { setCurrentUnit(null); } }, [leftSymbolRef, rightSymbolRef, setSelectedSymbol, hasProjectConfig]); const setAllSections = (side: Side, value: boolean) => { if (side === 'left') { if (result.diff?.left) { const displaySections = display.displaySections( result.diff.left, { mapping: undefined, regex: undefined, }, { showHiddenSymbols: false, showMappedSymbols: false, reverseFnOrder: false, }, ); for (const section of displaySections) { setUnitSectionCollapsed(currentUnitName, section.id, 'left', value); } } } else if (result.diff?.right) { const displaySections = display.displaySections( result.diff.right, { mapping: undefined, regex: undefined, }, { showHiddenSymbols: false, showMappedSymbols: false, reverseFnOrder: false, }, ); for (const section of displaySections) { setUnitSectionCollapsed(currentUnitName, section.id, 'right', value); } } }; const expandCollapse = (side: Side) => ( <>
); const onSearchChange = useCallback( (e: React.ChangeEvent) => setUnitSearch(currentUnitName, e.target.value), [currentUnitName, setUnitSearch], ); const onSettingsClick = useCallback(() => { setCurrentView('settings'); }, [setCurrentView]); const changeBase = useCallback(() => { setUnitMapping(currentUnitName, leftSymbolRef?.symbolName, null); setSelectedSymbol(leftSymbolRef, null); }, [currentUnitName, leftSymbolRef, setUnitMapping, setSelectedSymbol]); const changeTarget = useCallback(() => { setUnitMapping(currentUnitName, null, rightSymbolRef?.symbolName); setSelectedSymbol(null, rightSymbolRef); }, [currentUnitName, rightSymbolRef, setUnitMapping, setSelectedSymbol]); const onShowMappedSymbolsChange = useCallback( (e: React.ChangeEvent) => setShowMappedSymbols(e.target.checked), [setShowMappedSymbols], ); const showMappedSymbolsInput = (
); const filterRow = ( ); if ( leftColumnView.type !== 'asm' && rightColumnView.type !== 'asm' && leftColumnView.type !== 'data' && rightColumnView.type !== 'data' ) { const unitNameRow = ( {currentUnitName} ); const settingsRow = ( ); return (
{hasProjectConfig ? ( ) : null} Target object
{currentUnitName ? unitNameRow : filterRow} {expandCollapse('left')}
{hasProjectConfig && ( )} Base object
{currentUnitName ? filterRow : settingsRow} {expandCollapse('right')}
); } const matchPercent = leftColumnView.type === 'asm' || leftColumnView.type === 'data' ? leftColumnView.symbol.matchPercent : undefined; return (
{result.isMapping ? ( showMappedSymbolsInput ) : ( )}
{hasProjectConfig && ( )} {result.lastBuilt && ( Last built:{' '} {new Date(result.lastBuilt).toLocaleTimeString('en-US')} )}
{matchPercent !== undefined && ( <> {Math.floor(matchPercent).toFixed(0)}% {' | '} )}
{result.isMapping ? ( filterRow ) : ( )}
); }; const SymbolLabel = ({ view, side, isMapping, currentUnitName, }: { view: ColumnView; side: Side; isMapping: boolean; currentUnitName: string; }) => { switch (view.type) { case 'asm': case 'data': { const displayName = view.symbol.info.demangledName || view.symbol.info.name; return ( {displayName} ); } case 'buildStatus': return ( Build failed ); case 'symbols': { if (isMapping) { switch (side) { case 'left': return ( Choose target symbol ); case 'right': return ( Choose base symbol ); } } return ( {currentUnitName} ); } default: return ( Missing ); } }; const DiffViewContent = ({ result, height, width, leftColumnView, rightColumnView, showMappedSymbols, showHiddenSymbols, }: { result: DiffOutput; height: number; width: number; leftColumnView: ColumnView; rightColumnView: ColumnView; showMappedSymbols: boolean; showHiddenSymbols: boolean; }) => { // Shared symbols view state const [highlightedPath, setHighlightedPath] = useState(null); const [hoverSymbols, setHoverSymbols] = useState< [number | null, number | null] >([null, null]); if (leftColumnView.type === 'asm' && rightColumnView.type === 'asm') { // Render joint function view return ( ); } if (leftColumnView.type === 'data' && rightColumnView.type === 'data') { // Render joint data view return ( ); } let leftColumn = null; let rightColumn = null; if (leftColumnView.type === 'symbols') { leftColumn = ( ); } else if (leftColumnView.type === 'asm') { leftColumn = ( ); } else if (leftColumnView.type === 'data') { leftColumn = ( ); } else if (leftColumnView.type === 'buildStatus') { leftColumn = ( ); } else if (leftColumnView.type === 'none') { leftColumn = ; } if (rightColumnView.type === 'symbols') { rightColumn = ( ); } else if (rightColumnView.type === 'asm') { rightColumn = ( ); } else if (rightColumnView.type === 'data') { rightColumn = ( ); } else if (rightColumnView.type === 'buildStatus') { rightColumn = ( ); } else if (rightColumnView.type === 'none') { rightColumn = ; } return ( <> {leftColumn} {rightColumn} ); }; const NoObjectView = ({ height, width, }: { height: number; width: number; }) => (
No object configured
); const BuildStatusView = ({ status, height, width, }: { status: BuildStatus; height: number; width: number; }) => (
{status.cmdline}
{status.stdout}
{status.stderr}
); export default DiffView;