diff --git a/package.json b/package.json index 3e36665..05dea3a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "displayName": "objdiff", "description": "A local diffing tool for decompilation projects", "publisher": "decomp-dev", - "version": "0.2.0", + "version": "0.3.0", "repository": { "type": "git", "url": "https://github.com/encounter/objdiff-web" @@ -17,13 +17,14 @@ "extension:build": "rsbuild build --env-mode extension", "extension:dev": "rsbuild build -w -m development --env-mode extension", "extension:package": "pnpm run extension:build && vsce package --no-dependencies", - "extension:publish": "pnpm run extension:build && vsce publish --no-dependencies" + "extension:publish": "pnpm run extension:build && vsce publish --no-dependencies", + "postinstall": "tsx update-config.ts" }, "dependencies": { "@vscode/codicons": "^0.0.36", "clsx": "^2.1.1", "memoize-one": "^6.0.0", - "objdiff-wasm": "3.0.0-beta.6", + "objdiff-wasm": "3.0.0-beta.7", "picomatch": "^4.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -277,6 +278,21 @@ null, null ] + }, + "objdiff.mips.registerPrefix": { + "type": "boolean", + "description": "Display MIPS register names with a '$' prefix.", + "default": false + } + } + }, + { + "title": "PowerPC", + "properties": { + "objdiff.ppc.calculatePoolRelocations": { + "type": "boolean", + "description": "Display pooled data references in functions as fake relocations.", + "default": true } } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a830b9d..0a7c171 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^6.0.0 version: 6.0.0 objdiff-wasm: - specifier: 3.0.0-beta.6 - version: 3.0.0-beta.6 + specifier: 3.0.0-beta.7 + version: 3.0.0-beta.7 picomatch: specifier: ^4.0.2 version: 4.0.2 @@ -1400,8 +1400,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - objdiff-wasm@3.0.0-beta.6: - resolution: {integrity: sha512-0Z0V2DwiWQLU5wzacCzZjaHLstnIOFFaIIAny9YQXahpE+5rYtzbDlYfB9Bf74yS6a/E6F74hA/uxG1WwJxbXg==} + objdiff-wasm@3.0.0-beta.7: + resolution: {integrity: sha512-88xQK8b5ZhEjQnp3vOQL7i+29Yd1dIPPXCnTa8eVEMvSfelQigXznpDHkoZRcxDng7xFi8Uuhp4FyFEzh12U5Q==} object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} @@ -3221,7 +3221,7 @@ snapshots: dependencies: boolbase: 1.0.0 - objdiff-wasm@3.0.0-beta.6: {} + objdiff-wasm@3.0.0-beta.7: {} object-inspect@1.13.4: {} diff --git a/webview/common/Header.module.css b/webview/common/Header.module.css index 209be6a..260135a 100644 --- a/webview/common/Header.module.css +++ b/webview/common/Header.module.css @@ -38,3 +38,7 @@ .missing { color: var(--color-blue); } + +.spacer { + flex: 1 1 0; +} diff --git a/webview/state.ts b/webview/state.ts index 8b3bc30..015ec3e 100644 --- a/webview/state.ts +++ b/webview/state.ts @@ -391,10 +391,26 @@ const handleMessage = (event: MessageEvent) => { if (diffConfig == null) { diffConfig = buildDiffConfig(message.configProperties); } - newState.leftObject = diff.Object.parse( - new Uint8Array(message.leftObject), - diffConfig, - ); + try { + newState.leftObject = diff.Object.parse( + new Uint8Array(message.leftObject), + diffConfig, + ); + newState.leftStatus = { + success: true, + cmdline: '', + stdout: '', + stderr: '', + }; + } catch (e) { + newState.leftObject = null; + newState.leftStatus = { + success: false, + cmdline: '', + stdout: 'Failed to parse object', + stderr: e instanceof Error ? e.message : String(e), + }; + } } } if (message.rightObject !== undefined) { @@ -404,10 +420,26 @@ const handleMessage = (event: MessageEvent) => { if (diffConfig == null) { diffConfig = buildDiffConfig(message.configProperties); } - newState.rightObject = diff.Object.parse( - new Uint8Array(message.rightObject), - diffConfig, - ); + try { + newState.rightObject = diff.Object.parse( + new Uint8Array(message.rightObject), + diffConfig, + ); + newState.rightStatus = { + success: true, + cmdline: '', + stdout: '', + stderr: '', + }; + } catch (e) { + newState.rightObject = null; + newState.rightStatus = { + success: false, + cmdline: '', + stdout: 'Failed to parse object', + stderr: e instanceof Error ? e.message : String(e), + }; + } } } for (const k in newState) { diff --git a/webview/views/SymbolsView.tsx b/webview/views/SymbolsView.tsx index 3c89b99..5a9c4cc 100644 --- a/webview/views/SymbolsView.tsx +++ b/webview/views/SymbolsView.tsx @@ -4,7 +4,7 @@ import styles from './SymbolsView.module.css'; import clsx from 'clsx'; import memoizeOne from 'memoize-one'; import { type diff, display } from 'objdiff-wasm'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList, @@ -12,6 +12,7 @@ import { 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 { @@ -23,9 +24,11 @@ import { } from '../state'; import { percentClass, useFontSize } from '../util/util'; +type Side = keyof UnitScrollOffsets; + type SymbolTooltipContent = { symbolRef: display.SectionDisplaySymbol; - side: keyof UnitScrollOffsets; + side: Side; }; const { ContextMenuProvider, useContextMenu } = @@ -75,7 +78,7 @@ const SymbolRow = ({ obj: diff.ObjectDiff; section: SectionData; symbolRef: display.SectionDisplaySymbol; - side: keyof UnitScrollOffsets; + side: Side; style?: React.CSSProperties; }) => { const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol); @@ -164,10 +167,11 @@ const SymbolRow = ({ type SectionData = display.SectionDisplay & { collapsed: boolean }; type ItemData = { + status: BuildStatus | null; obj: diff.ObjectDiff | undefined; itemCount: number; sections: SectionData[]; - side: keyof UnitScrollOffsets; + side: Side; setSectionCollapsed: (section: string, collapsed: boolean) => void; }; @@ -215,14 +219,16 @@ const SymbolListRow = memo( ); const createItemDataFn = ( + status: BuildStatus | null, obj: diff.ObjectDiff | undefined, collapsedSections: Record, search: string | null, - side: keyof UnitScrollOffsets, + side: Side, setSectionCollapsed: (section: string, collapsed: boolean) => void, ): ItemData => { if (!obj) { return { + status, obj, itemCount: 0, sections: [], @@ -264,6 +270,7 @@ const createItemDataFn = ( }); } return { + status, obj, itemCount, sections, @@ -275,11 +282,19 @@ const createItemDataLeft = memoizeOne(createItemDataFn); const createItemDataRight = memoizeOne(createItemDataFn); const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { - const { buildRunning, currentUnit, hasProjectConfig } = useExtensionStore( + const { + buildRunning, + currentUnit, + hasProjectConfig, + leftStatus, + rightStatus, + } = useExtensionStore( useShallow((state) => ({ buildRunning: state.buildRunning, currentUnit: state.currentUnit, hasProjectConfig: state.projectConfig != null, + leftStatus: state.leftStatus, + rightStatus: state.rightStatus, })), ); const currentUnitName = currentUnit?.name || ''; @@ -307,14 +322,14 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { () => useAppStore.getState().getUnitState(currentUnitName).scrollOffsets, [currentUnitName], ); - const setLeftSectionCollapsed = useMemo( - () => (section: string, collapsed: boolean) => { + const setLeftSectionCollapsed = useCallback( + (section: string, collapsed: boolean) => { setUnitSectionCollapsed(currentUnitName, section, 'left', collapsed); }, [currentUnitName, setUnitSectionCollapsed], ); - const setRightSectionCollapsed = useMemo( - () => (section: string, collapsed: boolean) => { + const setRightSectionCollapsed = useCallback( + (section: string, collapsed: boolean) => { setUnitSectionCollapsed(currentUnitName, section, 'right', collapsed); }, [currentUnitName, setUnitSectionCollapsed], @@ -324,12 +339,21 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { height: number, width: number, itemData: ItemData, - side: keyof UnitScrollOffsets, + side: Side, ) => { if (!itemData.obj) { + if (!itemData.status || itemData.status.success) { + return ( +
+ No object configured +
+ ); + } return (
- No object configured +
{itemData.status.cmdline}
+
{itemData.status.stdout}
+
{itemData.status.stderr}
); } @@ -357,6 +381,7 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { const itemSize = useFontSize() * 1.33; const leftItemData = createItemDataLeft( + leftStatus, diff.left, collapsedSections.left, search, @@ -364,6 +389,7 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { setLeftSectionCollapsed, ); const rightItemData = createItemDataRight( + rightStatus, diff.right, collapsedSections.right, search, @@ -371,34 +397,49 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { 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} - -
+ + {currentUnitName} + ); const filterRow = ( -
- { - const value = e.target.value; - setUnitSearch(currentUnitName, value); - }} - /> -
+ setUnitSearch(currentUnitName, e.target.value)} + /> ); const settingsRow = ( -
- -
+ ); return ( @@ -417,7 +458,10 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { ) : null} Target object
- {currentUnitName ? unitNameRow : filterRow} +
+ {currentUnitName ? unitNameRow : filterRow} + {expandCollapse('left')} +
@@ -432,7 +476,10 @@ const SymbolsView = ({ diff }: { diff: diff.DiffResult }) => { )} Base object
- {currentUnitName ? filterRow : settingsRow} +
+ {currentUnitName ? filterRow : settingsRow} + {expandCollapse('right')} +
diff --git a/webview/views/UnitsView.module.css b/webview/views/UnitsView.module.css index 48ab185..180d18c 100644 --- a/webview/views/UnitsView.module.css +++ b/webview/views/UnitsView.module.css @@ -33,6 +33,10 @@ margin-right: 0.5em; } +.indent-highlighted { + border-right-color: var(--color-muted); +} + .toggle { display: inline-block; height: var(--list-row-height); @@ -55,5 +59,5 @@ } .collapsed { - opacity: 0.75; + color: color-mix(in srgb, var(--foreground) 75%, transparent); } diff --git a/webview/views/UnitsView.tsx b/webview/views/UnitsView.tsx index 5f09495..ee035ca 100644 --- a/webview/views/UnitsView.tsx +++ b/webview/views/UnitsView.tsx @@ -2,7 +2,7 @@ import styles from './UnitsView.module.css'; import clsx from 'clsx'; import memoizeOne from 'memoize-one'; -import { memo, useMemo } from 'react'; +import { memo, useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList, @@ -26,6 +26,7 @@ type DirectoryItem = { label: string; id: string; collapsed: boolean; + path: string[]; }; type UnitItem = { @@ -34,6 +35,7 @@ type UnitItem = { label: string; id: string; unit: Unit; + path: string[]; }; type Item = DirectoryItem | UnitItem; @@ -41,10 +43,16 @@ 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 } }: ListChildComponentProps) => { + ({ + index, + style, + data: { items, highlightedPath, setHighlightedPath }, + }: ListChildComponentProps) => { const setCollapsedUnit = useAppStore((state) => state.setCollapsedUnit); const item = items[index]; const classes = [styles.row]; @@ -65,7 +73,15 @@ const UnitRow = memo( } const indentItems = []; for (let i = 0; i < item.indent; i++) { - indentItems.push(); + indentItems.push( + , + ); } if (item.type === 'directory') { indentItems.push( @@ -84,8 +100,22 @@ const UnitRow = memo( className={clsx(classes)} style={style} onClick={() => { - if (item.type === 'unit') setCurrentUnit(item.unit); - else setCollapsedUnit(item.id, !item.collapsed); + 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, + ); + } + }} + onMouseEnter={() => { + setHighlightedPath( + item.type === 'unit' || item.collapsed + ? item.path[item.path.length - 1] + : item.id, + ); }} > {indentItems} @@ -111,10 +141,20 @@ function pushTreeItems(item: TreeItem | UnitItem, out: 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; +}; + const createItemData = memoizeOne( ( config: ProjectConfig | null, collapsedUnits: Record, + highlightedPath: string | null, + setHighlightedPath: (id: string | null) => void, ): ItemData => { const units = config?.units ?? []; const map = new Map(); @@ -125,7 +165,8 @@ const createItemData = memoizeOne( let parent: TreeItem | null = null; for (let i = 0; i < split.length - 1; i++) { const name = split[i]; - const key = split.slice(0, i + 1).join('/'); + const dirPath = split.slice(0, i + 1); + const key = dirPath.join('/'); let item = map.get(key); if (!item) { item = { @@ -135,6 +176,7 @@ const createItemData = memoizeOne( id: key, collapsed: !!collapsedUnits[key], children: [], + path: buildPath(dirPath), }; if (parent) { parent.children.push(item); @@ -151,6 +193,7 @@ const createItemData = memoizeOne( label: split[split.length - 1], id: path, unit, + path: buildPath(split), }; if (parent) { parent.children.push(unitItem); @@ -162,7 +205,12 @@ const createItemData = memoizeOne( for (const item of rootItems) { pushTreeItems(item, items); } - return { unitsCount: units.length, items }; + return { + unitsCount: units.length, + items, + highlightedPath, + setHighlightedPath, + }; }, ); @@ -180,7 +228,13 @@ const UnitsView = () => { [], ); const itemSize = useFontSize() * 1.33; - const itemData = createItemData(config, collapsedUnits); + const [highlightedPath, setHighlightedPath] = useState(null); + const itemData = createItemData( + config, + collapsedUnits, + highlightedPath, + setHighlightedPath, + ); return ( <>