You've already forked objdiff-web
mirror of
https://github.com/encounter/objdiff-web.git
synced 2026-07-10 12:18:37 -07:00
Add symbol mapping & diff UI improvements
This commit is contained in:
+19
-19
@@ -24,34 +24,34 @@
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"clsx": "^2.1.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"objdiff-wasm": "3.0.0-beta.8",
|
||||
"objdiff-wasm": "3.0.0-beta.9",
|
||||
"picomatch": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-tooltip": "^5.28.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.24",
|
||||
"react-window": "^1.8.10",
|
||||
"shescape": "^2.1.1",
|
||||
"zustand": "^5.0.2"
|
||||
"react-tooltip": "^5.28.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.11",
|
||||
"shescape": "^2.1.3",
|
||||
"zustand": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.3",
|
||||
"@rsbuild/core": "^1.1.8",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-type-check": "^1.1.0",
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@rsbuild/core": "^1.3.20",
|
||||
"@rsbuild/plugin-react": "^1.3.1",
|
||||
"@rsbuild/plugin-type-check": "^1.2.2",
|
||||
"@rsbuild/plugin-typed-css-modules": "^1.0.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/picomatch": "^3.0.1",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/picomatch": "^3.0.2",
|
||||
"@types/react": "^18.3.21",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/vscode": "^1.96.0",
|
||||
"@types/vscode": "^1.100.0",
|
||||
"@types/vscode-webview": "^1.57.5",
|
||||
"@vscode/test-cli": "^0.0.10",
|
||||
"@vscode/test-electron": "^2.4.1",
|
||||
"@vscode/vsce": "^3.2.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
"@vscode/test-electron": "^2.5.2",
|
||||
"@vscode/vsce": "^3.4.0",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"main": "./dist/extension.js",
|
||||
"files": [
|
||||
|
||||
Generated
+972
-278
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ export type StateMessage = {
|
||||
leftObject?: ArrayBuffer | null;
|
||||
rightObject?: ArrayBuffer | null;
|
||||
projectConfig?: ProjectConfig | null;
|
||||
diffLabel?: string | null;
|
||||
};
|
||||
|
||||
// decomp.me colors
|
||||
|
||||
+1
-3
@@ -15,9 +15,7 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
/* type checking */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src", "webview", "shared"]
|
||||
}
|
||||
|
||||
@@ -49,6 +49,14 @@
|
||||
--vscode-list-hoverBackground,
|
||||
light-dark(#f2f2f2, #2a2d2e)
|
||||
);
|
||||
--list-row-selection-background: var(
|
||||
--vscode-list-inactiveSelectionBackground,
|
||||
light-dark(#e4e6f1, #37373d)
|
||||
);
|
||||
--list-row-highlight-background: var(
|
||||
--vscode-list-activeSelectionBackground,
|
||||
light-dark(#e8e8e8, #04395e)
|
||||
);
|
||||
|
||||
--line-number-foreground: var(--vscode-editorLineNumber-foreground, #6e7681);
|
||||
|
||||
|
||||
+72
-41
@@ -1,50 +1,77 @@
|
||||
import './App.css';
|
||||
|
||||
import type { diff, display } from 'objdiff-wasm';
|
||||
import type { diff } from 'objdiff-wasm';
|
||||
import { useMemo } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useDiff } from './diff';
|
||||
import { useAppStore, useExtensionStore } from './state';
|
||||
import type { SymbolRefByName } from './state';
|
||||
import FunctionView from './views/FunctionView';
|
||||
import DiffView from './views/DiffView';
|
||||
import SettingsView from './views/SettingsView';
|
||||
import SymbolsView from './views/SymbolsView';
|
||||
import UnitsView from './views/UnitsView';
|
||||
|
||||
const findSymbol = (
|
||||
obj: diff.ObjectDiff | undefined,
|
||||
symbolRef: SymbolRefByName | null,
|
||||
): display.SectionDisplaySymbol | null => {
|
||||
if (!obj || !symbolRef) {
|
||||
return null;
|
||||
}
|
||||
const idx = obj.findSymbol(
|
||||
symbolRef.symbolName,
|
||||
symbolRef.sectionName ?? undefined,
|
||||
);
|
||||
if (idx !== undefined) {
|
||||
return {
|
||||
symbol: idx,
|
||||
isMappingSymbol: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const { buildRunning, result, config, ready } = useExtensionStore(
|
||||
const {
|
||||
buildRunning,
|
||||
configProperties,
|
||||
currentUnit,
|
||||
leftStatus,
|
||||
rightStatus,
|
||||
leftObject,
|
||||
rightObject,
|
||||
config,
|
||||
ready,
|
||||
} = useExtensionStore(
|
||||
useShallow((state) => ({
|
||||
buildRunning: state.buildRunning,
|
||||
result: state.result,
|
||||
configProperties: state.configProperties,
|
||||
currentUnit: state.currentUnit,
|
||||
leftStatus: state.leftStatus,
|
||||
rightStatus: state.rightStatus,
|
||||
leftObject: state.leftObject,
|
||||
rightObject: state.rightObject,
|
||||
config: state.projectConfig,
|
||||
ready: state.ready,
|
||||
})),
|
||||
);
|
||||
const { leftSymbolRef, rightSymbolRef, currentView } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
leftSymbolRef: state.leftSymbol,
|
||||
rightSymbolRef: state.rightSymbol,
|
||||
currentView: state.currentView,
|
||||
})),
|
||||
const { leftSymbolRef, rightSymbolRef, currentView, mappings } = useAppStore(
|
||||
useShallow((state) => {
|
||||
const unitState = state.getUnitState(currentUnit?.name ?? '');
|
||||
return {
|
||||
leftSymbolRef: state.leftSymbol,
|
||||
rightSymbolRef: state.rightSymbol,
|
||||
currentView: state.currentView,
|
||||
mappings: unitState?.mappings,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const mappingConfig = useMemo(() => {
|
||||
const result: diff.MappingConfig = {
|
||||
mappings: mappings == null ? [] : Object.entries(mappings),
|
||||
selectingLeft: undefined,
|
||||
selectingRight: undefined,
|
||||
};
|
||||
if (leftSymbolRef && !rightSymbolRef) {
|
||||
result.selectingRight = leftSymbolRef.symbolName;
|
||||
result.mappings = result.mappings.filter(
|
||||
([left, _]) => left !== leftSymbolRef.symbolName,
|
||||
);
|
||||
}
|
||||
if (!leftSymbolRef && rightSymbolRef) {
|
||||
result.selectingLeft = rightSymbolRef.symbolName;
|
||||
result.mappings = result.mappings.filter(
|
||||
([_, right]) => right !== rightSymbolRef.symbolName,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}, [leftSymbolRef, rightSymbolRef, mappings]);
|
||||
const result = useDiff({
|
||||
leftStatus,
|
||||
rightStatus,
|
||||
leftObject,
|
||||
rightObject,
|
||||
configProperties,
|
||||
mappingConfig,
|
||||
});
|
||||
|
||||
if (!ready) {
|
||||
// Uses panel background color to avoid flashing
|
||||
@@ -53,15 +80,19 @@ const App = () => {
|
||||
|
||||
switch (currentView) {
|
||||
case 'main':
|
||||
if (result) {
|
||||
const leftSymbol = findSymbol(result.left, leftSymbolRef);
|
||||
const rightSymbol = findSymbol(result.right, rightSymbolRef);
|
||||
if (leftSymbol || rightSymbol) {
|
||||
return (
|
||||
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
|
||||
);
|
||||
}
|
||||
return <SymbolsView diff={result} />;
|
||||
if (
|
||||
result.leftStatus ||
|
||||
result.rightStatus ||
|
||||
result.diff.left ||
|
||||
result.diff.right
|
||||
) {
|
||||
return (
|
||||
<DiffView
|
||||
result={result}
|
||||
leftSymbolRef={leftSymbolRef}
|
||||
rightSymbolRef={rightSymbolRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (buildRunning) {
|
||||
|
||||
@@ -23,9 +23,14 @@ export type ContextMenuState<T> = Readonly<{
|
||||
data: T;
|
||||
}>;
|
||||
|
||||
type ContextMenuProps<T> = React.PropsWithChildren<{
|
||||
export type ContextMenuRender<T> = (
|
||||
state: ContextMenuState<T>,
|
||||
close: () => void,
|
||||
) => React.ReactNode;
|
||||
|
||||
export type ContextMenuProps<T> = React.PropsWithChildren<{
|
||||
className?: string;
|
||||
render?: (state: ContextMenuState<T>, close: () => void) => React.ReactNode;
|
||||
render?: ContextMenuRender<T>;
|
||||
}>;
|
||||
|
||||
export function createContextMenu<T>(): {
|
||||
@@ -55,9 +60,7 @@ export function createContextMenu<T>(): {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setState(null);
|
||||
}, []);
|
||||
const close = useCallback(() => setState(null), []);
|
||||
|
||||
const closeIfOutside = useCallback(
|
||||
(e: Event) => {
|
||||
@@ -108,9 +111,7 @@ export function createContextMenu<T>(): {
|
||||
observer.observe(state.target.parentNode, {
|
||||
childList: true,
|
||||
});
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
}, [state?.target, close]);
|
||||
|
||||
@@ -186,12 +187,8 @@ export function renderContextItems(
|
||||
className={styles.contextMenuItem}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(item.val.value).then(
|
||||
() => {
|
||||
close();
|
||||
},
|
||||
(e) => {
|
||||
console.warn('Failed to copy:', e);
|
||||
},
|
||||
() => close(),
|
||||
(e) => console.warn('Failed to copy:', e),
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -42,3 +42,8 @@
|
||||
.spacer {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
import styles from './TooltipShared.module.css';
|
||||
|
||||
import type { display } from 'objdiff-wasm';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
type TooltipProps = {
|
||||
'data-tooltip-id': string;
|
||||
'data-tooltip-content': string;
|
||||
};
|
||||
|
||||
export type TooltipCallback<T> = (content: T) => display.HoverItem[] | null;
|
||||
|
||||
export function createTooltip<T>(): {
|
||||
Tooltip: React.FC<{
|
||||
callback: TooltipCallback<T>;
|
||||
}>;
|
||||
useTooltip: (content: T) => TooltipProps;
|
||||
} {
|
||||
const id = generateRandomString(10);
|
||||
return {
|
||||
Tooltip: ({ callback }) => {
|
||||
const callbackMemo = useCallback(
|
||||
(content: string) => {
|
||||
if (!content) {
|
||||
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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const TooltipShared = ({
|
||||
id,
|
||||
callback,
|
||||
@@ -74,4 +117,12 @@ const TooltipContent = ({ items }: { items: display.HoverItem[] }) => {
|
||||
|
||||
const TooltipContentMemo = React.memo(TooltipContent);
|
||||
|
||||
export default TooltipShared;
|
||||
function generateRandomString(length: number): string {
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { diff } from 'objdiff-wasm';
|
||||
import { useMemo } from 'react';
|
||||
import type { ConfigProperties } from '../shared/config';
|
||||
import type { BuildStatus } from '../shared/messages';
|
||||
import { buildDiffConfig } from './state';
|
||||
|
||||
export type DiffInput = {
|
||||
leftStatus: BuildStatus | null;
|
||||
rightStatus: BuildStatus | null;
|
||||
leftObject: ArrayBuffer | null;
|
||||
rightObject: ArrayBuffer | null;
|
||||
configProperties: ConfigProperties;
|
||||
mappingConfig: diff.MappingConfig;
|
||||
};
|
||||
|
||||
export type DiffOutput = {
|
||||
leftStatus: BuildStatus | null;
|
||||
rightStatus: BuildStatus | null;
|
||||
diff: diff.DiffResult | null;
|
||||
lastBuilt: number | null;
|
||||
isMapping: boolean;
|
||||
};
|
||||
|
||||
export const useDiff = ({
|
||||
leftStatus,
|
||||
rightStatus,
|
||||
leftObject,
|
||||
rightObject,
|
||||
configProperties,
|
||||
mappingConfig,
|
||||
}: DiffInput) =>
|
||||
useMemo(() => {
|
||||
const start = performance.now();
|
||||
const diffConfig = buildDiffConfig(configProperties);
|
||||
let left: diff.Object | undefined;
|
||||
let right: diff.Object | undefined;
|
||||
try {
|
||||
left =
|
||||
leftObject?.byteLength && leftStatus?.success
|
||||
? diff.Object.parse(new Uint8Array(leftObject), diffConfig)
|
||||
: undefined;
|
||||
} catch (e) {
|
||||
leftStatus = {
|
||||
success: false,
|
||||
cmdline: leftStatus?.cmdline ?? '',
|
||||
stdout: '',
|
||||
stderr: `Failed to parse left object: ${e}`,
|
||||
};
|
||||
}
|
||||
try {
|
||||
right =
|
||||
rightObject?.byteLength && rightStatus?.success
|
||||
? diff.Object.parse(new Uint8Array(rightObject), diffConfig)
|
||||
: undefined;
|
||||
} catch (e) {
|
||||
rightStatus = {
|
||||
success: false,
|
||||
cmdline: rightStatus?.cmdline ?? '',
|
||||
stdout: '',
|
||||
stderr: `Failed to parse right object: ${e}`,
|
||||
};
|
||||
}
|
||||
const result = diff.runDiff(left, right, diffConfig, mappingConfig);
|
||||
const end = performance.now();
|
||||
console.debug('Diff time:', end - start, 'ms');
|
||||
return {
|
||||
leftStatus,
|
||||
rightStatus,
|
||||
diff: result,
|
||||
lastBuilt: Date.now(),
|
||||
isMapping:
|
||||
mappingConfig.selectingLeft != null ||
|
||||
mappingConfig.selectingRight != null,
|
||||
};
|
||||
}, [
|
||||
leftStatus,
|
||||
rightStatus,
|
||||
leftObject,
|
||||
rightObject,
|
||||
configProperties,
|
||||
mappingConfig,
|
||||
]);
|
||||
+62
-18
@@ -36,6 +36,11 @@ function sendMessage(data: InboundMessage) {
|
||||
let resolvedProjectConfig: ProjectConfig | null = null;
|
||||
|
||||
async function fetchFile(path: string): Promise<Response> {
|
||||
if (!path) {
|
||||
return Promise.resolve(
|
||||
new Response(null, { status: 404, statusText: 'Not Found' }),
|
||||
);
|
||||
}
|
||||
const search = new URLSearchParams();
|
||||
search.set('path', path);
|
||||
const response = await fetch(`/api/get?${search.toString()}`);
|
||||
@@ -59,6 +64,54 @@ if (serializedConfigProperties) {
|
||||
configProperties = JSON.parse(serializedConfigProperties);
|
||||
}
|
||||
|
||||
async function fetchUnitFiles(unit: Unit, out: StateMessage): Promise<void> {
|
||||
const leftPromise = fetchFile(unit.target_path ?? '')
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then(
|
||||
(o) => {
|
||||
out.leftObject = o;
|
||||
out.leftStatus = {
|
||||
success: true,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
(e) => {
|
||||
out.leftObject = null;
|
||||
out.leftStatus = {
|
||||
success: false,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: `Failed to fetch object: ${e}`,
|
||||
};
|
||||
},
|
||||
);
|
||||
const rightPromise = fetchFile(unit.base_path ?? '')
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then(
|
||||
(o) => {
|
||||
out.rightObject = o;
|
||||
out.rightStatus = {
|
||||
success: true,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
};
|
||||
},
|
||||
(e) => {
|
||||
out.rightObject = null;
|
||||
out.rightStatus = {
|
||||
success: false,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: `Failed to fetch object: ${e}`,
|
||||
};
|
||||
},
|
||||
);
|
||||
await Promise.all([leftPromise, rightPromise]);
|
||||
}
|
||||
|
||||
async function handleMessage(msg: OutboundMessage): Promise<void> {
|
||||
switch (msg.type) {
|
||||
case 'ready': {
|
||||
@@ -70,17 +123,14 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
||||
buildRunning: false,
|
||||
configProperties,
|
||||
currentUnit: null,
|
||||
leftStatus: null,
|
||||
rightStatus: null,
|
||||
leftObject: null,
|
||||
rightObject: null,
|
||||
projectConfig: resolvedProjectConfig,
|
||||
};
|
||||
if (lastUnit) {
|
||||
out.leftObject = await fetchFile(lastUnit.target_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
out.rightObject = await fetchFile(lastUnit.base_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
await fetchUnitFiles(lastUnit, out);
|
||||
}
|
||||
sendMessage(out);
|
||||
break;
|
||||
@@ -90,16 +140,13 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
||||
const out: StateMessage = {
|
||||
type: 'state',
|
||||
buildRunning: false,
|
||||
leftStatus: null,
|
||||
rightStatus: null,
|
||||
leftObject: null,
|
||||
rightObject: null,
|
||||
};
|
||||
if (lastUnit) {
|
||||
out.leftObject = await fetchFile(lastUnit.target_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
out.rightObject = await fetchFile(lastUnit.base_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
await fetchUnitFiles(lastUnit, out);
|
||||
}
|
||||
sendMessage(out);
|
||||
break;
|
||||
@@ -115,17 +162,14 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
||||
type: 'state',
|
||||
buildRunning: false,
|
||||
currentUnit: unit,
|
||||
leftStatus: null,
|
||||
rightStatus: null,
|
||||
leftObject: null,
|
||||
rightObject: null,
|
||||
};
|
||||
if (unit) {
|
||||
sendMessage({ type: 'state', buildRunning: true });
|
||||
out.leftObject = await fetchFile(unit.target_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
out.rightObject = await fetchFile(unit.base_path ?? '').then((r) =>
|
||||
r.arrayBuffer(),
|
||||
);
|
||||
await fetchUnitFiles(unit, out);
|
||||
}
|
||||
lastUnit = unit;
|
||||
localStorage.setItem('lastUnit', JSON.stringify(unit));
|
||||
|
||||
+53
-130
@@ -33,20 +33,16 @@ export type SymbolRefByName = {
|
||||
sectionName: string | null;
|
||||
};
|
||||
|
||||
export type UnitScrollOffsets = {
|
||||
left: number;
|
||||
right: number;
|
||||
};
|
||||
export type UnitCollapsedSections = {
|
||||
left: Record<string, boolean>;
|
||||
right: Record<string, boolean>;
|
||||
};
|
||||
export type Side = 'left' | 'right';
|
||||
export type UnitScrollOffsets = { [key in Side]: number };
|
||||
export type UnitCollapsedSections = { [key in Side]: Record<string, boolean> };
|
||||
|
||||
export type UnitState = {
|
||||
scrollOffsets: UnitScrollOffsets;
|
||||
symbolScrollOffsets: Record<string, number>;
|
||||
collapsedSections: UnitCollapsedSections;
|
||||
search: string | null;
|
||||
mappings: Record<string, string>;
|
||||
};
|
||||
const defaultUnitState: UnitState = {
|
||||
scrollOffsets: { left: 0, right: 0 },
|
||||
@@ -56,6 +52,7 @@ const defaultUnitState: UnitState = {
|
||||
right: {},
|
||||
},
|
||||
search: null,
|
||||
mappings: {},
|
||||
};
|
||||
|
||||
export type CurrentView = 'main' | 'settings';
|
||||
@@ -68,7 +65,7 @@ export interface AppState {
|
||||
currentView: CurrentView;
|
||||
collapsedUnits: Record<string, boolean>;
|
||||
|
||||
getUnitState(unit: string): UnitState;
|
||||
getUnitState(unit: string | null | undefined): UnitState;
|
||||
setSelectedSymbol: (
|
||||
leftSymbol: SymbolRefByName | null,
|
||||
rightSymbol: SymbolRefByName | null,
|
||||
@@ -91,6 +88,11 @@ export interface AppState {
|
||||
) => void;
|
||||
setUnitSearch: (unit: string, search: string | null) => void;
|
||||
setUnitsScrollOffset: (offset: number) => void;
|
||||
setUnitMapping: (
|
||||
unit: string,
|
||||
left: string | null | undefined,
|
||||
right: string | null | undefined,
|
||||
) => void;
|
||||
setHighlight: (highlight: HighlightState) => void;
|
||||
setCurrentView: (view: CurrentView) => void;
|
||||
setCollapsedUnit: (unit: string, collapsed: boolean) => void;
|
||||
@@ -123,7 +125,9 @@ export const useAppStore = create<AppState>((set) => {
|
||||
collapsedUnits: {},
|
||||
|
||||
getUnitState(unit) {
|
||||
return this.unitStates[unit] ?? defaultUnitState;
|
||||
return unit == null
|
||||
? defaultUnitState
|
||||
: (this.unitStates[unit] ?? defaultUnitState);
|
||||
},
|
||||
setSelectedSymbol: (leftSymbol, rightSymbol) =>
|
||||
set({ leftSymbol, rightSymbol }),
|
||||
@@ -160,6 +164,23 @@ export const useAppStore = create<AppState>((set) => {
|
||||
search,
|
||||
})),
|
||||
setUnitsScrollOffset: (offset) => set({ unitsScrollOffset: offset }),
|
||||
setUnitMapping: (unit, left, right) =>
|
||||
setUnitState(unit, (state) => {
|
||||
const newMappings = Object.fromEntries(
|
||||
Object.entries(state.mappings || {}).filter(
|
||||
([k, v]) =>
|
||||
(left == null || k !== left) && (right == null || v !== right),
|
||||
),
|
||||
);
|
||||
if (left != null && right != null && left !== right) {
|
||||
// Only add new mapping if both sides are defined and different
|
||||
newMappings[left] = right;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
mappings: newMappings,
|
||||
};
|
||||
}),
|
||||
setHighlight: (highlight: HighlightState) => set({ highlight }),
|
||||
setCurrentView: (currentView) => set({ currentView }),
|
||||
setCollapsedUnit: (unit, collapsed) =>
|
||||
@@ -178,14 +199,11 @@ export type ExtensionState = {
|
||||
currentUnit: Unit | null;
|
||||
leftStatus: BuildStatus | null;
|
||||
rightStatus: BuildStatus | null;
|
||||
leftObject: diff.Object | null;
|
||||
rightObject: diff.Object | null;
|
||||
result: diff.DiffResult | null;
|
||||
lastBuilt: number | null;
|
||||
leftObject: ArrayBuffer | null;
|
||||
rightObject: ArrayBuffer | null;
|
||||
projectConfig: ProjectConfig | null;
|
||||
diffLabel: string | null;
|
||||
ready: boolean;
|
||||
|
||||
setResult: (result: diff.DiffResult | null | undefined) => void;
|
||||
};
|
||||
export const useExtensionStore = create(
|
||||
subscribeWithSelector<ExtensionState>((set) => ({
|
||||
@@ -197,18 +215,9 @@ export const useExtensionStore = create(
|
||||
rightStatus: null,
|
||||
leftObject: null,
|
||||
rightObject: null,
|
||||
result: null,
|
||||
lastBuilt: null,
|
||||
projectConfig: null,
|
||||
diffLabel: null,
|
||||
ready: false,
|
||||
|
||||
setResult: (result: diff.DiffResult | null | undefined) => {
|
||||
if (result === undefined) {
|
||||
set({ lastBuilt: Date.now() });
|
||||
} else {
|
||||
set({ result, lastBuilt: Date.now() });
|
||||
}
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -275,7 +284,8 @@ subscriptions.push(
|
||||
k !== 'setUnitsScrollOffset' &&
|
||||
k !== 'setHighlight' &&
|
||||
k !== 'setCurrentView' &&
|
||||
k !== 'setCollapsedUnit'
|
||||
k !== 'setCollapsedUnit' &&
|
||||
k !== 'setUnitMapping'
|
||||
) {
|
||||
serialized[k] = state[k] as any;
|
||||
}
|
||||
@@ -330,117 +340,30 @@ export function buildDiffConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
// Run diff when objects or config properties change
|
||||
subscriptions.push(
|
||||
useExtensionStore.subscribe(
|
||||
(state) => ({
|
||||
leftObject: state.leftObject,
|
||||
rightObject: state.rightObject,
|
||||
configProperties: state.configProperties,
|
||||
setResult: state.setResult,
|
||||
}),
|
||||
(
|
||||
{ leftObject, rightObject, configProperties, setResult },
|
||||
{
|
||||
leftObject: prevLeftObject,
|
||||
rightObject: prevRightObject,
|
||||
configProperties: prevConfigProperties,
|
||||
},
|
||||
) => {
|
||||
if (leftObject == null && rightObject == null) {
|
||||
setResult(null);
|
||||
} else if (
|
||||
configProperties === prevConfigProperties &&
|
||||
leftObject?.hash() === prevLeftObject?.hash() &&
|
||||
rightObject?.hash() === prevRightObject?.hash()
|
||||
) {
|
||||
// Nothing changed, but update build time
|
||||
setResult(undefined);
|
||||
} else {
|
||||
const start = performance.now();
|
||||
const diffConfig = buildDiffConfig(configProperties);
|
||||
const result = diff.runDiff(
|
||||
leftObject ?? undefined,
|
||||
rightObject ?? undefined,
|
||||
diffConfig,
|
||||
);
|
||||
const end = performance.now();
|
||||
console.debug('Diff time:', end - start, 'ms');
|
||||
setResult(result);
|
||||
}
|
||||
},
|
||||
{ equalityFn: shallow },
|
||||
),
|
||||
);
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data as InboundMessage;
|
||||
if (message.type === 'state') {
|
||||
console.log('Received state message', message);
|
||||
const newState: Partial<ExtensionState> = {
|
||||
...message,
|
||||
leftObject: undefined,
|
||||
rightObject: undefined,
|
||||
result: undefined,
|
||||
ready: true,
|
||||
};
|
||||
let diffConfig: diff.DiffConfig | null = null;
|
||||
if (message.leftObject !== undefined) {
|
||||
if (message.leftObject == null) {
|
||||
newState.leftObject = null;
|
||||
} else {
|
||||
if (diffConfig == null) {
|
||||
diffConfig = buildDiffConfig(message.configProperties);
|
||||
}
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Backwards compatibility for decomp.me
|
||||
if (message.leftObject && !message.leftStatus) {
|
||||
newState.leftStatus = {
|
||||
success: true,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
if (message.rightObject !== undefined) {
|
||||
if (message.rightObject == null) {
|
||||
newState.rightObject = null;
|
||||
} else {
|
||||
if (diffConfig == null) {
|
||||
diffConfig = buildDiffConfig(message.configProperties);
|
||||
}
|
||||
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),
|
||||
};
|
||||
}
|
||||
}
|
||||
if (message.rightObject && !message.rightStatus) {
|
||||
newState.rightStatus = {
|
||||
success: true,
|
||||
cmdline: '',
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
for (const k in newState) {
|
||||
const key = k as keyof typeof newState;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.content {
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex: 1 1 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.no-object {
|
||||
color: var(--color-blue);
|
||||
|
||||
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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,6 @@
|
||||
.instruction-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
height: var(--list-row-height);
|
||||
}
|
||||
|
||||
.instruction-cell {
|
||||
|
||||
+83
-197
@@ -4,26 +4,20 @@ import styles from './FunctionView.module.css';
|
||||
import clsx from 'clsx';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { type diff, display } from 'objdiff-wasm';
|
||||
import { memo, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { FixedSizeList, areEqual } from 'react-window';
|
||||
import type { ListChildComponentProps } from 'react-window';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { createContextMenu, renderContextItems } from '../common/ContextMenu';
|
||||
import TooltipShared from '../common/TooltipShared';
|
||||
import {
|
||||
buildDiffConfig,
|
||||
runBuild,
|
||||
useAppStore,
|
||||
useExtensionStore,
|
||||
} from '../state';
|
||||
import { createTooltip } from '../common/TooltipShared';
|
||||
import { buildDiffConfig, useAppStore, useExtensionStore } from '../state';
|
||||
import {
|
||||
type HighlightState,
|
||||
highlightColumn,
|
||||
highlightMatches,
|
||||
updateHighlight,
|
||||
} from '../util/highlight';
|
||||
import { percentClass, useFontSize } from '../util/util';
|
||||
import { useFontSize } from '../util/util';
|
||||
|
||||
const ROTATION_CLASSES = [
|
||||
styles.rotation0,
|
||||
@@ -37,10 +31,20 @@ const ROTATION_CLASSES = [
|
||||
styles.rotation8,
|
||||
];
|
||||
|
||||
const { ContextMenuProvider, useContextMenu } = createContextMenu<{
|
||||
export type InstructionTooltipContent = {
|
||||
column: number;
|
||||
row: number;
|
||||
}>();
|
||||
};
|
||||
|
||||
export const {
|
||||
Tooltip: InstructionTooltip,
|
||||
useTooltip: useInstructionTooltip,
|
||||
} = createTooltip<InstructionTooltipContent>();
|
||||
|
||||
export const {
|
||||
ContextMenuProvider: InstructionContextMenuProvider,
|
||||
useContextMenu: useInstructionContextMenu,
|
||||
} = createContextMenu<InstructionTooltipContent>();
|
||||
|
||||
const AsmCell = ({
|
||||
obj,
|
||||
@@ -53,15 +57,28 @@ const AsmCell = ({
|
||||
}: {
|
||||
obj: diff.ObjectDiff | undefined;
|
||||
config: diff.DiffConfig;
|
||||
symbol: display.SectionDisplaySymbol | null;
|
||||
symbol: display.SymbolRef | null;
|
||||
row: number;
|
||||
column: number;
|
||||
highlight: HighlightState;
|
||||
setHighlight: (highlight: HighlightState) => void;
|
||||
}) => {
|
||||
const onContextMenu = useContextMenu();
|
||||
const onContextMenu = useInstructionContextMenu();
|
||||
const tooltipContent: InstructionTooltipContent = useMemo(
|
||||
() => ({
|
||||
column,
|
||||
row,
|
||||
}),
|
||||
[column, row],
|
||||
);
|
||||
const tooltipProps = useInstructionTooltip(tooltipContent);
|
||||
const onContextMenuMemo = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement>) => onContextMenu(e, tooltipContent),
|
||||
[onContextMenu, tooltipContent],
|
||||
);
|
||||
|
||||
if (!obj || !symbol) {
|
||||
return <div className={styles.instructionCell} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const highlight = highlightColumn(highlightState, column);
|
||||
@@ -198,33 +215,24 @@ const AsmCell = ({
|
||||
return <div className={clsx(classes)} />;
|
||||
}
|
||||
|
||||
const tooltipContent: InstructionTooltipContent = { column, row };
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes)}
|
||||
data-tooltip-id="instruction-tooltip"
|
||||
data-tooltip-content={JSON.stringify(tooltipContent)}
|
||||
onContextMenu={(e) => onContextMenu(e, { column, row })}
|
||||
onContextMenu={onContextMenuMemo}
|
||||
{...tooltipProps}
|
||||
>
|
||||
{out}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type InstructionTooltipContent = {
|
||||
column: number;
|
||||
row: number;
|
||||
};
|
||||
|
||||
type ItemData = {
|
||||
itemCount: number;
|
||||
symbolName: string;
|
||||
result: diff.DiffResult;
|
||||
config: diff.DiffConfig;
|
||||
matchPercent?: number;
|
||||
left: display.SectionDisplaySymbol | null;
|
||||
leftSymbol: display.SymbolDisplay | null;
|
||||
right: display.SectionDisplaySymbol | null;
|
||||
rightSymbol: display.SymbolDisplay | null;
|
||||
highlight: HighlightState;
|
||||
setHighlight: (highlight: HighlightState) => void;
|
||||
@@ -234,7 +242,7 @@ const AsmRow = memo(
|
||||
({
|
||||
index,
|
||||
style,
|
||||
data: { result, config, left, right, highlight, setHighlight },
|
||||
data: { result, config, leftSymbol, rightSymbol, highlight, setHighlight },
|
||||
}: ListChildComponentProps<ItemData>) => {
|
||||
return (
|
||||
<div
|
||||
@@ -254,7 +262,7 @@ const AsmRow = memo(
|
||||
<AsmCell
|
||||
obj={result.left}
|
||||
config={config}
|
||||
symbol={left}
|
||||
symbol={leftSymbol?.info.id ?? null}
|
||||
row={index}
|
||||
column={0}
|
||||
highlight={highlight}
|
||||
@@ -263,7 +271,7 @@ const AsmRow = memo(
|
||||
<AsmCell
|
||||
obj={result.right}
|
||||
config={config}
|
||||
symbol={right}
|
||||
symbol={rightSymbol?.info.id ?? null}
|
||||
row={index}
|
||||
column={1}
|
||||
highlight={highlight}
|
||||
@@ -278,20 +286,16 @@ const AsmRow = memo(
|
||||
const createItemData = memoizeOne(
|
||||
(
|
||||
result: diff.DiffResult,
|
||||
left: display.SectionDisplaySymbol | null,
|
||||
right: display.SectionDisplaySymbol | null,
|
||||
leftSymbol: display.SymbolDisplay | null,
|
||||
rightSymbol: display.SymbolDisplay | null,
|
||||
highlight: HighlightState,
|
||||
setHighlight: (highlight: HighlightState) => 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 = ({
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const displayName = symbol.demangledName || symbol.name;
|
||||
const displayName = symbol.info.demangledName || symbol.info.name;
|
||||
return (
|
||||
<span
|
||||
className={clsx(headerStyles.label, headerStyles.emphasized)}
|
||||
@@ -333,36 +335,35 @@ const SymbolLabel = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FunctionView = ({
|
||||
export const InstructionList = ({
|
||||
height,
|
||||
width,
|
||||
diff,
|
||||
left,
|
||||
right,
|
||||
leftSymbol,
|
||||
rightSymbol,
|
||||
}: {
|
||||
height: number;
|
||||
width: number;
|
||||
diff: diff.DiffResult;
|
||||
left: display.SectionDisplaySymbol | null;
|
||||
right: display.SectionDisplaySymbol | null;
|
||||
leftSymbol: display.SymbolDisplay | null;
|
||||
rightSymbol: display.SymbolDisplay | null;
|
||||
}) => {
|
||||
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 (
|
||||
<>
|
||||
<div className={headerStyles.header}>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
<button title="Back" onClick={() => setSelectedSymbol(null, null)}>
|
||||
<span className="codicon codicon-chevron-left" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={headerStyles.row}>
|
||||
<SymbolLabel symbol={itemData.leftSymbol} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={headerStyles.column}>
|
||||
<div className={headerStyles.row}>
|
||||
{hasProjectConfig && (
|
||||
<button
|
||||
title="Build"
|
||||
onClick={() => runBuild()}
|
||||
disabled={buildRunning}
|
||||
>
|
||||
<span className="codicon codicon-refresh" />
|
||||
</button>
|
||||
)}
|
||||
{lastBuilt && (
|
||||
<span className={headerStyles.label}>
|
||||
Last built: {new Date(lastBuilt).toLocaleTimeString('en-US')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={headerStyles.row}>
|
||||
{itemData.matchPercent !== undefined && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
headerStyles.label,
|
||||
percentClass(itemData.matchPercent),
|
||||
)}
|
||||
>
|
||||
{Math.floor(itemData.matchPercent).toFixed(0)}%
|
||||
</span>
|
||||
{' | '}
|
||||
</>
|
||||
)}
|
||||
<SymbolLabel symbol={itemData.rightSymbol} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.instructionList}>
|
||||
<ContextMenuProvider
|
||||
render={({ data }, close) => {
|
||||
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);
|
||||
}}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={itemData.itemCount}
|
||||
itemSize={itemSize}
|
||||
width={width}
|
||||
itemData={itemData}
|
||||
overscanCount={20}
|
||||
onScroll={(e) => {
|
||||
setSymbolScrollOffset(
|
||||
currentUnitName,
|
||||
itemData.symbolName,
|
||||
e.scrollOffset,
|
||||
);
|
||||
}}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
>
|
||||
{AsmRow}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</ContextMenuProvider>
|
||||
</div>
|
||||
<TooltipShared
|
||||
id="instruction-tooltip"
|
||||
callback={(content) => {
|
||||
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,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={itemData.itemCount}
|
||||
itemSize={itemSize}
|
||||
width={width}
|
||||
itemData={itemData}
|
||||
overscanCount={20}
|
||||
onScroll={(e) => {
|
||||
setSymbolScrollOffset(
|
||||
currentUnitName,
|
||||
itemData.symbolName,
|
||||
e.scrollOffset,
|
||||
);
|
||||
}}
|
||||
initialScrollOffset={initialScrollOffset}
|
||||
>
|
||||
{AsmRow}
|
||||
</FixedSizeList>
|
||||
);
|
||||
};
|
||||
|
||||
export default FunctionView;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+286
-358
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ListChildComponentProps } from 'react-window';
|
||||
import styles from './TreeView.module.css';
|
||||
|
||||
export type NodeInner<T> = {
|
||||
id: string;
|
||||
indent: number;
|
||||
path: string[];
|
||||
data: T;
|
||||
};
|
||||
|
||||
export type BranchNode<T> = NodeInner<T> & {
|
||||
type: 'branch';
|
||||
collapsed: boolean;
|
||||
};
|
||||
|
||||
export type LeafNode<T> = NodeInner<T> & {
|
||||
type: 'leaf';
|
||||
};
|
||||
|
||||
export type Node<B, L> = BranchNode<B> | LeafNode<L>;
|
||||
|
||||
export type TreeData<B, L> = {
|
||||
leafCount: number;
|
||||
nodes: Node<B, L>[];
|
||||
highlightedPath: string | null;
|
||||
setHighlightedPath: (id: string | null) => void;
|
||||
};
|
||||
|
||||
export type TreeRowProps<B, L> = ListChildComponentProps<TreeData<B, L>> & {
|
||||
rowProps?: React.HTMLProps<HTMLDivElement> | null;
|
||||
render: (item: Node<B, L>) => React.ReactNode;
|
||||
getClasses?: (item: Node<B, L>) => string[];
|
||||
onLeafClick?: (item: LeafNode<L>) => void;
|
||||
onHover?: (item: Node<B, L>) => void;
|
||||
setBranchCollapsed?: (id: string, collapsed: boolean) => void;
|
||||
};
|
||||
|
||||
export function TreeRow<B, L>({
|
||||
index,
|
||||
style,
|
||||
data: { nodes, highlightedPath, setHighlightedPath },
|
||||
rowProps,
|
||||
render,
|
||||
getClasses,
|
||||
onLeafClick,
|
||||
onHover,
|
||||
setBranchCollapsed,
|
||||
}: TreeRowProps<B, L>) {
|
||||
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(
|
||||
<span
|
||||
key={i}
|
||||
className={clsx(
|
||||
styles.indent,
|
||||
node.path[i] === highlightedPath && styles.indentHighlighted,
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if (node.type === 'branch') {
|
||||
indentItems.push(
|
||||
<span
|
||||
key="toggle"
|
||||
className={clsx(
|
||||
styles.toggle,
|
||||
'codicon',
|
||||
node.collapsed ? 'codicon-chevron-right' : 'codicon-chevron-down',
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes)}
|
||||
style={style}
|
||||
{...rowProps}
|
||||
onClick={() => {
|
||||
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)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type NodeWithChildren<B, L> = BranchNode<B> & {
|
||||
children: (NodeWithChildren<B, L> | LeafNode<L>)[];
|
||||
};
|
||||
|
||||
export type SimpleTreeNodeData = { label: string };
|
||||
export type SimpleTreeData<L> = 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<L>(
|
||||
items: L[],
|
||||
getPath: (item: L) => string,
|
||||
collapsed: Record<string, boolean>,
|
||||
highlightedPath: string | null,
|
||||
setHighlightedPath: (id: string | null) => void,
|
||||
): SimpleTreeData<L> {
|
||||
type SimpleTreeNode = NodeWithChildren<
|
||||
SimpleTreeNodeData,
|
||||
L & SimpleTreeNodeData
|
||||
>;
|
||||
const map = new Map<string, SimpleTreeNode>();
|
||||
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<L & SimpleTreeNodeData> = {
|
||||
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<L>['nodes'] = [];
|
||||
for (const node of rootNodes) {
|
||||
pushNodes(node, nodes);
|
||||
}
|
||||
return {
|
||||
leafCount: items.length,
|
||||
nodes,
|
||||
highlightedPath,
|
||||
setHighlightedPath,
|
||||
};
|
||||
}
|
||||
|
||||
function pushNodes<B, L>(
|
||||
item: NodeWithChildren<B, L> | LeafNode<L>,
|
||||
out: Node<B, L>[],
|
||||
) {
|
||||
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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user