Refactor based on objdiff v3.0.0-alpha.1

This commit is contained in:
Luke Street
2025-03-02 22:55:54 -07:00
parent 0791f6a148
commit 4050ec4d95
33 changed files with 3439 additions and 6399 deletions
+2 -1
View File
@@ -27,7 +27,8 @@
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
}, },
"[json]": { "[json]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": false
}, },
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "biomejs.biome" "editor.defaultFormatter": "biomejs.biome"
+2 -2
View File
@@ -10,13 +10,13 @@ Open the project in Visual Studio Code.
- Install dependencies: - Install dependencies:
```bash ```bash
npm install pnpm install
``` ```
- Start dev server using `Ctrl+Shift+B` or by running: - Start dev server using `Ctrl+Shift+B` or by running:
```bash ```bash
npm run watch pnpm watch
``` ```
- Run the extension in debug mode using `F5`. - Run the extension in debug mode using `F5`.
+6
View File
@@ -27,6 +27,12 @@
"recommended": true, "recommended": true,
"a11y": { "a11y": {
"all": false "all": false
},
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off"
} }
} }
} }
-3916
View File
File diff suppressed because it is too large Load Diff
+39 -18
View File
@@ -1,9 +1,9 @@
{ {
"name": "objdiff", "name": "objdiff",
"displayName": "objdiff", "displayName": "objdiff",
"description": "objdiff", "description": "A local diffing tool for decompilation projects",
"publisher": "decomp-dev", "publisher": "decomp-dev",
"version": "0.0.2", "version": "0.1.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/encounter/objdiff-code" "url": "https://github.com/encounter/objdiff-code"
@@ -20,13 +20,15 @@
"@protobuf-ts/runtime": "^2.9.4", "@protobuf-ts/runtime": "^2.9.4",
"@vscode/codicons": "^0.0.36", "@vscode/codicons": "^0.0.36",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"core-js": "^3.39.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"objdiff-wasm": "^3.0.0-alpha.1",
"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-virtualized-auto-sizer": "^1.0.24", "react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10", "react-window": "^1.8.10",
"shescape": "^2.1.1",
"zustand": "^5.0.2" "zustand": "^5.0.2"
}, },
"devDependencies": { "devDependencies": {
@@ -35,7 +37,6 @@
"@rsbuild/plugin-react": "^1.0.7", "@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-type-check": "^1.1.0", "@rsbuild/plugin-type-check": "^1.1.0",
"@rsbuild/plugin-typed-css-modules": "^1.0.2", "@rsbuild/plugin-typed-css-modules": "^1.0.2",
"@types/core-js": "^2.5.8",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/picomatch": "^3.0.1", "@types/picomatch": "^3.0.1",
"@types/react": "^18.3.1", "@types/react": "^18.3.1",
@@ -95,23 +96,31 @@
} }
], ],
"configuration": [ "configuration": [
{
"title": "Extension",
"properties": {
"objdiff.binaryPath": {
"type": "string",
"description": "Path to the objdiff-cli binary",
"scope": "machine"
}
}
},
{ {
"title": "General", "title": "General",
"properties": { "properties": {
"objdiff.relaxRelocDiffs": { "objdiff.functionRelocDiffs": {
"type": "boolean", "type": "string",
"description": "Ignores differences in relocation targets. (Address, name, etc)", "description": "How relocation targets will be diffed in the function view.",
"default": false "default": "name_address",
"enum": [
"none",
"name_address",
"data_value",
"all"
],
"enumItemLabels": [
"None",
"Name or address",
"Data value",
"Name or address, data value"
],
"enumDescriptions": [
null,
null,
null,
null
]
}, },
"objdiff.spaceBetweenArgs": { "objdiff.spaceBetweenArgs": {
"type": "boolean", "type": "boolean",
@@ -122,6 +131,11 @@
"type": "boolean", "type": "boolean",
"description": "Combines data sections with equal names.", "description": "Combines data sections with equal names.",
"default": false "default": false
},
"objdiff.combineTextSections": {
"type": "boolean",
"description": "Combines all text sections into one.",
"default": false
} }
} }
}, },
@@ -324,5 +338,12 @@
"contents": "Loading..." "contents": "Loading..."
} }
] ]
},
"pnpm": {
"onlyBuiltDependencies": [
"@biomejs/biome",
"core-js",
"esbuild"
]
} }
} }
+2388
View File
File diff suppressed because it is too large Load Diff
+18 -6
View File
@@ -1,5 +1,6 @@
import fs from 'node:fs'; import fs from 'node:fs';
import type { ServerResponse } from 'node:http'; import type { ServerResponse } from 'node:http';
import path from 'node:path';
import { type RequestHandler, defineConfig } from '@rsbuild/core'; import { type RequestHandler, defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react'; import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check'; import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
@@ -92,15 +93,27 @@ export default defineConfig({
}, },
}); });
const PROJECT_ROOT = '../prime';
// Mock API middleware for development. // Mock API middleware for development.
const apiMiddleware: RequestHandler = (req, res, next) => { const apiMiddleware: RequestHandler = (req, res, next) => {
if (req.method === 'GET' && req.url === '/api/project') { if (!req.url || !req.headers.host || req.method !== 'GET') {
return sendFile(res, '../prime/objdiff.json', 'application/json'); return next();
} }
if (req.method === 'GET' && req.url === '/api/diff') { const url = new URL(req.url, `http://${req.headers.host}`);
return sendFile(res, '../prime/diff.binpb', 'application/octet-stream'); if (!url) {
return next();
} }
next(); if (url.pathname === '/api/get') {
const file = url.searchParams.get('path');
if (file) {
const filepath = path.join(PROJECT_ROOT, file);
if (filepath.startsWith(PROJECT_ROOT)) {
return sendFile(res, filepath, 'application/octet-stream');
}
}
}
return next();
}; };
// Send a file as a response. // Send a file as a response.
@@ -115,7 +128,6 @@ function sendFile(
throw err; throw err;
} }
let statusCode = 500; let statusCode = 500;
// biome-ignore lint/suspicious/noExplicitAny: Node error
if ((err as any).code === 'ENOENT') { if ((err as any).code === 'ENOENT') {
statusCode = 404; statusCode = 404;
} }
-230
View File
@@ -1,230 +0,0 @@
{
"properties": [
{
"id": "relaxRelocDiffs",
"type": "boolean",
"default": false,
"name": "Relax relocation diffs",
"description": "Ignores differences in relocation targets. (Address, name, etc)"
},
{
"id": "spaceBetweenArgs",
"type": "boolean",
"default": true,
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "n32",
"name": "N32"
},
{
"value": "n64",
"name": "N64"
}
]
},
{
"id": "mips.instrCategory",
"type": "choice",
"default": "auto",
"name": "Instruction category",
"description": "MIPS instruction category to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "cpu",
"name": "CPU"
},
{
"value": "rsp",
"name": "RSP (N64)"
},
{
"value": "r3000gte",
"name": "R3000 GTE (PS1)"
},
{
"value": "r4000allegrex",
"name": "R4000 ALLEGREX (PSP)"
},
{
"value": "r5900",
"name": "R5900 EE (PS2)"
}
]
},
{
"id": "x86.formatter",
"type": "choice",
"default": "intel",
"name": "Format",
"description": "x86 disassembly syntax.",
"items": [
{
"value": "intel",
"name": "Intel"
},
{
"value": "gas",
"name": "AT&T"
},
{
"value": "nasm",
"name": "NASM"
},
{
"value": "masm",
"name": "MASM"
}
]
}
],
"groups": [
{
"id": "general",
"name": "General",
"properties": [
"relaxRelocDiffs",
"spaceBetweenArgs",
"combineDataSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": [
"mips.abi",
"mips.instrCategory"
]
},
{
"id": "x86",
"name": "x86",
"properties": [
"x86.formatter"
]
}
]
}
+1 -1
View File
@@ -257,7 +257,7 @@ export type ConfigSchema = {
groups: ConfigGroup[]; groups: ConfigGroup[];
}; };
import configSchema from './config-schema.json'; import configSchema from 'objdiff-wasm/dist/config-schema.json';
export const CONFIG_SCHEMA = configSchema as ConfigSchema; export const CONFIG_SCHEMA = configSchema as ConfigSchema;
export function getPropertyValue( export function getPropertyValue(
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -5,12 +5,27 @@ import type {
Unit, Unit,
} from './config'; } from './config';
export type WebviewProps = {
extensionVersion: string;
resourceRoot: string;
};
export type BuildStatus = {
success: boolean;
cmdline: string;
stdout: string;
stderr: string;
};
export type StateMessage = { export type StateMessage = {
type: 'state'; type: 'state';
buildRunning?: boolean; buildRunning?: boolean;
configProperties?: ConfigProperties; configProperties?: ConfigProperties;
currentUnit?: Unit | null; currentUnit?: Unit | null;
data?: ArrayBuffer | null; leftStatus?: BuildStatus | null;
rightStatus?: BuildStatus | null;
leftObject?: ArrayBuffer | null;
rightObject?: ArrayBuffer | null;
projectConfig?: ProjectConfig | null; projectConfig?: ProjectConfig | null;
}; };
+23 -7
View File
@@ -1,6 +1,10 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type { Unit } from '../shared/config'; import type { Unit } from '../shared/config';
import type { InboundMessage, OutboundMessage } from '../shared/messages'; import type {
InboundMessage,
OutboundMessage,
WebviewProps,
} from '../shared/messages';
import { Workspace } from './workspace'; import { Workspace } from './workspace';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
@@ -42,7 +46,6 @@ export function activate(context: vscode.ExtensionContext) {
workspace = new Workspace( workspace = new Workspace(
chan, chan,
vscode.workspace.workspaceFolders[0], vscode.workspace.workspaceFolders[0],
storageUri,
deferredCurrentUnit, deferredCurrentUnit,
); );
workspace.onDidChangeProjectConfig( workspace.onDidChangeProjectConfig(
@@ -61,7 +64,8 @@ export function activate(context: vscode.ExtensionContext) {
if (!unit) { if (!unit) {
sendMessage({ sendMessage({
type: 'state', type: 'state',
data: null, leftObject: null,
rightObject: null,
currentUnit: null, currentUnit: null,
}); });
} }
@@ -73,7 +77,8 @@ export function activate(context: vscode.ExtensionContext) {
(data) => { (data) => {
sendMessage({ sendMessage({
type: 'state', type: 'state',
data: data?.buffer || null, leftObject: data?.leftObject?.buffer || null,
rightObject: data?.rightObject?.buffer || null,
currentUnit: workspace?.currentUnit || null, currentUnit: workspace?.currentUnit || null,
}); });
}, },
@@ -244,8 +249,17 @@ export function activate(context: vscode.ExtensionContext) {
// For development, allow static assets (production will be inlined) // For development, allow static assets (production will be inlined)
html = html.replaceAll(/"\/static\/(.*?)"/g, (_, p) => { html = html.replaceAll(/"\/static\/(.*?)"/g, (_, p) => {
const assetUri = vscode.Uri.joinPath(webviewRoot, 'static', p); const assetUri = vscode.Uri.joinPath(webviewRoot, 'static', p);
return `"${view.webview.asWebviewUri(assetUri)}"`; return JSON.stringify(view.webview.asWebviewUri(assetUri).toString());
}); });
// Inject extension information into the webview
const props: WebviewProps = {
extensionVersion: context.extension.packageJSON.version,
resourceRoot: `${view.webview.asWebviewUri(webviewRoot).toString()}/`,
};
html = html.replace(
'<head>',
`<head><script>window.webviewProps=${JSON.stringify(props)}</script>`,
);
view.webview.options = { view.webview.options = {
localResourceRoots: [context.extensionUri], localResourceRoots: [context.extensionUri],
enableScripts: true, enableScripts: true,
@@ -257,7 +271,8 @@ export function activate(context: vscode.ExtensionContext) {
buildRunning: workspace?.buildRunning || false, buildRunning: workspace?.buildRunning || false,
configProperties: workspace?.configProperties || {}, configProperties: workspace?.configProperties || {},
currentUnit: workspace?.currentUnit || null, currentUnit: workspace?.currentUnit || null,
data: workspace?.cachedData?.buffer || null, leftObject: workspace?.cachedData?.leftObject?.buffer || null,
rightObject: workspace?.cachedData?.rightObject?.buffer || null,
projectConfig: workspace?.projectConfig || null, projectConfig: workspace?.projectConfig || null,
} as InboundMessage); } as InboundMessage);
view.webview.onDidReceiveMessage( view.webview.onDidReceiveMessage(
@@ -269,7 +284,8 @@ export function activate(context: vscode.ExtensionContext) {
buildRunning: workspace?.buildRunning || false, buildRunning: workspace?.buildRunning || false,
configProperties: workspace?.configProperties || {}, configProperties: workspace?.configProperties || {},
currentUnit: workspace?.currentUnit || null, currentUnit: workspace?.currentUnit || null,
data: workspace?.cachedData?.buffer || null, leftObject: workspace?.cachedData?.leftObject?.buffer || null,
rightObject: workspace?.cachedData?.rightObject?.buffer || null,
projectConfig: workspace?.projectConfig || null, projectConfig: workspace?.projectConfig || null,
} as InboundMessage); } as InboundMessage);
chan.info('Webview ready'); chan.info('Webview ready');
+129
View File
@@ -0,0 +1,129 @@
import * as vscode from 'vscode';
export type Task = {
type: string;
command: string;
args: string[];
};
export interface TaskExecutor {
run(task: Task): Promise<TaskResult>;
}
export interface TaskResult {
code: number;
stdout?: string;
stderr?: string;
startTime: number;
}
interface RunningTask {
execution: vscode.TaskExecution;
resolve: (result: TaskResult) => void;
reject: (reason?: any) => void;
startTime: number;
}
export class VscodeTaskExecutor implements TaskExecutor, vscode.Disposable {
private runningTasks: RunningTask[] = [];
private disposables: vscode.Disposable[] = [];
private taskId = 0;
constructor(private workspaceFolder: vscode.WorkspaceFolder) {
vscode.tasks.onDidEndTaskProcess(
(e) => {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
const index = this.runningTasks.findIndex(
(task) =>
task.execution.task.definition.id ===
e.execution.task.definition.id,
);
if (index !== -1) {
const task = this.runningTasks[index];
this.runningTasks.splice(index, 1);
task.resolve({
code: e.exitCode ?? -1,
startTime: task.startTime,
});
}
},
null,
this.disposables,
);
}
async run(task: Task): Promise<TaskResult> {
const { type, command, args } = task;
const startTime = performance.now();
const vscodeTask = new vscode.Task(
{
type: 'objdiff',
taskType: type,
id: this.taskId++,
},
this.workspaceFolder,
'objdiff',
'objdiff',
new vscode.ShellExecution(command, args),
);
vscodeTask.presentationOptions.reveal = vscode.TaskRevealKind.Silent;
const execution = await vscode.tasks.executeTask(vscodeTask);
return new Promise((resolve, reject) => {
this.runningTasks.push({
execution,
resolve,
reject,
startTime,
});
});
}
dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
}
}
export class DirectTaskExecutor implements TaskExecutor {
constructor(private workspaceFolder: vscode.WorkspaceFolder) {}
async run(task: Task): Promise<TaskResult> {
const { command, args } = task;
const startTime = performance.now();
return new Promise((resolve, reject) => {
const { execFile } = require('node:child_process');
execFile(
command,
args,
{
cwd: this.workspaceFolder.uri.fsPath,
encoding: 'utf8',
},
(error: any, stdout: string, stderr: string) => {
if (error) {
if (stdout || stderr) {
resolve({
code: error.code ?? -1,
stdout,
stderr,
startTime,
});
} else {
reject(error);
}
} else {
resolve({
code: 0,
stdout,
stderr,
startTime,
});
}
},
);
});
}
}
+101 -159
View File
@@ -1,4 +1,5 @@
import * as picomatch from 'picomatch'; import * as picomatch from 'picomatch';
import { Shescape } from 'shescape';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { import {
CONFIG_FILENAME, CONFIG_FILENAME,
@@ -6,13 +7,23 @@ import {
type ConfigPropertyValue, type ConfigPropertyValue,
type ProjectConfig, type ProjectConfig,
type Unit, type Unit,
getModifiedConfigProperties,
resolveProjectConfig, resolveProjectConfig,
} from '../shared/config'; } from '../shared/config';
import {
DirectTaskExecutor,
type Task,
type TaskResult,
VscodeTaskExecutor,
} from './util';
export type BuildData = {
leftObject: Uint8Array | null;
rightObject: Uint8Array | null;
};
export class Workspace extends vscode.Disposable { export class Workspace extends vscode.Disposable {
public buildRunning = false; public buildRunning = false;
public cachedData: Uint8Array | null = null; public cachedData: BuildData | null = null;
public configProperties: ConfigProperties = {}; public configProperties: ConfigProperties = {};
public currentUnit?: Unit; public currentUnit?: Unit;
public projectConfig?: ProjectConfig; public projectConfig?: ProjectConfig;
@@ -22,19 +33,22 @@ export class Workspace extends vscode.Disposable {
public onDidChangeConfigProperties: vscode.Event<ConfigProperties>; public onDidChangeConfigProperties: vscode.Event<ConfigProperties>;
public onDidChangeCurrentUnit: vscode.Event<Unit | undefined>; public onDidChangeCurrentUnit: vscode.Event<Unit | undefined>;
public onDidChangeBuildRunning: vscode.Event<boolean>; public onDidChangeBuildRunning: vscode.Event<boolean>;
public onDidChangeData: vscode.Event<Uint8Array | null>; public onDidChangeData: vscode.Event<BuildData | null>;
private subscriptions: vscode.Disposable[] = []; private subscriptions: vscode.Disposable[] = [];
private wwSubscriptions: vscode.Disposable[] = []; private wwSubscriptions: vscode.Disposable[] = [];
private pathMatcher?: picomatch.Matcher; private pathMatcher?: picomatch.Matcher;
private buildTaskExecutor: VscodeTaskExecutor;
private directTaskExecutor: DirectTaskExecutor;
private didChangeBuildRunningEmitter = new vscode.EventEmitter<boolean>(); private didChangeBuildRunningEmitter = new vscode.EventEmitter<boolean>();
private didChangeConfigPropertiesEmitter = private didChangeConfigPropertiesEmitter =
new vscode.EventEmitter<ConfigProperties>(); new vscode.EventEmitter<ConfigProperties>();
private didChangeCurrentUnitEmitter = new vscode.EventEmitter< private didChangeCurrentUnitEmitter = new vscode.EventEmitter<
Unit | undefined Unit | undefined
>(); >();
private didChangeDataEmitter = new vscode.EventEmitter<Uint8Array | null>(); private didChangeDataEmitter = new vscode.EventEmitter<BuildData | null>();
private didChangeProjectConfigEmitter = new vscode.EventEmitter< private didChangeProjectConfigEmitter = new vscode.EventEmitter<
ProjectConfig | undefined ProjectConfig | undefined
>(); >();
@@ -42,7 +56,6 @@ export class Workspace extends vscode.Disposable {
constructor( constructor(
public readonly chan: vscode.LogOutputChannel, public readonly chan: vscode.LogOutputChannel,
public readonly workspaceFolder: vscode.WorkspaceFolder, public readonly workspaceFolder: vscode.WorkspaceFolder,
private readonly storageUri: vscode.Uri,
public deferredCurrentUnit?: string, public deferredCurrentUnit?: string,
) { ) {
super(() => { super(() => {
@@ -55,11 +68,9 @@ export class Workspace extends vscode.Disposable {
this.onDidChangeData = this.didChangeDataEmitter.event; this.onDidChangeData = this.didChangeDataEmitter.event;
this.onDidChangeProjectConfig = this.didChangeProjectConfigEmitter.event; this.onDidChangeProjectConfig = this.didChangeProjectConfigEmitter.event;
vscode.tasks.onDidEndTaskProcess( this.buildTaskExecutor = new VscodeTaskExecutor(workspaceFolder);
this.onDidEndTaskProcess, this.directTaskExecutor = new DirectTaskExecutor(workspaceFolder);
this,
this.subscriptions,
);
vscode.workspace.onDidChangeConfiguration( vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration, this.onDidChangeConfiguration,
this, this,
@@ -90,7 +101,6 @@ export class Workspace extends vscode.Disposable {
chan.info(`Initialized workspace: ${this.workspaceFolder.uri.toString()}`); chan.info(`Initialized workspace: ${this.workspaceFolder.uri.toString()}`);
} }
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showError(message: string, ...args: any[]) { showError(message: string, ...args: any[]) {
this.chan.error(message, ...args); this.chan.error(message, ...args);
vscode.window vscode.window
@@ -104,7 +114,6 @@ export class Workspace extends vscode.Disposable {
}); });
} }
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showWarning(message: string, ...args: any[]) { showWarning(message: string, ...args: any[]) {
this.chan.warn(message, ...args); this.chan.warn(message, ...args);
vscode.window.showWarningMessage(`objdiff: ${message}`); vscode.window.showWarningMessage(`objdiff: ${message}`);
@@ -262,7 +271,7 @@ export class Workspace extends vscode.Disposable {
return true; return true;
} }
tryBuild() { async tryBuild() {
if (this.buildRunning) { if (this.buildRunning) {
return; return;
} }
@@ -283,145 +292,101 @@ export class Workspace extends vscode.Disposable {
const basePath = const basePath =
this.currentUnit.base_path && this.currentUnit.base_path &&
vscode.Uri.joinPath(this.workspaceFolder.uri, this.currentUnit.base_path); vscode.Uri.joinPath(this.workspaceFolder.uri, this.currentUnit.base_path);
this.chan.info('Diffing', targetPath?.toString(), basePath?.toString());
if (!targetPath && !basePath) { if (!targetPath && !basePath) {
this.showWarning('No target or base path'); this.showWarning('No target or base path');
return; return;
} }
const buildCmd = this.projectConfig.custom_make || 'make';
const buildArgs = this.projectConfig.custom_args || [];
const hash = cyrb53(this.workspaceFolder.uri.toString());
const outputUri = vscode.Uri.joinPath(
this.storageUri,
`diff_${hash}.binpb`,
);
const args = [];
if (this.currentUnit.target_path && this.projectConfig.build_target) {
if (args.length) {
args.push('&&');
}
args.push(buildCmd, ...buildArgs, this.currentUnit.target_path);
}
if (
this.currentUnit.base_path &&
(this.projectConfig.build_base ||
this.projectConfig.build_base === undefined)
) {
if (args.length) {
args.push('&&');
}
args.push(buildCmd, ...buildArgs, this.currentUnit.base_path);
}
if (args.length) {
args.push('&&');
}
const binaryPath = this.configProperties.binaryPath as string | undefined;
if (!binaryPath) {
vscode.window
.showWarningMessage('objdiff.binaryPath not set', {
title: 'Open settings',
})
.then((item) => {
if (item) {
vscode.commands.executeCommand(
'workbench.action.openSettings',
'objdiff.binaryPath',
);
}
});
return;
}
args.push(binaryPath, 'diff');
if (targetPath) {
args.push('-1', targetPath.fsPath);
}
if (basePath) {
args.push('-2', basePath.fsPath);
}
args.push('--format', 'proto', '-o', outputUri.fsPath);
const configProperties = getModifiedConfigProperties(this.configProperties);
for (const key in configProperties) {
args.push('-c', `${key}=${configProperties[key]}`);
}
const startTime = performance.now();
const task = new vscode.Task(
{
type: 'objdiff',
taskType: 'build',
startTime,
},
this.workspaceFolder,
'objdiff',
'objdiff',
new vscode.ShellExecution(args[0], args.slice(1)),
);
task.presentationOptions.reveal = vscode.TaskRevealKind.Silent;
this.buildRunning = true; this.buildRunning = true;
this.didChangeBuildRunningEmitter.fire(true); this.didChangeBuildRunningEmitter.fire(true);
vscode.tasks.executeTask(task).then(
({ task, terminate: _ }) => {
const curTime = performance.now();
this.chan.info(
'Diff task started in',
curTime - task.definition.startTime,
);
},
(reason) => {
this.showError('Failed to start diff task', reason);
},
);
}
private async onDidEndTaskProcess(e: vscode.TaskProcessEndEvent) {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
try { try {
const endTime = performance.now(); const buildCmd = this.projectConfig.custom_make || 'make';
this.chan.info( const buildArgs = this.projectConfig.custom_args || [];
'Task ended',
e.exitCode, // Build target object
endTime - e.execution.task.definition.startTime, if (this.currentUnit.target_path && this.projectConfig.build_target) {
); const result = await this.runTask({
const proc = e.execution.task.execution as vscode.ProcessExecution; type: 'objdiff',
const outputFile = proc.args[proc.args.indexOf('-o') + 1]; command: buildCmd,
const outputUri = vscode.Uri.file(outputFile); args: [...buildArgs, this.currentUnit.target_path],
if (e.exitCode === 0) { });
const data = await vscode.workspace.fs.readFile(outputUri); if (result.code !== 0) {
this.chan.info( this.showError(`Target build failed with code ${result.code}`);
'Read output file', return;
outputFile,
'with size',
data.byteLength,
);
this.cachedData = data;
this.didChangeDataEmitter.fire(data);
} else {
this.showError(`Build failed with code ${e.exitCode}`);
}
let exists = false;
try {
const stat = await vscode.workspace.fs.stat(outputUri);
exists = stat.type === vscode.FileType.File;
} catch (reason) {
if (
reason instanceof vscode.FileSystemError &&
reason.code !== 'FileNotFound'
) {
throw reason;
} }
} }
if (exists) {
await vscode.workspace.fs.delete(outputUri); // Build base object
if (
this.currentUnit.base_path &&
(this.projectConfig.build_base ||
this.projectConfig.build_base === undefined)
) {
const result = await this.runTask({
type: 'objdiff',
command: buildCmd,
args: [...buildArgs, this.currentUnit.base_path],
});
if (result.code !== 0) {
this.showError(`Base build failed with code ${result.code}`);
return;
}
} }
// Read target object
let targetData: Uint8Array | null = null;
if (targetPath) {
targetData = await vscode.workspace.fs.readFile(targetPath);
}
// Read base object
let baseData: Uint8Array | null = null;
if (basePath) {
baseData = await vscode.workspace.fs.readFile(basePath);
}
this.cachedData = { leftObject: targetData, rightObject: baseData };
this.didChangeDataEmitter.fire(this.cachedData);
} catch (reason) { } catch (reason) {
this.showError('Failed to process build result', reason); this.showError('Failed to execute build', reason);
} finally { } finally {
this.buildRunning = false; this.buildRunning = false;
this.didChangeBuildRunningEmitter.fire(false); this.didChangeBuildRunningEmitter.fire(false);
} }
} }
private async runTask(task: Task): Promise<TaskResult> {
this.logTask(task);
const result = await this.directTaskExecutor.run(task);
this.logTaskResult(task, result);
return result;
}
private logTask(task: Task) {
const command = new Shescape({ flagProtection: false, shell: true })
.quoteAll([task.command, ...task.args])
.join(' ');
this.chan.info('Executing', command);
}
private logTaskResult(task: Task, result: TaskResult) {
if (result.code === 0) {
const endTime = performance.now();
const elapsed = (endTime - result.startTime).toFixed(0);
this.chan.info(`Command ${task.command} succeeded in ${elapsed}ms`);
return;
}
this.chan.error(
[
`Command ${task.command} failed with code ${result.code}`,
result.stdout,
result.stderr,
]
.filter(Boolean)
.join('\n'),
);
}
private async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) { private async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) {
if (!e.affectsConfiguration('objdiff')) { if (!e.affectsConfiguration('objdiff')) {
return; return;
@@ -455,6 +420,7 @@ export class Workspace extends vscode.Disposable {
private disposeImpl() { private disposeImpl() {
this.chan.info('Disposing workspace'); this.chan.info('Disposing workspace');
this.buildTaskExecutor.dispose();
this.projectConfigWatcher.dispose(); this.projectConfigWatcher.dispose();
for (const sub of this.wwSubscriptions) { for (const sub of this.wwSubscriptions) {
sub.dispose(); sub.dispose();
@@ -468,27 +434,3 @@ export class Workspace extends vscode.Disposable {
this.subscriptions = []; this.subscriptions = [];
} }
} }
/**
* cyrb53 (c) 2018 bryc (github.com/bryc)
* License: Public domain (or MIT if needed). Attribution appreciated.
* A fast and simple 53-bit string hash function with decent collision resistance.
* Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
*/
function cyrb53(str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed;
let h2 = 0x41c6ce57 ^ seed;
for (let i = 0; i < str.length; i++) {
const ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return (
(h2 >>> 0).toString(16).padStart(8, '0') +
(h1 >>> 0).toString(16).padStart(8, '0')
);
}
+4 -4
View File
@@ -3,12 +3,13 @@ import { CONFIG_SCHEMA } from './shared/config';
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8')); const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));
const extensionConfig = packageJson.contributes.configuration.find( const extensionConfig = packageJson.contributes.configuration.find(
// biome-ignore lint/suspicious/noExplicitAny: ignore
(config: any) => config.title === 'Extension', (config: any) => config.title === 'Extension',
); );
const categories = [extensionConfig]; const categories: any[] = [];
if (extensionConfig) {
categories.push(extensionConfig);
}
for (const group of CONFIG_SCHEMA.groups) { for (const group of CONFIG_SCHEMA.groups) {
// biome-ignore lint/suspicious/noExplicitAny: ignore
const category: any = { const category: any = {
title: group.name, title: group.name,
properties: {}, properties: {},
@@ -18,7 +19,6 @@ for (const group of CONFIG_SCHEMA.groups) {
if (!property) { if (!property) {
continue; continue;
} }
// biome-ignore lint/suspicious/noExplicitAny: ignore
const config: any = { const config: any = {
type: property.type === 'boolean' ? 'boolean' : 'string', type: property.type === 'boolean' ? 'boolean' : 'string',
description: property.description, description: property.description,
+7
View File
@@ -26,6 +26,7 @@
--color-red: #f85149; --color-red: #f85149;
--color-blue: #add8e6; --color-blue: #add8e6;
--color-muted: var(--vscode-disabledForeground, rgba(204, 204, 204, 0.5)); --color-muted: var(--vscode-disabledForeground, rgba(204, 204, 204, 0.5));
--color-bright: light-dark(black, white);
--panel-background: var(--vscode-panel-background, #181818); --panel-background: var(--vscode-panel-background, #181818);
--panel-separator: var(--vscode-menu-separatorBackground, #454545); --panel-separator: var(--vscode-menu-separatorBackground, #454545);
@@ -71,6 +72,12 @@
--checkbox-border-color: var(--vscode-settings-checkboxBorder, #3c3c3c); --checkbox-border-color: var(--vscode-settings-checkboxBorder, #3c3c3c);
color-scheme: light dark; color-scheme: light dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
} }
body { body {
+24 -28
View File
@@ -1,12 +1,7 @@
import './App.css'; import './App.css';
import type { diff, display } from 'objdiff-wasm';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
import { SectionKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
ObjectDiff,
SymbolDiff,
} from '../shared/gen/diff_pb';
import FunctionView from './FunctionView'; import FunctionView from './FunctionView';
import SettingsView from './SettingsView'; import SettingsView from './SettingsView';
import SymbolsView from './SymbolsView'; import SymbolsView from './SymbolsView';
@@ -15,39 +10,38 @@ import { useAppStore, useExtensionStore } from './state';
import type { SymbolRefByName } from './state'; import type { SymbolRefByName } from './state';
const findSymbol = ( const findSymbol = (
obj: ObjectDiff | undefined, obj: diff.ObjectDiff | undefined,
symbolRef: SymbolRefByName | null, symbolRef: SymbolRefByName | null,
): SymbolDiff | null => { ): display.SectionDisplaySymbol | null => {
if (!obj || !symbolRef) { if (!obj || !symbolRef) {
return null; return null;
} }
for (const section of obj.sections) { const idx = obj.findSymbol(
if (section.name === symbolRef.section_name) { symbolRef.symbolName,
if (section.kind === SectionKind.SECTION_TEXT) { symbolRef.sectionName ?? undefined,
for (const diff of section.symbols) { );
const symbol = diff.symbol as DiffSymbol; if (idx !== undefined) {
if (symbol.name === symbolRef.symbol_name) { return {
return diff; symbol: idx,
} isMappingSymbol: false,
} };
}
}
} }
return null; return null;
}; };
const App = () => { const App = () => {
const { buildRunning, diff, config, ready } = useExtensionStore( const { buildRunning, result, config, ready } = useExtensionStore(
useShallow((state) => ({ useShallow((state) => ({
buildRunning: state.buildRunning, buildRunning: state.buildRunning,
diff: state.diff, result: state.result,
config: state.projectConfig, config: state.projectConfig,
ready: state.ready, ready: state.ready,
})), })),
); );
const { selectedSymbolRef, currentView } = useAppStore( const { leftSymbolRef, rightSymbolRef, currentView } = useAppStore(
useShallow((state) => ({ useShallow((state) => ({
selectedSymbolRef: state.selectedSymbol, leftSymbolRef: state.leftSymbol,
rightSymbolRef: state.rightSymbol,
currentView: state.currentView, currentView: state.currentView,
})), })),
); );
@@ -57,13 +51,15 @@ const App = () => {
return <div className="loading-root" />; return <div className="loading-root" />;
} }
if (diff) { if (result) {
const leftSymbol = findSymbol(diff.left, selectedSymbolRef); const leftSymbol = findSymbol(result.left, leftSymbolRef);
const rightSymbol = findSymbol(diff.right, selectedSymbolRef); const rightSymbol = findSymbol(result.right, rightSymbolRef);
if (leftSymbol || rightSymbol) { if (leftSymbol || rightSymbol) {
return <FunctionView left={leftSymbol} right={rightSymbol} />; return (
<FunctionView diff={result} left={leftSymbol} right={rightSymbol} />
);
} }
return <SymbolsView diff={diff} />; return <SymbolsView diff={result} />;
} }
switch (currentView) { switch (currentView) {
+13 -11
View File
@@ -27,23 +27,25 @@
color: white; color: white;
background-color: #aa8b00; background-color: #aa8b00;
} }
.line-number {
color: var(--line-number-foreground); .diff-any {
}
.diff_any {
background-color: rgba(255, 255, 255, 0.02); background-color: rgba(255, 255, 255, 0.02);
} }
.diff_change {
.segment-dim {
color: var(--line-number-foreground);
}
.segment-bright {
color: var(--color-bright);
}
.segment-replace {
color: var(--color-blue); color: var(--color-blue);
} }
.diff_add { .segment-delete {
color: var(--color-green);
}
.diff_remove {
color: var(--color-red); color: var(--color-red);
} }
.symbol { .segment-insert {
color: light-dark(black, white); color: var(--color-green);
} }
.rotation0 { .rotation0 {
+194 -108
View File
@@ -3,25 +3,25 @@ import headerStyles from './Header.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 { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; 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 { DiffKind } from '../shared/gen/diff_pb'; import TooltipShared from './TooltipShared';
import type {
Symbol as DiffSymbol,
InstructionDiff,
SymbolDiff,
} from '../shared/gen/diff_pb';
import { displayDiff } from './diff';
import { import {
type HighlightState, type HighlightState,
highlightColumn, highlightColumn,
highlightMatches, highlightMatches,
updateHighlight, updateHighlight,
} from './highlight'; } from './highlight';
import { runBuild, useAppStore, useExtensionStore } from './state'; import {
buildDiffConfig,
runBuild,
useAppStore,
useExtensionStore,
} from './state';
import { percentClass, useFontSize } from './util'; import { percentClass, useFontSize } from './util';
const ROTATION_CLASSES = [ const ROTATION_CLASSES = [
@@ -37,100 +37,118 @@ const ROTATION_CLASSES = [
]; ];
const AsmCell = ({ const AsmCell = ({
insDiff, obj,
config,
symbol, symbol,
row,
column, column,
highlight: highlightState, highlight: highlightState,
setHighlight, setHighlight,
}: { }: {
insDiff: InstructionDiff | undefined; obj: diff.ObjectDiff | undefined;
symbol: DiffSymbol | undefined; config: diff.DiffConfig;
symbol: display.SectionDisplaySymbol | null;
row: number;
column: number; column: number;
highlight: HighlightState; highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void; setHighlight: (highlight: HighlightState) => void;
}) => { }) => {
if (!insDiff || !symbol) { if (!obj || !symbol) {
return <div className={styles.instructionCell} />; return <div className={styles.instructionCell} />;
} }
const highlight = highlightColumn(highlightState, column); const highlight = highlightColumn(highlightState, column);
const out: React.ReactNode[] = []; const out: React.ReactNode[] = [];
const insRow = display.displayInstructionRow(obj, symbol, row, config);
let index = 0; let index = 0;
displayDiff(insDiff, symbol.address, (t) => { for (const segment of insRow.segments) {
let className: string | undefined; let className: string | undefined;
if (t.diff_index != null) { switch (segment.color.tag) {
className = ROTATION_CLASSES[t.diff_index % ROTATION_CLASSES.length]; case 'normal':
break;
case 'dim':
className = styles.segmentDim;
break;
case 'bright':
className = styles.segmentBright;
break;
case 'replace':
className = styles.segmentReplace;
break;
case 'delete':
className = styles.segmentDelete;
break;
case 'insert':
className = styles.segmentInsert;
break;
case 'rotating':
className =
ROTATION_CLASSES[segment.color.val % ROTATION_CLASSES.length];
break;
default:
console.warn('Unknown color type', segment.color);
break;
} }
const t = segment.text;
let text = ''; let text = '';
let postText = ''; // unhighlightable text after the token let postText = ''; // unhighlightable text after the token
let padTo = 0;
let isToken = false; let isToken = false;
switch (t.type) { switch (t.tag) {
case 'basic': case 'basic':
text = t.text; text = t.val;
break;
case 'basic_color':
text = t.text;
className = ROTATION_CLASSES[t.index % ROTATION_CLASSES.length];
break; break;
case 'line': case 'line':
text = (t.line_number || 0).toString(10); text = t.val.toString(10);
className = styles.lineNumber;
padTo = 5;
break; break;
case 'address': case 'address':
text = (t.address || 0).toString(16); text = t.val.toString(16);
postText = ':'; postText = ':';
padTo = 5;
isToken = true; isToken = true;
break; break;
case 'opcode': case 'opcode':
text = t.mnemonic; text = t.val.mnemonic;
padTo = 8;
isToken = true; isToken = true;
if (insDiff.diff_kind === DiffKind.DIFF_OP_MISMATCH) {
className = styles.diff_change;
}
break; break;
case 'argument': { case 'signed':
const value = t.value.value; if (t.val < 0) {
switch (value.oneofKind) { text = `-0x${(-t.val).toString(16)}`;
case 'signed': } else {
if (value.signed < 0) { text = `0x${t.val.toString(16)}`;
text = `-0x${(-value.signed).toString(16)}`;
} else {
text = `0x${value.signed.toString(16)}`;
}
break;
case 'unsigned':
text = `0x${value.unsigned.toString(16)}`;
break;
case 'opaque':
text = value.opaque;
break;
} }
isToken = true; isToken = true;
break; break;
} case 'unsigned':
case 'branch_dest': text = `0x${t.val.toString(16)}`;
text = (t.address || 0).toString(16);
isToken = true; isToken = true;
break; break;
case 'symbol': { case 'opaque':
const symbol = t.target.symbol as DiffSymbol; text = t.val;
text = symbol.demangled_name || symbol.name; isToken = true;
if (t.diff_index == null) { break;
className = styles.symbol; case 'branch-dest':
text = t.val.toString(16);
isToken = true;
break;
case 'symbol':
text = t.val.demangledName || t.val.name;
isToken = true;
break;
case 'addend':
if (t.val < 0) {
text = `-0x${(-t.val).toString(16)}`;
} else {
text = `+0x${t.val.toString(16)}`;
} }
isToken = true;
break; break;
}
case 'spacing': case 'spacing':
text = ' '.repeat(t.count); text = ' '.repeat(t.val);
break; break;
case 'eol':
continue;
default: default:
console.warn('Unknown text type', t); console.warn('Unknown text type', t);
return null; break;
} }
out.push( out.push(
<span <span
@@ -151,39 +169,55 @@ const AsmCell = ({
); );
index++; index++;
if (postText) { if (postText) {
out.push(<span key={index}>{postText}</span>); out.push(
<span key={index} className={className}>
{postText}
</span>,
);
index++; index++;
} }
if (padTo > text.length + postText.length) { if (segment.padTo > text.length + postText.length) {
const spacing = ' '.repeat(padTo - text.length - postText.length); const spacing = ' '.repeat(segment.padTo - text.length - postText.length);
out.push(<span key={index}>{spacing}</span>); out.push(<span key={index}>{spacing}</span>);
index++; index++;
} }
}); }
const classes = [styles.instructionCell]; const classes = [styles.instructionCell];
if (insDiff.diff_kind) { if (insRow.diffKind !== 'none') {
classes.push(styles.diff_any); classes.push(styles.diffAny);
} }
switch (insDiff.diff_kind) { if (!out.length) {
case DiffKind.DIFF_DELETE: return <div className={clsx(classes)} />;
classes.push(styles.diff_remove);
break;
case DiffKind.DIFF_INSERT:
classes.push(styles.diff_add);
break;
case DiffKind.DIFF_REPLACE:
classes.push(styles.diff_change);
break;
} }
return <div className={clsx(classes)}>{out}</div>; const tooltipContent: InstructionTooltipContent = { column, row };
return (
<div
className={clsx(classes)}
data-tooltip-id="instruction-tooltip"
data-tooltip-content={JSON.stringify(tooltipContent)}
>
{out}
</div>
);
};
type InstructionTooltipContent = {
column: number;
row: number;
}; };
type ItemData = { type ItemData = {
itemCount: number; itemCount: number;
left: SymbolDiff | null; symbolName: string;
right: SymbolDiff | null; result: diff.DiffResult;
config: diff.DiffConfig;
matchPercent?: number;
left: display.SectionDisplaySymbol | null;
leftSymbol: display.SymbolDisplay | null;
right: display.SectionDisplaySymbol | null;
rightSymbol: display.SymbolDisplay | null;
highlight: HighlightState; highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void; setHighlight: (highlight: HighlightState) => void;
}; };
@@ -192,10 +226,8 @@ const AsmRow = memo(
({ ({
index, index,
style, style,
data: { left, right, highlight, setHighlight }, data: { result, config, left, right, highlight, setHighlight },
}: ListChildComponentProps<ItemData>) => { }: ListChildComponentProps<ItemData>) => {
const leftIns = left?.instructions[index];
const rightIns = right?.instructions[index];
return ( return (
<div <div
className={styles.instructionRow} className={styles.instructionRow}
@@ -212,15 +244,19 @@ const AsmRow = memo(
}} }}
> >
<AsmCell <AsmCell
insDiff={leftIns} obj={result.left}
symbol={left?.symbol} config={config}
symbol={left}
row={index}
column={0} column={0}
highlight={highlight} highlight={highlight}
setHighlight={setHighlight} setHighlight={setHighlight}
/> />
<AsmCell <AsmCell
insDiff={rightIns} obj={result.right}
symbol={right?.symbol} config={config}
symbol={right}
row={index}
column={1} column={1}
highlight={highlight} highlight={highlight}
setHighlight={setHighlight} setHighlight={setHighlight}
@@ -233,23 +269,41 @@ const AsmRow = memo(
const createItemData = memoizeOne( const createItemData = memoizeOne(
( (
left: SymbolDiff | null, result: diff.DiffResult,
right: SymbolDiff | null, left: display.SectionDisplaySymbol | null,
right: display.SectionDisplaySymbol | 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(
left?.instructions.length || 0, leftSymbol?.rowCount || 0,
right?.instructions.length || 0, rightSymbol?.rowCount || 0,
); );
return { itemCount, left, right, highlight, setHighlight }; const symbolName = leftSymbol?.name || rightSymbol?.name || '';
const config = buildDiffConfig(null);
return {
itemCount,
symbolName,
result,
config,
left,
leftSymbol,
right,
rightSymbol,
highlight,
setHighlight,
};
}, },
); );
const SymbolLabel = ({ const SymbolLabel = ({
symbol, symbol,
}: { }: {
symbol: SymbolDiff | null; symbol: display.SymbolDisplay | null;
}) => { }) => {
if (!symbol) { if (!symbol) {
return ( return (
@@ -258,21 +312,26 @@ const SymbolLabel = ({
</span> </span>
); );
} }
const demangledName = symbol.symbol?.demangled_name || symbol.symbol?.name; const displayName = symbol.demangledName || symbol.name;
return ( return (
<span <span
className={clsx(headerStyles.label, headerStyles.emphasized)} className={clsx(headerStyles.label, headerStyles.emphasized)}
title={demangledName} title={displayName}
> >
{demangledName} {displayName}
</span> </span>
); );
}; };
const FunctionView = ({ const FunctionView = ({
diff,
left, left,
right, right,
}: { left: SymbolDiff | null; right: SymbolDiff | null }) => { }: {
diff: diff.DiffResult;
left: display.SectionDisplaySymbol | null;
right: display.SectionDisplaySymbol | null;
}) => {
const { buildRunning, currentUnit, lastBuilt } = useExtensionStore( const { buildRunning, currentUnit, lastBuilt } = useExtensionStore(
useShallow((state) => ({ useShallow((state) => ({
buildRunning: state.buildRunning, buildRunning: state.buildRunning,
@@ -291,29 +350,27 @@ const FunctionView = ({
})), })),
); );
const symbolName = left?.symbol?.name || right?.symbol?.name || ''; const itemData = createItemData(diff, left, right, highlight, setHighlight);
const initialScrollOffset = useMemo( const initialScrollOffset = useMemo(
() => () =>
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[ useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
symbolName itemData.symbolName
] || 0, ] || 0,
[currentUnitName, symbolName], [currentUnitName, itemData.symbolName],
); );
const itemSize = useFontSize() * 1.33; const itemSize = useFontSize() * 1.33;
const itemData = createItemData(left, right, highlight, setHighlight);
const matchPercent = right?.match_percent;
return ( return (
<> <>
<div className={headerStyles.header}> <div className={headerStyles.header}>
<div className={headerStyles.column}> <div className={headerStyles.column}>
<div className={headerStyles.row}> <div className={headerStyles.row}>
<button title="Back" onClick={() => setSelectedSymbol(null)}> <button title="Back" onClick={() => setSelectedSymbol(null, null)}>
<span className="codicon codicon-chevron-left" /> <span className="codicon codicon-chevron-left" />
</button> </button>
</div> </div>
<div className={headerStyles.row}> <div className={headerStyles.row}>
<SymbolLabel symbol={left} /> <SymbolLabel symbol={itemData.leftSymbol} />
</div> </div>
</div> </div>
<div className={headerStyles.column}> <div className={headerStyles.column}>
@@ -332,20 +389,20 @@ const FunctionView = ({
)} )}
</div> </div>
<div className={headerStyles.row}> <div className={headerStyles.row}>
{matchPercent !== undefined && ( {itemData.matchPercent !== undefined && (
<> <>
<span <span
className={clsx( className={clsx(
headerStyles.label, headerStyles.label,
percentClass(matchPercent), percentClass(itemData.matchPercent),
)} )}
> >
{Math.floor(matchPercent).toFixed(0)}% {Math.floor(itemData.matchPercent).toFixed(0)}%
</span> </span>
{' | '} {' | '}
</> </>
)} )}
<SymbolLabel symbol={right} /> <SymbolLabel symbol={itemData.rightSymbol} />
</div> </div>
</div> </div>
</div> </div>
@@ -362,7 +419,7 @@ const FunctionView = ({
onScroll={(e) => { onScroll={(e) => {
setSymbolScrollOffset( setSymbolScrollOffset(
currentUnitName, currentUnitName,
symbolName, itemData.symbolName,
e.scrollOffset, e.scrollOffset,
); );
}} }}
@@ -373,6 +430,35 @@ const FunctionView = ({
)} )}
</AutoSizer> </AutoSizer>
</div> </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,
);
}}
/>
</> </>
); );
}; };
+1 -1
View File
@@ -31,7 +31,7 @@
} }
.emphasized { .emphasized {
color: light-dark(black, white); color: var(--color-bright);
} }
.missing { .missing {

Some files were not shown because too many files have changed in this diff Show More