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",
|
"@vscode/codicons": "^0.0.36",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"objdiff-wasm": "3.0.0-beta.8",
|
"objdiff-wasm": "3.0.0-beta.9",
|
||||||
"picomatch": "^4.0.2",
|
"picomatch": "^4.0.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-tooltip": "^5.28.0",
|
"react-tooltip": "^5.28.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.24",
|
"react-virtualized-auto-sizer": "^1.0.26",
|
||||||
"react-window": "^1.8.10",
|
"react-window": "^1.8.11",
|
||||||
"shescape": "^2.1.1",
|
"shescape": "^2.1.3",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.9.3",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@rsbuild/core": "^1.1.8",
|
"@rsbuild/core": "^1.3.20",
|
||||||
"@rsbuild/plugin-react": "^1.0.7",
|
"@rsbuild/plugin-react": "^1.3.1",
|
||||||
"@rsbuild/plugin-type-check": "^1.1.0",
|
"@rsbuild/plugin-type-check": "^1.2.2",
|
||||||
"@rsbuild/plugin-typed-css-modules": "^1.0.2",
|
"@rsbuild/plugin-typed-css-modules": "^1.0.2",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.15.18",
|
||||||
"@types/picomatch": "^3.0.1",
|
"@types/picomatch": "^3.0.2",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.21",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@types/vscode": "^1.96.0",
|
"@types/vscode": "^1.100.0",
|
||||||
"@types/vscode-webview": "^1.57.5",
|
"@types/vscode-webview": "^1.57.5",
|
||||||
"@vscode/test-cli": "^0.0.10",
|
"@vscode/test-cli": "^0.0.10",
|
||||||
"@vscode/test-electron": "^2.4.1",
|
"@vscode/test-electron": "^2.5.2",
|
||||||
"@vscode/vsce": "^3.2.2",
|
"@vscode/vsce": "^3.4.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.8.3"
|
||||||
},
|
},
|
||||||
"main": "./dist/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
Generated
+972
-278
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@ export type StateMessage = {
|
|||||||
leftObject?: ArrayBuffer | null;
|
leftObject?: ArrayBuffer | null;
|
||||||
rightObject?: ArrayBuffer | null;
|
rightObject?: ArrayBuffer | null;
|
||||||
projectConfig?: ProjectConfig | null;
|
projectConfig?: ProjectConfig | null;
|
||||||
|
diffLabel?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// decomp.me colors
|
// decomp.me colors
|
||||||
|
|||||||
+1
-3
@@ -15,9 +15,7 @@
|
|||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
/* type checking */
|
/* type checking */
|
||||||
"strict": true,
|
"strict": true
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true
|
|
||||||
},
|
},
|
||||||
"include": ["src", "webview", "shared"]
|
"include": ["src", "webview", "shared"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
--vscode-list-hoverBackground,
|
--vscode-list-hoverBackground,
|
||||||
light-dark(#f2f2f2, #2a2d2e)
|
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);
|
--line-number-foreground: var(--vscode-editorLineNumber-foreground, #6e7681);
|
||||||
|
|
||||||
|
|||||||
+72
-41
@@ -1,50 +1,77 @@
|
|||||||
import './App.css';
|
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 { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { useDiff } from './diff';
|
||||||
import { useAppStore, useExtensionStore } from './state';
|
import { useAppStore, useExtensionStore } from './state';
|
||||||
import type { SymbolRefByName } from './state';
|
import DiffView from './views/DiffView';
|
||||||
import FunctionView from './views/FunctionView';
|
|
||||||
import SettingsView from './views/SettingsView';
|
import SettingsView from './views/SettingsView';
|
||||||
import SymbolsView from './views/SymbolsView';
|
|
||||||
import UnitsView from './views/UnitsView';
|
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 App = () => {
|
||||||
const { buildRunning, result, config, ready } = useExtensionStore(
|
const {
|
||||||
|
buildRunning,
|
||||||
|
configProperties,
|
||||||
|
currentUnit,
|
||||||
|
leftStatus,
|
||||||
|
rightStatus,
|
||||||
|
leftObject,
|
||||||
|
rightObject,
|
||||||
|
config,
|
||||||
|
ready,
|
||||||
|
} = useExtensionStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
buildRunning: state.buildRunning,
|
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,
|
config: state.projectConfig,
|
||||||
ready: state.ready,
|
ready: state.ready,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const { leftSymbolRef, rightSymbolRef, currentView } = useAppStore(
|
const { leftSymbolRef, rightSymbolRef, currentView, mappings } = useAppStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => {
|
||||||
leftSymbolRef: state.leftSymbol,
|
const unitState = state.getUnitState(currentUnit?.name ?? '');
|
||||||
rightSymbolRef: state.rightSymbol,
|
return {
|
||||||
currentView: state.currentView,
|
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) {
|
if (!ready) {
|
||||||
// Uses panel background color to avoid flashing
|
// Uses panel background color to avoid flashing
|
||||||
@@ -53,15 +80,19 @@ const App = () => {
|
|||||||
|
|
||||||
switch (currentView) {
|
switch (currentView) {
|
||||||
case 'main':
|
case 'main':
|
||||||
if (result) {
|
if (
|
||||||
const leftSymbol = findSymbol(result.left, leftSymbolRef);
|
result.leftStatus ||
|
||||||
const rightSymbol = findSymbol(result.right, rightSymbolRef);
|
result.rightStatus ||
|
||||||
if (leftSymbol || rightSymbol) {
|
result.diff.left ||
|
||||||
return (
|
result.diff.right
|
||||||
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
|
) {
|
||||||
);
|
return (
|
||||||
}
|
<DiffView
|
||||||
return <SymbolsView diff={result} />;
|
result={result}
|
||||||
|
leftSymbolRef={leftSymbolRef}
|
||||||
|
rightSymbolRef={rightSymbolRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildRunning) {
|
if (buildRunning) {
|
||||||
|
|||||||
@@ -23,9 +23,14 @@ export type ContextMenuState<T> = Readonly<{
|
|||||||
data: T;
|
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;
|
className?: string;
|
||||||
render?: (state: ContextMenuState<T>, close: () => void) => React.ReactNode;
|
render?: ContextMenuRender<T>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function createContextMenu<T>(): {
|
export function createContextMenu<T>(): {
|
||||||
@@ -55,9 +60,7 @@ export function createContextMenu<T>(): {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const close = useCallback(() => {
|
const close = useCallback(() => setState(null), []);
|
||||||
setState(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeIfOutside = useCallback(
|
const closeIfOutside = useCallback(
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
@@ -108,9 +111,7 @@ export function createContextMenu<T>(): {
|
|||||||
observer.observe(state.target.parentNode, {
|
observer.observe(state.target.parentNode, {
|
||||||
childList: true,
|
childList: true,
|
||||||
});
|
});
|
||||||
return () => {
|
return () => observer.disconnect();
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [state?.target, close]);
|
}, [state?.target, close]);
|
||||||
|
|
||||||
@@ -186,12 +187,8 @@ export function renderContextItems(
|
|||||||
className={styles.contextMenuItem}
|
className={styles.contextMenuItem}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(item.val.value).then(
|
navigator.clipboard.writeText(item.val.value).then(
|
||||||
() => {
|
() => close(),
|
||||||
close();
|
(e) => console.warn('Failed to copy:', e),
|
||||||
},
|
|
||||||
(e) => {
|
|
||||||
console.warn('Failed to copy:', e);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,3 +42,8 @@
|
|||||||
.spacer {
|
.spacer {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,52 @@
|
|||||||
import styles from './TooltipShared.module.css';
|
import styles from './TooltipShared.module.css';
|
||||||
|
|
||||||
import type { display } from 'objdiff-wasm';
|
import type { display } from 'objdiff-wasm';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Tooltip } from 'react-tooltip';
|
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 = ({
|
const TooltipShared = ({
|
||||||
id,
|
id,
|
||||||
callback,
|
callback,
|
||||||
@@ -74,4 +117,12 @@ const TooltipContent = ({ items }: { items: display.HoverItem[] }) => {
|
|||||||
|
|
||||||
const TooltipContentMemo = React.memo(TooltipContent);
|
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;
|
let resolvedProjectConfig: ProjectConfig | null = null;
|
||||||
|
|
||||||
async function fetchFile(path: string): Promise<Response> {
|
async function fetchFile(path: string): Promise<Response> {
|
||||||
|
if (!path) {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(null, { status: 404, statusText: 'Not Found' }),
|
||||||
|
);
|
||||||
|
}
|
||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
search.set('path', path);
|
search.set('path', path);
|
||||||
const response = await fetch(`/api/get?${search.toString()}`);
|
const response = await fetch(`/api/get?${search.toString()}`);
|
||||||
@@ -59,6 +64,54 @@ if (serializedConfigProperties) {
|
|||||||
configProperties = JSON.parse(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> {
|
async function handleMessage(msg: OutboundMessage): Promise<void> {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'ready': {
|
case 'ready': {
|
||||||
@@ -70,17 +123,14 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
|||||||
buildRunning: false,
|
buildRunning: false,
|
||||||
configProperties,
|
configProperties,
|
||||||
currentUnit: null,
|
currentUnit: null,
|
||||||
|
leftStatus: null,
|
||||||
|
rightStatus: null,
|
||||||
leftObject: null,
|
leftObject: null,
|
||||||
rightObject: null,
|
rightObject: null,
|
||||||
projectConfig: resolvedProjectConfig,
|
projectConfig: resolvedProjectConfig,
|
||||||
};
|
};
|
||||||
if (lastUnit) {
|
if (lastUnit) {
|
||||||
out.leftObject = await fetchFile(lastUnit.target_path ?? '').then((r) =>
|
await fetchUnitFiles(lastUnit, out);
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
out.rightObject = await fetchFile(lastUnit.base_path ?? '').then((r) =>
|
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
sendMessage(out);
|
sendMessage(out);
|
||||||
break;
|
break;
|
||||||
@@ -90,16 +140,13 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
|||||||
const out: StateMessage = {
|
const out: StateMessage = {
|
||||||
type: 'state',
|
type: 'state',
|
||||||
buildRunning: false,
|
buildRunning: false,
|
||||||
|
leftStatus: null,
|
||||||
|
rightStatus: null,
|
||||||
leftObject: null,
|
leftObject: null,
|
||||||
rightObject: null,
|
rightObject: null,
|
||||||
};
|
};
|
||||||
if (lastUnit) {
|
if (lastUnit) {
|
||||||
out.leftObject = await fetchFile(lastUnit.target_path ?? '').then((r) =>
|
await fetchUnitFiles(lastUnit, out);
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
out.rightObject = await fetchFile(lastUnit.base_path ?? '').then((r) =>
|
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
sendMessage(out);
|
sendMessage(out);
|
||||||
break;
|
break;
|
||||||
@@ -115,17 +162,14 @@ async function handleMessage(msg: OutboundMessage): Promise<void> {
|
|||||||
type: 'state',
|
type: 'state',
|
||||||
buildRunning: false,
|
buildRunning: false,
|
||||||
currentUnit: unit,
|
currentUnit: unit,
|
||||||
|
leftStatus: null,
|
||||||
|
rightStatus: null,
|
||||||
leftObject: null,
|
leftObject: null,
|
||||||
rightObject: null,
|
rightObject: null,
|
||||||
};
|
};
|
||||||
if (unit) {
|
if (unit) {
|
||||||
sendMessage({ type: 'state', buildRunning: true });
|
sendMessage({ type: 'state', buildRunning: true });
|
||||||
out.leftObject = await fetchFile(unit.target_path ?? '').then((r) =>
|
await fetchUnitFiles(unit, out);
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
out.rightObject = await fetchFile(unit.base_path ?? '').then((r) =>
|
|
||||||
r.arrayBuffer(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
lastUnit = unit;
|
lastUnit = unit;
|
||||||
localStorage.setItem('lastUnit', JSON.stringify(unit));
|
localStorage.setItem('lastUnit', JSON.stringify(unit));
|
||||||
|
|||||||
+53
-130
@@ -33,20 +33,16 @@ export type SymbolRefByName = {
|
|||||||
sectionName: string | null;
|
sectionName: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UnitScrollOffsets = {
|
export type Side = 'left' | 'right';
|
||||||
left: number;
|
export type UnitScrollOffsets = { [key in Side]: number };
|
||||||
right: number;
|
export type UnitCollapsedSections = { [key in Side]: Record<string, boolean> };
|
||||||
};
|
|
||||||
export type UnitCollapsedSections = {
|
|
||||||
left: Record<string, boolean>;
|
|
||||||
right: Record<string, boolean>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UnitState = {
|
export type UnitState = {
|
||||||
scrollOffsets: UnitScrollOffsets;
|
scrollOffsets: UnitScrollOffsets;
|
||||||
symbolScrollOffsets: Record<string, number>;
|
symbolScrollOffsets: Record<string, number>;
|
||||||
collapsedSections: UnitCollapsedSections;
|
collapsedSections: UnitCollapsedSections;
|
||||||
search: string | null;
|
search: string | null;
|
||||||
|
mappings: Record<string, string>;
|
||||||
};
|
};
|
||||||
const defaultUnitState: UnitState = {
|
const defaultUnitState: UnitState = {
|
||||||
scrollOffsets: { left: 0, right: 0 },
|
scrollOffsets: { left: 0, right: 0 },
|
||||||
@@ -56,6 +52,7 @@ const defaultUnitState: UnitState = {
|
|||||||
right: {},
|
right: {},
|
||||||
},
|
},
|
||||||
search: null,
|
search: null,
|
||||||
|
mappings: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CurrentView = 'main' | 'settings';
|
export type CurrentView = 'main' | 'settings';
|
||||||
@@ -68,7 +65,7 @@ export interface AppState {
|
|||||||
currentView: CurrentView;
|
currentView: CurrentView;
|
||||||
collapsedUnits: Record<string, boolean>;
|
collapsedUnits: Record<string, boolean>;
|
||||||
|
|
||||||
getUnitState(unit: string): UnitState;
|
getUnitState(unit: string | null | undefined): UnitState;
|
||||||
setSelectedSymbol: (
|
setSelectedSymbol: (
|
||||||
leftSymbol: SymbolRefByName | null,
|
leftSymbol: SymbolRefByName | null,
|
||||||
rightSymbol: SymbolRefByName | null,
|
rightSymbol: SymbolRefByName | null,
|
||||||
@@ -91,6 +88,11 @@ export interface AppState {
|
|||||||
) => void;
|
) => void;
|
||||||
setUnitSearch: (unit: string, search: string | null) => void;
|
setUnitSearch: (unit: string, search: string | null) => void;
|
||||||
setUnitsScrollOffset: (offset: number) => void;
|
setUnitsScrollOffset: (offset: number) => void;
|
||||||
|
setUnitMapping: (
|
||||||
|
unit: string,
|
||||||
|
left: string | null | undefined,
|
||||||
|
right: string | null | undefined,
|
||||||
|
) => void;
|
||||||
setHighlight: (highlight: HighlightState) => void;
|
setHighlight: (highlight: HighlightState) => void;
|
||||||
setCurrentView: (view: CurrentView) => void;
|
setCurrentView: (view: CurrentView) => void;
|
||||||
setCollapsedUnit: (unit: string, collapsed: boolean) => void;
|
setCollapsedUnit: (unit: string, collapsed: boolean) => void;
|
||||||
@@ -123,7 +125,9 @@ export const useAppStore = create<AppState>((set) => {
|
|||||||
collapsedUnits: {},
|
collapsedUnits: {},
|
||||||
|
|
||||||
getUnitState(unit) {
|
getUnitState(unit) {
|
||||||
return this.unitStates[unit] ?? defaultUnitState;
|
return unit == null
|
||||||
|
? defaultUnitState
|
||||||
|
: (this.unitStates[unit] ?? defaultUnitState);
|
||||||
},
|
},
|
||||||
setSelectedSymbol: (leftSymbol, rightSymbol) =>
|
setSelectedSymbol: (leftSymbol, rightSymbol) =>
|
||||||
set({ leftSymbol, rightSymbol }),
|
set({ leftSymbol, rightSymbol }),
|
||||||
@@ -160,6 +164,23 @@ export const useAppStore = create<AppState>((set) => {
|
|||||||
search,
|
search,
|
||||||
})),
|
})),
|
||||||
setUnitsScrollOffset: (offset) => set({ unitsScrollOffset: offset }),
|
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 }),
|
setHighlight: (highlight: HighlightState) => set({ highlight }),
|
||||||
setCurrentView: (currentView) => set({ currentView }),
|
setCurrentView: (currentView) => set({ currentView }),
|
||||||
setCollapsedUnit: (unit, collapsed) =>
|
setCollapsedUnit: (unit, collapsed) =>
|
||||||
@@ -178,14 +199,11 @@ export type ExtensionState = {
|
|||||||
currentUnit: Unit | null;
|
currentUnit: Unit | null;
|
||||||
leftStatus: BuildStatus | null;
|
leftStatus: BuildStatus | null;
|
||||||
rightStatus: BuildStatus | null;
|
rightStatus: BuildStatus | null;
|
||||||
leftObject: diff.Object | null;
|
leftObject: ArrayBuffer | null;
|
||||||
rightObject: diff.Object | null;
|
rightObject: ArrayBuffer | null;
|
||||||
result: diff.DiffResult | null;
|
|
||||||
lastBuilt: number | null;
|
|
||||||
projectConfig: ProjectConfig | null;
|
projectConfig: ProjectConfig | null;
|
||||||
|
diffLabel: string | null;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
||||||
setResult: (result: diff.DiffResult | null | undefined) => void;
|
|
||||||
};
|
};
|
||||||
export const useExtensionStore = create(
|
export const useExtensionStore = create(
|
||||||
subscribeWithSelector<ExtensionState>((set) => ({
|
subscribeWithSelector<ExtensionState>((set) => ({
|
||||||
@@ -197,18 +215,9 @@ export const useExtensionStore = create(
|
|||||||
rightStatus: null,
|
rightStatus: null,
|
||||||
leftObject: null,
|
leftObject: null,
|
||||||
rightObject: null,
|
rightObject: null,
|
||||||
result: null,
|
|
||||||
lastBuilt: null,
|
|
||||||
projectConfig: null,
|
projectConfig: null,
|
||||||
|
diffLabel: null,
|
||||||
ready: false,
|
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 !== 'setUnitsScrollOffset' &&
|
||||||
k !== 'setHighlight' &&
|
k !== 'setHighlight' &&
|
||||||
k !== 'setCurrentView' &&
|
k !== 'setCurrentView' &&
|
||||||
k !== 'setCollapsedUnit'
|
k !== 'setCollapsedUnit' &&
|
||||||
|
k !== 'setUnitMapping'
|
||||||
) {
|
) {
|
||||||
serialized[k] = state[k] as any;
|
serialized[k] = state[k] as any;
|
||||||
}
|
}
|
||||||
@@ -330,117 +340,30 @@ export function buildDiffConfig(
|
|||||||
return config;
|
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 handleMessage = (event: MessageEvent) => {
|
||||||
const message = event.data as InboundMessage;
|
const message = event.data as InboundMessage;
|
||||||
if (message.type === 'state') {
|
if (message.type === 'state') {
|
||||||
|
console.log('Received state message', message);
|
||||||
const newState: Partial<ExtensionState> = {
|
const newState: Partial<ExtensionState> = {
|
||||||
...message,
|
...message,
|
||||||
leftObject: undefined,
|
|
||||||
rightObject: undefined,
|
|
||||||
result: undefined,
|
|
||||||
ready: true,
|
ready: true,
|
||||||
};
|
};
|
||||||
let diffConfig: diff.DiffConfig | null = null;
|
// Backwards compatibility for decomp.me
|
||||||
if (message.leftObject !== undefined) {
|
if (message.leftObject && !message.leftStatus) {
|
||||||
if (message.leftObject == null) {
|
newState.leftStatus = {
|
||||||
newState.leftObject = null;
|
success: true,
|
||||||
} else {
|
cmdline: '',
|
||||||
if (diffConfig == null) {
|
stdout: '',
|
||||||
diffConfig = buildDiffConfig(message.configProperties);
|
stderr: '',
|
||||||
}
|
};
|
||||||
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) {
|
if (message.rightObject && !message.rightStatus) {
|
||||||
if (message.rightObject == null) {
|
newState.rightStatus = {
|
||||||
newState.rightObject = null;
|
success: true,
|
||||||
} else {
|
cmdline: '',
|
||||||
if (diffConfig == null) {
|
stdout: '',
|
||||||
diffConfig = buildDiffConfig(message.configProperties);
|
stderr: '',
|
||||||
}
|
};
|
||||||
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) {
|
for (const k in newState) {
|
||||||
const key = k as keyof typeof 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 {
|
.instruction-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.5em;
|
|
||||||
height: var(--list-row-height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instruction-cell {
|
.instruction-cell {
|
||||||
|
|||||||
+83
-197
@@ -4,26 +4,20 @@ import styles from './FunctionView.module.css';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import { type diff, display } from 'objdiff-wasm';
|
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, areEqual } from 'react-window';
|
import { FixedSizeList, areEqual } from 'react-window';
|
||||||
import type { ListChildComponentProps } from 'react-window';
|
import type { ListChildComponentProps } from 'react-window';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { createContextMenu, renderContextItems } from '../common/ContextMenu';
|
import { createContextMenu, renderContextItems } from '../common/ContextMenu';
|
||||||
import TooltipShared from '../common/TooltipShared';
|
import { createTooltip } from '../common/TooltipShared';
|
||||||
import {
|
import { buildDiffConfig, useAppStore, useExtensionStore } from '../state';
|
||||||
buildDiffConfig,
|
|
||||||
runBuild,
|
|
||||||
useAppStore,
|
|
||||||
useExtensionStore,
|
|
||||||
} from '../state';
|
|
||||||
import {
|
import {
|
||||||
type HighlightState,
|
type HighlightState,
|
||||||
highlightColumn,
|
highlightColumn,
|
||||||
highlightMatches,
|
highlightMatches,
|
||||||
updateHighlight,
|
updateHighlight,
|
||||||
} from '../util/highlight';
|
} from '../util/highlight';
|
||||||
import { percentClass, useFontSize } from '../util/util';
|
import { useFontSize } from '../util/util';
|
||||||
|
|
||||||
const ROTATION_CLASSES = [
|
const ROTATION_CLASSES = [
|
||||||
styles.rotation0,
|
styles.rotation0,
|
||||||
@@ -37,10 +31,20 @@ const ROTATION_CLASSES = [
|
|||||||
styles.rotation8,
|
styles.rotation8,
|
||||||
];
|
];
|
||||||
|
|
||||||
const { ContextMenuProvider, useContextMenu } = createContextMenu<{
|
export type InstructionTooltipContent = {
|
||||||
column: number;
|
column: number;
|
||||||
row: number;
|
row: number;
|
||||||
}>();
|
};
|
||||||
|
|
||||||
|
export const {
|
||||||
|
Tooltip: InstructionTooltip,
|
||||||
|
useTooltip: useInstructionTooltip,
|
||||||
|
} = createTooltip<InstructionTooltipContent>();
|
||||||
|
|
||||||
|
export const {
|
||||||
|
ContextMenuProvider: InstructionContextMenuProvider,
|
||||||
|
useContextMenu: useInstructionContextMenu,
|
||||||
|
} = createContextMenu<InstructionTooltipContent>();
|
||||||
|
|
||||||
const AsmCell = ({
|
const AsmCell = ({
|
||||||
obj,
|
obj,
|
||||||
@@ -53,15 +57,28 @@ const AsmCell = ({
|
|||||||
}: {
|
}: {
|
||||||
obj: diff.ObjectDiff | undefined;
|
obj: diff.ObjectDiff | undefined;
|
||||||
config: diff.DiffConfig;
|
config: diff.DiffConfig;
|
||||||
symbol: display.SectionDisplaySymbol | null;
|
symbol: display.SymbolRef | null;
|
||||||
row: number;
|
row: number;
|
||||||
column: number;
|
column: number;
|
||||||
highlight: HighlightState;
|
highlight: HighlightState;
|
||||||
setHighlight: (highlight: HighlightState) => void;
|
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) {
|
if (!obj || !symbol) {
|
||||||
return <div className={styles.instructionCell} />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlight = highlightColumn(highlightState, column);
|
const highlight = highlightColumn(highlightState, column);
|
||||||
@@ -198,33 +215,24 @@ const AsmCell = ({
|
|||||||
return <div className={clsx(classes)} />;
|
return <div className={clsx(classes)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltipContent: InstructionTooltipContent = { column, row };
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(classes)}
|
className={clsx(classes)}
|
||||||
data-tooltip-id="instruction-tooltip"
|
onContextMenu={onContextMenuMemo}
|
||||||
data-tooltip-content={JSON.stringify(tooltipContent)}
|
{...tooltipProps}
|
||||||
onContextMenu={(e) => onContextMenu(e, { column, row })}
|
|
||||||
>
|
>
|
||||||
{out}
|
{out}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type InstructionTooltipContent = {
|
|
||||||
column: number;
|
|
||||||
row: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ItemData = {
|
type ItemData = {
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
symbolName: string;
|
symbolName: string;
|
||||||
result: diff.DiffResult;
|
result: diff.DiffResult;
|
||||||
config: diff.DiffConfig;
|
config: diff.DiffConfig;
|
||||||
matchPercent?: number;
|
matchPercent?: number;
|
||||||
left: display.SectionDisplaySymbol | null;
|
|
||||||
leftSymbol: display.SymbolDisplay | null;
|
leftSymbol: display.SymbolDisplay | null;
|
||||||
right: display.SectionDisplaySymbol | null;
|
|
||||||
rightSymbol: display.SymbolDisplay | null;
|
rightSymbol: display.SymbolDisplay | null;
|
||||||
highlight: HighlightState;
|
highlight: HighlightState;
|
||||||
setHighlight: (highlight: HighlightState) => void;
|
setHighlight: (highlight: HighlightState) => void;
|
||||||
@@ -234,7 +242,7 @@ const AsmRow = memo(
|
|||||||
({
|
({
|
||||||
index,
|
index,
|
||||||
style,
|
style,
|
||||||
data: { result, config, left, right, highlight, setHighlight },
|
data: { result, config, leftSymbol, rightSymbol, highlight, setHighlight },
|
||||||
}: ListChildComponentProps<ItemData>) => {
|
}: ListChildComponentProps<ItemData>) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -254,7 +262,7 @@ const AsmRow = memo(
|
|||||||
<AsmCell
|
<AsmCell
|
||||||
obj={result.left}
|
obj={result.left}
|
||||||
config={config}
|
config={config}
|
||||||
symbol={left}
|
symbol={leftSymbol?.info.id ?? null}
|
||||||
row={index}
|
row={index}
|
||||||
column={0}
|
column={0}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
@@ -263,7 +271,7 @@ const AsmRow = memo(
|
|||||||
<AsmCell
|
<AsmCell
|
||||||
obj={result.right}
|
obj={result.right}
|
||||||
config={config}
|
config={config}
|
||||||
symbol={right}
|
symbol={rightSymbol?.info.id ?? null}
|
||||||
row={index}
|
row={index}
|
||||||
column={1}
|
column={1}
|
||||||
highlight={highlight}
|
highlight={highlight}
|
||||||
@@ -278,20 +286,16 @@ const AsmRow = memo(
|
|||||||
const createItemData = memoizeOne(
|
const createItemData = memoizeOne(
|
||||||
(
|
(
|
||||||
result: diff.DiffResult,
|
result: diff.DiffResult,
|
||||||
left: display.SectionDisplaySymbol | null,
|
leftSymbol: display.SymbolDisplay | null,
|
||||||
right: display.SectionDisplaySymbol | null,
|
rightSymbol: display.SymbolDisplay | null,
|
||||||
highlight: HighlightState,
|
highlight: HighlightState,
|
||||||
setHighlight: (highlight: HighlightState) => void,
|
setHighlight: (highlight: HighlightState) => void,
|
||||||
): ItemData => {
|
): ItemData => {
|
||||||
const leftSymbol = left ? display.displaySymbol(result.left!, left) : null;
|
|
||||||
const rightSymbol = right
|
|
||||||
? display.displaySymbol(result.right!, right)
|
|
||||||
: null;
|
|
||||||
const itemCount = Math.max(
|
const itemCount = Math.max(
|
||||||
leftSymbol?.rowCount || 0,
|
leftSymbol?.rowCount || 0,
|
||||||
rightSymbol?.rowCount || 0,
|
rightSymbol?.rowCount || 0,
|
||||||
);
|
);
|
||||||
const symbolName = leftSymbol?.name || rightSymbol?.name || '';
|
const symbolName = leftSymbol?.info.name || rightSymbol?.info.name || '';
|
||||||
const config = buildDiffConfig(null);
|
const config = buildDiffConfig(null);
|
||||||
const matchPercent = rightSymbol?.matchPercent;
|
const matchPercent = rightSymbol?.matchPercent;
|
||||||
return {
|
return {
|
||||||
@@ -300,9 +304,7 @@ const createItemData = memoizeOne(
|
|||||||
result,
|
result,
|
||||||
config,
|
config,
|
||||||
matchPercent,
|
matchPercent,
|
||||||
left,
|
|
||||||
leftSymbol,
|
leftSymbol,
|
||||||
right,
|
|
||||||
rightSymbol,
|
rightSymbol,
|
||||||
highlight,
|
highlight,
|
||||||
setHighlight,
|
setHighlight,
|
||||||
@@ -322,7 +324,7 @@ const SymbolLabel = ({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const displayName = symbol.demangledName || symbol.name;
|
const displayName = symbol.info.demangledName || symbol.info.name;
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={clsx(headerStyles.label, headerStyles.emphasized)}
|
className={clsx(headerStyles.label, headerStyles.emphasized)}
|
||||||
@@ -333,36 +335,35 @@ const SymbolLabel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FunctionView = ({
|
export const InstructionList = ({
|
||||||
|
height,
|
||||||
|
width,
|
||||||
diff,
|
diff,
|
||||||
left,
|
leftSymbol,
|
||||||
right,
|
rightSymbol,
|
||||||
}: {
|
}: {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
diff: diff.DiffResult;
|
diff: diff.DiffResult;
|
||||||
left: display.SectionDisplaySymbol | null;
|
leftSymbol: display.SymbolDisplay | null;
|
||||||
right: display.SectionDisplaySymbol | null;
|
rightSymbol: display.SymbolDisplay | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { buildRunning, currentUnit, lastBuilt, hasProjectConfig } =
|
const currentUnit = useExtensionStore((state) => state.currentUnit);
|
||||||
useExtensionStore(
|
const { highlight, setSymbolScrollOffset, setHighlight } = useAppStore(
|
||||||
useShallow((state) => ({
|
useShallow((state) => ({
|
||||||
buildRunning: state.buildRunning,
|
highlight: state.highlight,
|
||||||
currentUnit: state.currentUnit,
|
setSymbolScrollOffset: state.setSymbolScrollOffset,
|
||||||
lastBuilt: state.lastBuilt,
|
setHighlight: state.setHighlight,
|
||||||
hasProjectConfig: state.projectConfig != null,
|
})),
|
||||||
})),
|
);
|
||||||
);
|
const itemData = createItemData(
|
||||||
|
diff,
|
||||||
|
leftSymbol,
|
||||||
|
rightSymbol,
|
||||||
|
highlight,
|
||||||
|
setHighlight,
|
||||||
|
);
|
||||||
const currentUnitName = currentUnit?.name || '';
|
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(
|
const initialScrollOffset = useMemo(
|
||||||
() =>
|
() =>
|
||||||
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
|
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
|
||||||
@@ -370,140 +371,25 @@ const FunctionView = ({
|
|||||||
] || 0,
|
] || 0,
|
||||||
[currentUnitName, itemData.symbolName],
|
[currentUnitName, itemData.symbolName],
|
||||||
);
|
);
|
||||||
|
|
||||||
const itemSize = useFontSize() * 1.33;
|
const itemSize = useFontSize() * 1.33;
|
||||||
return (
|
return (
|
||||||
<>
|
<FixedSizeList
|
||||||
<div className={headerStyles.header}>
|
height={height}
|
||||||
<div className={headerStyles.column}>
|
itemCount={itemData.itemCount}
|
||||||
<div className={headerStyles.row}>
|
itemSize={itemSize}
|
||||||
<button title="Back" onClick={() => setSelectedSymbol(null, null)}>
|
width={width}
|
||||||
<span className="codicon codicon-chevron-left" />
|
itemData={itemData}
|
||||||
</button>
|
overscanCount={20}
|
||||||
</div>
|
onScroll={(e) => {
|
||||||
<div className={headerStyles.row}>
|
setSymbolScrollOffset(
|
||||||
<SymbolLabel symbol={itemData.leftSymbol} />
|
currentUnitName,
|
||||||
</div>
|
itemData.symbolName,
|
||||||
</div>
|
e.scrollOffset,
|
||||||
<div className={headerStyles.column}>
|
);
|
||||||
<div className={headerStyles.row}>
|
}}
|
||||||
{hasProjectConfig && (
|
initialScrollOffset={initialScrollOffset}
|
||||||
<button
|
>
|
||||||
title="Build"
|
{AsmRow}
|
||||||
onClick={() => runBuild()}
|
</FixedSizeList>
|
||||||
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,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FunctionView;
|
|
||||||
|
|||||||
@@ -11,41 +11,6 @@
|
|||||||
padding: 0;
|
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 {
|
.flag-local {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
@@ -62,6 +27,10 @@
|
|||||||
color: var(--color-blue);
|
color: var(--color-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flag-hidden {
|
||||||
|
color: var(--color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.symbol-name {
|
.symbol-name {
|
||||||
color: var(--color-bright);
|
color: var(--color-bright);
|
||||||
}
|
}
|
||||||
@@ -76,3 +45,19 @@
|
|||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
white-space: pre;
|
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