Add symbol mapping & diff UI improvements

This commit is contained in:
Luke Street
2025-05-27 22:15:56 -06:00
parent 047c28560f
commit 3a86f14bfa
22 changed files with 2872 additions and 1330 deletions
+19 -19
View File
@@ -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": [
+972 -278
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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
View File
@@ -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"]
} }
+8
View File
@@ -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
View File
@@ -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) {
+11 -14
View File
@@ -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);
},
); );
}} }}
> >
+5
View File
@@ -42,3 +42,8 @@
.spacer { .spacer {
flex: 1 1 0; flex: 1 1 0;
} }
.input {
display: flex;
align-items: center;
}
+53 -2
View File
@@ -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;
}
+82
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+23
View File
@@ -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
-2
View File
@@ -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
View File
@@ -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;
+20 -35
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff
+42
View File
@@ -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);
}
+215
View File
@@ -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