A gazillion changes

This commit is contained in:
Luke Street
2025-01-04 21:06:02 -07:00
parent 0c6f39c4b4
commit 3d753edd0f
28 changed files with 3331 additions and 847 deletions
+2
View File
@@ -0,0 +1,2 @@
NODE_ENV=development
DEV_SERVER=true
+2
View File
@@ -0,0 +1,2 @@
NODE_ENV=development
DEV_SERVER=false
+3
View File
@@ -10,3 +10,6 @@ dist/
# IDE
.vscode-test/
*.vsix
# Ignore auto generated CSS declarations
*.module.css.d.ts
+544 -3
View File
File diff suppressed because it is too large Load Diff
+211 -14
View File
@@ -1,17 +1,21 @@
{
"name": "objdiff-code",
"displayName": "objdiff-code",
"name": "objdiff",
"displayName": "objdiff",
"description": "objdiff",
"publisher": "decomp-dev",
"version": "0.0.1",
"scripts": {
"build": "rsbuild build",
"watch": "rsbuild build --watch --mode development",
"watch": "rsbuild build -w --env-mode watch",
"dev": "rsbuild dev --env-mode dev",
"check": "biome check --write",
"format": "biome format --write"
},
"dependencies": {
"@protobuf-ts/runtime": "^2.9.4",
"@vscode/codicons": "^0.0.36",
"clsx": "^2.1.1",
"core-js": "^3.39.0",
"memoize-one": "^6.0.0",
"picomatch": "^4.0.2",
"react": "^18.3.1",
@@ -25,6 +29,9 @@
"@rsbuild/core": "^1.1.8",
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-type-check": "^1.1.0",
"@rsbuild/plugin-typed-css-modules": "^1.0.2",
"@types/core-js": "^2.5.8",
"@types/node": "^22.10.2",
"@types/picomatch": "^3.0.1",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
@@ -33,15 +40,22 @@
"@types/vscode-webview": "^1.57.5",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "^2.4.1",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
},
"main": "./dist/extension.js",
"engines": {
"vscode": "^1.96.0"
},
"browserslist": ["chrome 128"],
"categories": ["Other"],
"activationEvents": ["*"],
"browserslist": [
"chrome 128"
],
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"contributes": {
"commands": [
{
@@ -60,6 +74,10 @@
"command": "objdiff.chooseCurrentFile",
"title": "objdiff: Switch To Current File"
},
{
"command": "objdiff.settings",
"title": "objdiff: Settings"
},
{
"command": "objdiff.copySymbolName",
"title": "Copy name",
@@ -71,16 +89,195 @@
"enablement": "webviewId == 'objdiff' && contextType == 'symbol' && symbolDemangledName"
}
],
"configuration": {
"title": "objdiff",
"properties": {
"objdiff.binaryPath": {
"type": "string",
"description": "Path to the objdiff-cli binary",
"ignoreSync": true
"configuration": [
{
"title": "Extension",
"properties": {
"objdiff.binaryPath": {
"type": "string",
"description": "Path to the objdiff-cli binary",
"scope": "machine"
}
}
},
{
"title": "General",
"properties": {
"objdiff.relaxRelocDiffs": {
"type": "boolean",
"description": "Ignores differences in relocation targets. (Address, name, etc)",
"default": false
},
"objdiff.spaceBetweenArgs": {
"type": "boolean",
"description": "Adds a space between arguments in the diff output.",
"default": true
},
"objdiff.combineDataSections": {
"type": "boolean",
"description": "Combines data sections with equal names.",
"default": false
}
}
},
{
"title": "ARM",
"properties": {
"objdiff.arm.archVersion": {
"type": "string",
"description": "ARM architecture version to use for disassembly.",
"default": "auto",
"enum": [
"auto",
"v4t",
"v5te",
"v6k"
],
"enumItemLabels": [
"Auto",
"ARMv4T (GBA)",
"ARMv5TE (DS)",
"ARMv6K (3DS)"
],
"enumDescriptions": [
null,
null,
null,
null
]
},
"objdiff.arm.unifiedSyntax": {
"type": "boolean",
"description": "Disassemble as unified assembly language (UAL).",
"default": false
},
"objdiff.arm.avRegisters": {
"type": "boolean",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8.",
"default": false
},
"objdiff.arm.r9Usage": {
"type": "string",
"default": "generalPurpose",
"enum": [
"generalPurpose",
"sb",
"tr"
],
"enumItemLabels": [
"R9 or V6",
"SB (static base)",
"TR (TLS register)"
],
"enumDescriptions": [
"Use R9 as a general-purpose register.",
"Used for position-independent data (PID).",
"Used for thread-local storage."
]
},
"objdiff.arm.slUsage": {
"type": "boolean",
"description": "Used for explicit stack limits.",
"default": false
},
"objdiff.arm.fpUsage": {
"type": "boolean",
"description": "Used for frame pointers.",
"default": false
},
"objdiff.arm.ipUsage": {
"type": "boolean",
"description": "Used for interworking and long branches.",
"default": false
}
}
},
{
"title": "MIPS",
"properties": {
"objdiff.mips.abi": {
"type": "string",
"description": "MIPS ABI to use for disassembly.",
"default": "auto",
"enum": [
"auto",
"o32",
"n32",
"n64"
],
"enumItemLabels": [
"Auto",
"O32",
"N32",
"N64"
],
"enumDescriptions": [
null,
null,
null,
null
]
},
"objdiff.mips.instrCategory": {
"type": "string",
"description": "MIPS instruction category to use for disassembly.",
"default": "auto",
"enum": [
"auto",
"cpu",
"rsp",
"r3000gte",
"r4000allegrex",
"r5900"
],
"enumItemLabels": [
"Auto",
"CPU",
"RSP (N64)",
"R3000 GTE (PS1)",
"R4000 ALLEGREX (PSP)",
"R5900 EE (PS2)"
],
"enumDescriptions": [
null,
null,
null,
null,
null,
null
]
}
}
},
{
"title": "x86",
"properties": {
"objdiff.x86.formatter": {
"type": "string",
"description": "x86 disassembly syntax.",
"default": "intel",
"enum": [
"intel",
"gas",
"nasm",
"masm"
],
"enumItemLabels": [
"Intel",
"AT&T",
"NASM",
"MASM"
],
"enumDescriptions": [
null,
null,
null,
null
]
}
}
}
},
],
"menus": {
"webview/context": [
{
+63 -8
View File
@@ -1,13 +1,26 @@
import { defineConfig } from '@rsbuild/core';
import fs from 'node:fs';
import type { ServerResponse } from 'node:http';
import { type RequestHandler, defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
import { pluginTypedCSSModules } from '@rsbuild/plugin-typed-css-modules';
const devServer = process.env.DEV_SERVER === 'true';
export default defineConfig({
// Disable HMR and live reload. Neither the extension nor the
// webview can communicate with the rsbuild dev server.
dev: {
hmr: false,
liveReload: false,
hmr: devServer,
liveReload: devServer,
setupMiddlewares: [
(middlewares, _server) => {
if (devServer) {
middlewares.unshift(apiMiddleware);
}
return middlewares;
},
],
},
environments: {
extension: {
@@ -38,8 +51,9 @@ export default defineConfig({
// VS Code webviews don't have easy access to resources,
// (especially if the extension is running on web) so we
// simply inline everything into the HTML.
inlineScripts: true,
inlineStyles: true,
dataUriLimit: devServer ? undefined : 1000000000,
inlineScripts: !devServer,
inlineStyles: !devServer,
legalComments: 'none',
},
// <script defer> doesn't work with inline scripts,
@@ -51,8 +65,9 @@ export default defineConfig({
},
plugins: [
pluginReact({
fastRefresh: false,
fastRefresh: devServer,
}),
pluginTypedCSSModules(),
],
},
},
@@ -60,7 +75,7 @@ export default defineConfig({
// the webview must be self-contained files.
performance: {
chunkSplit: {
strategy: 'all-in-one',
strategy: devServer ? undefined : 'all-in-one',
},
},
// Enable async TypeScript type checking.
@@ -71,8 +86,48 @@ export default defineConfig({
tools: {
rspack: {
output: {
asyncChunks: false,
asyncChunks: devServer,
},
},
},
});
// Mock API middleware for development.
const apiMiddleware: RequestHandler = (req, res, next) => {
if (req.method === 'GET' && req.url === '/api/project') {
return sendFile(res, '../prime/objdiff.json', 'application/json');
}
if (req.method === 'GET' && req.url === '/api/diff') {
return sendFile(res, '../prime/diff.binpb', 'application/octet-stream');
}
next();
};
// Send a file as a response.
function sendFile(
res: ServerResponse,
path: string,
contentType: string,
): void {
const stream = fs.createReadStream(path);
stream.on('error', (err) => {
if (res.headersSent) {
throw err;
}
let statusCode = 500;
// biome-ignore lint/suspicious/noExplicitAny: Node error
if ((err as any).code === 'ENOENT') {
statusCode = 404;
}
res.writeHead(statusCode, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify({ error: err.message }));
});
stream.on('ready', () => {
res.writeHead(200, {
'Content-Type': contentType,
});
});
stream.pipe(res);
}
+230
View File
@@ -0,0 +1,230 @@
{
"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"
]
}
]
}
+71 -4
View File
@@ -1,5 +1,3 @@
// import * as vscode from 'vscode';
export const DEFAULT_WATCH_PATTERNS = [
'*.c',
'*.cp',
@@ -19,10 +17,12 @@ export const DEFAULT_WATCH_PATTERNS = [
'*.json',
];
export const CONFIG_FILENAME = 'objdiff.json';
/**
* Configuration file for objdiff
*/
export interface ObjdiffConfiguration {
export interface ProjectConfig {
/**
* Minimum version of objdiff required to load this configuration file.
*/
@@ -179,7 +179,7 @@ export interface ProgressCategory {
name?: string;
}
export function resolveConfig(config: ObjdiffConfiguration) {
export function resolveProjectConfig(config: ProjectConfig) {
if (config.watch_patterns === undefined) {
config.watch_patterns = DEFAULT_WATCH_PATTERNS;
}
@@ -215,3 +215,70 @@ export function resolveConfig(config: ObjdiffConfiguration) {
}
return config;
}
export type ConfigPropertyBase = {
id: string;
name: string;
description?: string;
default: ConfigPropertyValue;
};
export type ConfigPropertyValue = boolean | string;
export type ConfigProperties = Record<string, ConfigPropertyValue>;
export type ConfigPropertyBoolean = ConfigPropertyBase & {
type: 'boolean';
default: boolean;
};
export type ConfigPropertyChoice = ConfigPropertyBase & {
type: 'choice';
default: string;
items: ConfigPropertyChoiceItem[];
};
export type ConfigProperty = ConfigPropertyBoolean | ConfigPropertyChoice;
export type ConfigPropertyChoiceItem = {
value: string;
name: string;
description?: string;
};
export type ConfigGroup = {
id: string;
name: string;
properties: string[];
};
export type ConfigSchema = {
properties: ConfigProperty[];
groups: ConfigGroup[];
};
import configSchema from './config-schema.json';
export const CONFIG_SCHEMA = configSchema as ConfigSchema;
export function getPropertyValue(
properties: ConfigProperties,
id: string,
): ConfigPropertyValue {
const property = CONFIG_SCHEMA.properties.find((p) => p.id === id);
if (!property) {
throw new Error(`Property not found: ${id}`);
}
return properties[id] ?? property.default;
}
export function getModifiedConfigProperties(
properties: ConfigProperties,
): ConfigProperties {
const modified: ConfigProperties = {};
for (const property of CONFIG_SCHEMA.properties) {
if (property.default !== properties[property.id]) {
modified[property.id] = properties[property.id];
}
}
return modified;
}
+27 -18
View File
@@ -1,24 +1,21 @@
import type { ObjdiffConfiguration, Unit } from './config';
export type DiffMessage = {
type: 'diff';
data: ArrayBuffer | null;
currentUnit: Unit | null;
};
export type TaskMessage = {
type: 'task';
taskType: string;
running: boolean;
};
import type {
ConfigProperties,
ConfigPropertyValue,
ProjectConfig,
Unit,
} from './config';
export type StateMessage = {
type: 'state';
config: ObjdiffConfiguration | null;
buildRunning?: boolean;
configProperties?: ConfigProperties;
currentUnit?: Unit | null;
data?: ArrayBuffer | null;
projectConfig?: ProjectConfig | null;
};
// extension -> webview
export type InboundMessage = DiffMessage | TaskMessage | StateMessage;
export type InboundMessage = StateMessage;
export type ReadyMessage = {
type: 'ready';
@@ -43,10 +40,22 @@ export type QuickPickUnitMessage = {
type: 'quickPickUnit';
};
export type SetConfigPropertyMessage = {
type: 'setConfigProperty';
id: string;
value: ConfigPropertyValue | undefined;
};
export type OpenSettingsMessage = {
type: 'openSettings';
};
// webview -> extension
export type OutboundMessage =
| ReadyMessage
| LineRangesMessage
| OpenSettingsMessage
| QuickPickUnitMessage
| ReadyMessage
| RunTaskMessage
| SetCurrentUnitMessage
| QuickPickUnitMessage;
| SetConfigPropertyMessage
| SetCurrentUnitMessage;
+83 -460
View File
File diff suppressed because it is too large Load Diff
+494
View File
@@ -0,0 +1,494 @@
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import {
CONFIG_FILENAME,
type ConfigProperties,
type ConfigPropertyValue,
type ProjectConfig,
type Unit,
getModifiedConfigProperties,
resolveProjectConfig,
} from '../shared/config';
export class Workspace extends vscode.Disposable {
public buildRunning = false;
public cachedData: Uint8Array | null = null;
public configProperties: ConfigProperties = {};
public currentUnit?: Unit;
public projectConfig?: ProjectConfig;
public projectConfigWatcher: vscode.FileSystemWatcher;
public workspaceWatcher?: vscode.FileSystemWatcher;
public onDidChangeProjectConfig: vscode.Event<ProjectConfig | undefined>;
public onDidChangeConfigProperties: vscode.Event<ConfigProperties>;
public onDidChangeCurrentUnit: vscode.Event<Unit | undefined>;
public onDidChangeBuildRunning: vscode.Event<boolean>;
public onDidChangeData: vscode.Event<Uint8Array | null>;
private subscriptions: vscode.Disposable[] = [];
private wwSubscriptions: vscode.Disposable[] = [];
private pathMatcher?: picomatch.Matcher;
private didChangeBuildRunningEmitter = new vscode.EventEmitter<boolean>();
private didChangeConfigPropertiesEmitter =
new vscode.EventEmitter<ConfigProperties>();
private didChangeCurrentUnitEmitter = new vscode.EventEmitter<
Unit | undefined
>();
private didChangeDataEmitter = new vscode.EventEmitter<Uint8Array | null>();
private didChangeProjectConfigEmitter = new vscode.EventEmitter<
ProjectConfig | undefined
>();
constructor(
public readonly chan: vscode.LogOutputChannel,
public readonly workspaceFolder: vscode.WorkspaceFolder,
private readonly storageUri: vscode.Uri,
public deferredCurrentUnit?: string,
) {
super(() => {
this.disposeImpl();
});
this.onDidChangeBuildRunning = this.didChangeBuildRunningEmitter.event;
this.onDidChangeConfigProperties =
this.didChangeConfigPropertiesEmitter.event;
this.onDidChangeCurrentUnit = this.didChangeCurrentUnitEmitter.event;
this.onDidChangeData = this.didChangeDataEmitter.event;
this.onDidChangeProjectConfig = this.didChangeProjectConfigEmitter.event;
vscode.tasks.onDidEndTaskProcess(
this.onDidEndTaskProcess,
this,
this.subscriptions,
);
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
this.subscriptions,
);
this.loadConfigProperties();
this.projectConfigWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(workspaceFolder, CONFIG_FILENAME),
);
this.projectConfigWatcher.onDidCreate(
this.loadProjectConfig,
this,
this.subscriptions,
);
this.projectConfigWatcher.onDidChange(
this.loadProjectConfig,
this,
this.subscriptions,
);
this.projectConfigWatcher.onDidDelete(
this.loadProjectConfig,
this,
this.subscriptions,
);
this.loadProjectConfig();
chan.info(`Initialized workspace: ${this.workspaceFolder.uri.toString()}`);
}
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showError(message: string, ...args: any[]) {
this.chan.error(message, ...args);
vscode.window
.showErrorMessage(`objdiff: ${message}`, {
title: 'Show log',
})
.then((item) => {
if (item) {
this.chan.show();
}
});
}
// biome-ignore lint/suspicious/noExplicitAny: pass through message args
showWarning(message: string, ...args: any[]) {
this.chan.warn(message, ...args);
vscode.window.showWarningMessage(`objdiff: ${message}`);
}
async loadProjectConfig() {
const configUri = vscode.Uri.joinPath(
this.workspaceFolder.uri,
CONFIG_FILENAME,
);
try {
const stat = await vscode.workspace.fs.stat(configUri);
if (stat.type !== vscode.FileType.File) {
this.showError('Config path is not a file', configUri.toString());
return;
}
} catch (reason) {
if (reason instanceof vscode.FileSystemError) {
if (reason.code === 'FileNotFound') {
this.chan.warn('Config file not found', configUri.toString());
this.projectConfig = undefined;
this.onConfigChange();
return;
}
}
this.showError(
'Failed to stat config file',
configUri.toString(),
reason,
);
return;
}
try {
const data = await vscode.workspace.fs.readFile(configUri);
this.projectConfig = JSON.parse(new TextDecoder().decode(data));
} catch (reason) {
this.showError(
'Failed to load config file',
configUri.toString(),
reason,
);
return;
}
this.onConfigChange();
}
private onConfigChange() {
this.chan.info('Loaded new config');
if (this.projectConfig) {
this.projectConfig = resolveProjectConfig(this.projectConfig);
}
const watchPatterns = this.projectConfig?.watch_patterns || [];
if (watchPatterns.length) {
this.pathMatcher = picomatch(watchPatterns, {
basename: true,
strictSlashes: true,
});
} else {
this.pathMatcher = undefined;
}
this.chan.info('Watch patterns:', watchPatterns);
if (this.workspaceWatcher) {
for (const sub of this.wwSubscriptions) {
sub.dispose();
}
this.wwSubscriptions = [];
this.workspaceWatcher.dispose();
this.workspaceWatcher = undefined;
}
if (this.pathMatcher) {
this.workspaceWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this.workspaceFolder.uri, '*/**'),
);
this.workspaceWatcher.onDidChange(
this.onWorkspaceFileChange,
this,
this.wwSubscriptions,
);
this.workspaceWatcher.onDidCreate(
this.onWorkspaceFileChange,
this,
this.wwSubscriptions,
);
this.workspaceWatcher.onDidDelete(
this.onWorkspaceFileChange,
this,
this.wwSubscriptions,
);
}
this.didChangeProjectConfigEmitter.fire(this.projectConfig);
if (this.projectConfig && this.deferredCurrentUnit) {
this.currentUnit = this.projectConfig.units?.find(
(unit) => unit.name === this.deferredCurrentUnit,
);
this.deferredCurrentUnit = undefined;
}
if (this.projectConfig && this.currentUnit) {
this.tryBuild();
}
}
private onWorkspaceFileChange(uri: vscode.Uri) {
if (!uri.fsPath.startsWith(this.workspaceFolder.uri.fsPath)) {
return;
}
const relPath = uri.fsPath.slice(
this.workspaceFolder.uri.fsPath.length + 1,
);
if (!this.pathMatcher || !this.pathMatcher(relPath)) {
return;
}
this.chan.info('Workspace file changed', uri.toString());
if (this.projectConfig && this.currentUnit) {
this.tryBuild();
}
}
setCurrentUnit(unit: Unit | undefined) {
this.currentUnit = unit;
this.didChangeCurrentUnitEmitter.fire(this.currentUnit);
if (this.projectConfig && this.currentUnit) {
this.tryBuild();
}
}
tryUpdateCurrentUnit() {
if (!this.projectConfig) {
return false;
}
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
this.showWarning('No active editor');
return false;
}
if (activeEditor.document.uri.scheme !== 'file') {
this.showWarning('Active editor not a file');
return false;
}
const fsPath = activeEditor.document.uri.fsPath;
if (!fsPath.startsWith(this.workspaceFolder.uri.fsPath)) {
this.showWarning('Active editor not in workspace', fsPath);
return false;
}
const relPath = fsPath.slice(this.workspaceFolder.uri.fsPath.length + 1);
const unit = this.projectConfig.units?.find(
(unit) => unit.metadata?.source_path === relPath,
);
if (!unit) {
this.showWarning(`No unit found for ${relPath}`);
return false;
}
this.currentUnit = unit;
this.didChangeCurrentUnitEmitter.fire(this.currentUnit);
this.tryBuild();
return true;
}
tryBuild() {
if (this.buildRunning) {
return;
}
if (!this.projectConfig) {
this.showWarning('No configuration loaded');
return;
}
if (!this.currentUnit) {
this.showWarning('No unit selected');
return;
}
const targetPath =
this.currentUnit.target_path &&
vscode.Uri.joinPath(
this.workspaceFolder.uri,
this.currentUnit.target_path,
);
const basePath =
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) {
this.showWarning('No target or base path');
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.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 {
const endTime = performance.now();
this.chan.info(
'Task ended',
e.exitCode,
endTime - e.execution.task.definition.startTime,
);
const proc = e.execution.task.execution as vscode.ProcessExecution;
const outputFile = proc.args[proc.args.indexOf('-o') + 1];
const outputUri = vscode.Uri.file(outputFile);
if (e.exitCode === 0) {
const data = await vscode.workspace.fs.readFile(outputUri);
this.chan.info(
'Read output file',
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);
}
} catch (reason) {
this.showError('Failed to process build result', reason);
} finally {
this.buildRunning = false;
this.didChangeBuildRunningEmitter.fire(false);
}
}
private async onDidChangeConfiguration(e: vscode.ConfigurationChangeEvent) {
if (!e.affectsConfiguration('objdiff')) {
return;
}
this.loadConfigProperties();
this.chan.info('Configuration changed');
}
private loadConfigProperties(): Record<string, ConfigPropertyValue> {
const config = vscode.workspace.getConfiguration('objdiff');
const properties: Record<string, ConfigPropertyValue> = {};
for (const key in config) {
const value = config.get(key);
if (typeof value === 'object') {
for (const subkey in value) {
properties[`${key}.${subkey}`] = (
value as Record<string, ConfigPropertyValue>
)[subkey];
}
} else {
properties[key] = value as ConfigPropertyValue;
}
}
this.configProperties = properties;
this.didChangeConfigPropertiesEmitter.fire(properties);
if (this.projectConfig && this.currentUnit) {
this.tryBuild();
}
return properties;
}
private disposeImpl() {
this.chan.info('Disposing workspace');
this.projectConfigWatcher.dispose();
for (const sub of this.wwSubscriptions) {
sub.dispose();
}
this.wwSubscriptions = [];
this.workspaceWatcher?.dispose();
this.workspaceWatcher = undefined;
for (const sub of this.subscriptions) {
sub.dispose();
}
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')
);
}
+37
View File
@@ -0,0 +1,37 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { CONFIG_SCHEMA } from './shared/config';
const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'));
const extensionConfig = packageJson.contributes.configuration.find(
// biome-ignore lint/suspicious/noExplicitAny: ignore
(config: any) => config.title === 'Extension',
);
const categories = [extensionConfig];
for (const group of CONFIG_SCHEMA.groups) {
// biome-ignore lint/suspicious/noExplicitAny: ignore
const category: any = {
title: group.name,
properties: {},
};
for (const id of group.properties) {
const property = CONFIG_SCHEMA.properties.find((p) => p.id === id);
if (!property) {
continue;
}
// biome-ignore lint/suspicious/noExplicitAny: ignore
const config: any = {
type: property.type === 'boolean' ? 'boolean' : 'string',
description: property.description,
default: property.default,
};
if (property.type === 'choice') {
config.enum = property.items.map((item) => item.value);
config.enumItemLabels = property.items.map((item) => item.name);
config.enumDescriptions = property.items.map((item) => item.description);
}
category.properties[`objdiff.${property.id}`] = config;
}
categories.push(category);
}
packageJson.contributes.configuration = categories;
writeFileSync('./package.json', `${JSON.stringify(packageJson, null, 2)}\n`);
+165 -17
View File
@@ -1,6 +1,75 @@
@import url("@vscode/codicons/dist/codicon.css");
:root {
--font-size: var(--vscode-editor-font-size, 13px);
--list-row-height: calc(var(--font-size) * 1.33);
--code-font-family: var(
--vscode-editor-font-family,
JetBrainsMono Nerd Font,
JetBrains Mono,
Consolas,
"Courier New",
monospace
);
--code-font-weight: var(--vscode-editor-font-weight, normal);
--code-font-size: var(--vscode-editor-font-size, 14px);
--ui-font-family: var(
--vscode-font-family,
system-ui,
"Ubuntu",
"Droid Sans",
sans-serif
);
--ui-font-weight: var(--vscode-font-weight, normal);
--ui-font-size: var(--vscode-font-size, 13px);
--color-green: #00ff00;
--color-red: #f85149;
--color-blue: #add8e6;
--color-muted: var(--vscode-disabledForeground, rgba(204, 204, 204, 0.5));
--panel-background: var(--vscode-panel-background, #181818);
--panel-separator: var(--vscode-menu-separatorBackground, #454545);
--foreground: var(--vscode-foreground, #ccc);
--background: var(--vscode-editor-background, #1f1f1f);
--list-row-height: calc(var(--code-font-size) * 1.33);
--list-row-hover-background: var(--vscode-list-hoverBackground, #2a2d2e);
--line-number-foreground: var(--vscode-editorLineNumber-foreground, #6e7681);
--button-background-color: var(--vscode-button-secondaryBackground, #313131);
--button-foreground-color: var(--vscode-button-secondaryForeground, #ccc);
--button-border-color: var(--vscode-button-border, rgba(255, 255, 255, 0.07));
--button-hover-background-color: var(
--vscode-button-secondaryHoverBackground,
#3c3c3c
);
--button-active-background-color: var(
--vscode-toolbar-activeBackground,
rgba(99, 102, 103, 0.31)
);
--button-disabled-foreground-color: var(
--vscode-disabledForeground,
rgba(204, 204, 204, 0.5)
);
--focus-border-color: var(--vscode-focusBorder, #0078d4);
--input-background-color: var(--vscode-input-background, #313131);
--input-foreground-color: var(--vscode-input-foreground, #ccc);
--input-border-color: var(--vscode-input-border, #3c3c3c);
--input-placeholder-foreground-color: var(
--vscode-input-placeholderForeground,
#989898
);
--checkbox-background-color: var(
--vscode-settings-checkboxBackground,
#313131
);
--checkbox-foreground-color: var(--vscode-settings-checkboxForeground, #ccc);
--checkbox-border-color: var(--vscode-settings-checkboxBorder, #3c3c3c);
color-scheme: light dark;
}
@@ -8,11 +77,12 @@ body {
margin: 0;
padding: 0;
min-height: 100vh;
color: var(--vscode-foreground, #fff);
font-family: var(--vscode-font-family, system-ui);
font-weight: var(--vscode-font-weight, normal);
font-size: var(--vscode-font-size, 13px);
background-color: var(--vscode-editor-background, #1e1e1e);
color: var(--foreground);
font-family: var(--ui-font-family);
font-weight: var(--ui-font-weight);
font-size: var(--ui-font-size);
background-color: var(--background);
overflow: hidden;
&.vscode-light,
&.vscode-high-contrast-light {
@@ -30,6 +100,12 @@ body {
min-height: 100vh;
}
.loading-root {
height: 100vh;
width: 100vw;
background-color: var(--panel-background);
}
.content {
flex: 1;
display: flex;
@@ -52,23 +128,24 @@ body {
}
button {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
display: flex;
background-color: var(--button-background-color);
color: var(--button-foreground-color);
border: 1px solid var(--button-border-color);
border-radius: 3px;
cursor: pointer;
font-family: var(--vscode-font-family, system-ui);
font-weight: var(--vscode-font-weight, normal);
font-size: var(--vscode-font-size, 13px);
font-family: var(--ui-font-family);
font-weight: var(--ui-font-weight);
font-size: var(--ui-font-size);
&:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
background-color: var(--button-hover-background-color);
}
&:focus {
opacity: 1;
outline-color: var(--vscode-focusBorder);
outline-color: var(--focus-border-color);
outline-offset: -1px;
outline-style: solid;
outline-width: 1px;
@@ -76,10 +153,81 @@ button {
&:active {
outline: 0 !important;
background-color: var(--vscode-toolbar-activeBackground);
background-color: var(--button-active-background-color);
}
&:disabled {
color: var(--vscode-disabledForeground);
color: var(--button-disabled-foreground-color);
}
> .codicon {
height: 18px;
}
}
input {
background-color: var(--input-background-color);
color: var(--input-foreground-color);
border: 1px solid var(--input-border-color);
border-radius: 3px;
font-family: var(--ui-font-family);
font-weight: var(--ui-font-weight);
font-size: var(--ui-font-size);
&::placeholder {
color: var(--input-placeholder-foreground-color);
}
&:focus {
border-color: var(--focus-border-color);
outline: none;
}
}
input[type="checkbox"] {
appearance: none;
border: 1px solid var(--checkbox-border-color);
border-radius: 3px;
height: 1.2em;
margin-left: 0;
margin-right: 0.5em;
padding: 0;
width: 1.2em;
background-color: var(--checkbox-background-color);
color: var(--checkbox-foreground-color);
cursor: pointer;
font: normal normal normal 16px / 1 codicon;
&:checked {
background-color: var(--checkbox-background-color);
&::before {
content: "\eab2";
display: block;
text-align: center;
line-height: 1.2em;
}
}
&:focus {
border-color: var(--focus-border-color);
}
}
select {
background-color: var(--input-background-color);
color: var(--input-foreground-color);
border: 1px solid var(--input-border-color);
border-radius: 3px;
margin-left: 0.5em;
font-family: var(--ui-font-family);
font-weight: var(--ui-font-weight);
font-size: var(--ui-font-size);
&:focus {
border-color: var(--focus-border-color);
outline: none;
}
}
+43 -11
View File
@@ -1,5 +1,6 @@
import './App.css';
import { useShallow } from 'zustand/react/shallow';
import { SectionKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
@@ -7,6 +8,7 @@ import type {
SymbolDiff,
} from '../shared/gen/diff_pb';
import FunctionView from './FunctionView';
import SettingsView from './SettingsView';
import SymbolsView from './SymbolsView';
import UnitsView from './UnitsView';
import { useAppStore, useExtensionStore } from './state';
@@ -35,9 +37,25 @@ const findSymbol = (
};
const App = () => {
const { diff } = useExtensionStore();
const selectedSymbolRef = useAppStore((state) => state.selectedSymbol);
const config = useExtensionStore((state) => state.config);
const { buildRunning, diff, config, ready } = useExtensionStore(
useShallow((state) => ({
buildRunning: state.buildRunning,
diff: state.diff,
config: state.projectConfig,
ready: state.ready,
})),
);
const { selectedSymbolRef, currentView } = useAppStore(
useShallow((state) => ({
selectedSymbolRef: state.selectedSymbol,
currentView: state.currentView,
})),
);
if (!ready) {
// Uses panel background color to avoid flashing
return <div className="loading-root" />;
}
if (diff) {
const leftSymbol = findSymbol(diff.left, selectedSymbolRef);
@@ -48,14 +66,28 @@ const App = () => {
return <SymbolsView diff={diff} />;
}
return config ? (
<UnitsView />
) : (
<div className="content">
<h1>objdiff</h1>
<p>No configuration loaded.</p>
</div>
);
switch (currentView) {
case 'main':
if (buildRunning) {
return (
<div className="content">
<p>Building...</p>
</div>
);
}
return config ? (
<UnitsView />
) : (
<div className="content">
<h1>objdiff</h1>
<p>No configuration loaded.</p>
</div>
);
case 'settings':
return <SettingsView />;
default:
return null;
}
};
export default App;
+11 -7
View File
@@ -11,9 +11,9 @@
.instruction-cell {
flex: 1 0 0;
font-family: var(--vscode-editor-font-family, monospace);
font-weight: var(--vscode-editor-font-weight, normal);
font-size: var(--font-size);
font-family: var(--code-font-family);
font-weight: var(--code-font-weight);
font-size: var(--code-font-size);
text-wrap: nowrap;
white-space: pre;
overflow: hidden;
@@ -23,20 +23,24 @@
.highlightable {
cursor: pointer;
}
.highlighted {
color: white;
background-color: #aa8b00;
}
.line-number {
color: var(--vscode-editorLineNumber-foreground);
color: var(--line-number-foreground);
}
.diff_any {
background-color: rgba(255, 255, 255, 0.02);
}
.diff_change {
color: #6d6dff;
color: var(--color-blue);
}
.diff_add {
color: #45bd00;
color: var(--color-green);
}
.diff_remove {
color: #c82829;
color: var(--color-red);
}
.symbol {
color: light-dark(black, white);
+162 -35
View File
@@ -7,6 +7,7 @@ import { memo, useMemo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList, areEqual } from 'react-window';
import type { ListChildComponentProps } from 'react-window';
import { useShallow } from 'zustand/react/shallow';
import { DiffKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
@@ -14,26 +15,51 @@ import type {
SymbolDiff,
} from '../shared/gen/diff_pb';
import { displayDiff } from './diff';
import { useAppStore, useExtensionStore, vscode } from './state';
import {
type HighlightState,
highlightColumn,
highlightMatches,
updateHighlight,
} from './highlight';
import { runBuild, useAppStore, useExtensionStore } from './state';
import { percentClass, useFontSize } from './util';
const ROTATION_CLASSES = [
styles.rotation0,
styles.rotation1,
styles.rotation2,
styles.rotation3,
styles.rotation4,
styles.rotation5,
styles.rotation6,
styles.rotation7,
styles.rotation8,
];
const AsmCell = ({
insDiff,
symbol,
column,
highlight: highlightState,
setHighlight,
}: {
insDiff: InstructionDiff | undefined;
symbol: DiffSymbol | undefined;
column: number;
highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void;
}) => {
if (!insDiff || !symbol) {
return <div className={styles.instructionCell} />;
}
const highlight = highlightColumn(highlightState, column);
const out: React.ReactNode[] = [];
let index = 0;
displayDiff(insDiff, symbol.address, (t) => {
let className: string | undefined;
if (t.diff_index != null) {
className = styles[`rotation${t.diff_index % 9}`];
className = ROTATION_CLASSES[t.diff_index % ROTATION_CLASSES.length];
}
let text = '';
let postText = ''; // unhighlightable text after the token
@@ -45,7 +71,7 @@ const AsmCell = ({
break;
case 'basic_color':
text = t.text;
className = styles[`rotation${t.index % 9}`];
className = ROTATION_CLASSES[t.index % ROTATION_CLASSES.length];
break;
case 'line':
text = (t.line_number || 0).toString(10);
@@ -111,11 +137,11 @@ const AsmCell = ({
key={index}
className={clsx(className, {
[styles.highlightable]: isToken,
// [styles.highlighted]: highlighter?.value === text,
[styles.highlighted]: highlightMatches(highlight, t),
})}
onClick={(e) => {
if (isToken) {
// highlighter?.select(text);
setHighlight(updateHighlight(highlightState, t, column));
e.stopPropagation();
}
}}
@@ -158,20 +184,47 @@ type ItemData = {
itemCount: number;
left: SymbolDiff | null;
right: SymbolDiff | null;
highlight: HighlightState;
setHighlight: (highlight: HighlightState) => void;
};
const AsmRow = memo(
({
index,
style,
data: { left, right },
data: { left, right, highlight, setHighlight },
}: ListChildComponentProps<ItemData>) => {
const leftIns = left?.instructions[index];
const rightIns = right?.instructions[index];
return (
<div className={styles.instructionRow} style={style}>
<AsmCell insDiff={leftIns} symbol={left?.symbol} />
<AsmCell insDiff={rightIns} symbol={right?.symbol} />
<div
className={styles.instructionRow}
style={style}
onClick={() => {
// Clear highlight on background click
setHighlight({ left: null, right: null });
}}
onMouseDown={(e) => {
// Prevent double click text selection
if (e.detail > 1) {
e.preventDefault();
}
}}
>
<AsmCell
insDiff={leftIns}
symbol={left?.symbol}
column={0}
highlight={highlight}
setHighlight={setHighlight}
/>
<AsmCell
insDiff={rightIns}
symbol={right?.symbol}
column={1}
highlight={highlight}
setHighlight={setHighlight}
/>
</div>
);
},
@@ -179,52 +232,122 @@ const AsmRow = memo(
);
const createItemData = memoizeOne(
(left: SymbolDiff | null, right: SymbolDiff | null): ItemData => {
(
left: SymbolDiff | null,
right: SymbolDiff | null,
highlight: HighlightState,
setHighlight: (highlight: HighlightState) => void,
): ItemData => {
const itemCount = Math.max(
left?.instructions.length || 0,
right?.instructions.length || 0,
);
return { itemCount, left, right };
return { itemCount, left, right, highlight, setHighlight };
},
);
const SymbolLabel = ({
symbol,
}: {
symbol: SymbolDiff | null;
}) => {
if (!symbol) {
return (
<span className={clsx(headerStyles.label, headerStyles.missing)}>
Missing
</span>
);
}
const demangledName = symbol.symbol?.demangled_name || symbol.symbol?.name;
return (
<span
className={clsx(headerStyles.label, headerStyles.emphasized)}
title={demangledName}
>
{demangledName}
</span>
);
};
const FunctionView = ({
left,
right,
}: { left: SymbolDiff | null; right: SymbolDiff | null }) => {
const buildRunning = useExtensionStore((state) => state.buildRunning);
const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol);
const setSymbolScrollOffset = useAppStore(
(state) => state.setSymbolScrollOffset,
const { buildRunning, currentUnit, lastBuilt } = useExtensionStore(
useShallow((state) => ({
buildRunning: state.buildRunning,
currentUnit: state.currentUnit,
lastBuilt: state.lastBuilt,
})),
);
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 symbolName = left?.symbol?.name || right?.symbol?.name || '';
const initialScrollOffset = useMemo(
() => useAppStore.getState().symbolScrollOffsets[symbolName] || 0,
[symbolName],
() =>
useAppStore.getState().getUnitState(currentUnitName).symbolScrollOffsets[
symbolName
] || 0,
[currentUnitName, symbolName],
);
const itemSize = useFontSize() * 1.33;
const itemData = createItemData(left, right);
const demangledName =
left?.symbol?.demangled_name || right?.symbol?.demangled_name || symbolName;
const matchPercent = right?.match_percent || 0;
const itemData = createItemData(left, right, highlight, setHighlight);
const matchPercent = right?.match_percent;
return (
<>
<div className={headerStyles.header}>
<button onClick={() => setSelectedSymbol(null)}>Back</button>
<button
onClick={() =>
vscode.postMessage({ type: 'runTask', taskType: 'build' })
}
disabled={buildRunning}
>
Build
</button>
<span className={percentClass(matchPercent)}>
{Math.floor(matchPercent).toFixed(0)}%
</span>
<span title={demangledName}>{demangledName}</span>
<div className={headerStyles.column}>
<div className={headerStyles.row}>
<button title="Back" onClick={() => setSelectedSymbol(null)}>
<span className="codicon codicon-chevron-left" />
</button>
</div>
<div className={headerStyles.row}>
<SymbolLabel symbol={left} />
</div>
</div>
<div className={headerStyles.column}>
<div className={headerStyles.row}>
<button
title="Build"
onClick={() => runBuild()}
disabled={buildRunning}
>
<span className="codicon codicon-refresh" />
</button>
{lastBuilt && (
<span className={headerStyles.label}>
Last built: {new Date(lastBuilt).toLocaleTimeString('en-US')}
</span>
)}
</div>
<div className={headerStyles.row}>
{matchPercent !== undefined && (
<>
<span
className={clsx(
headerStyles.label,
percentClass(matchPercent),
)}
>
{Math.floor(matchPercent).toFixed(0)}%
</span>
{' | '}
</>
)}
<SymbolLabel symbol={right} />
</div>
</div>
</div>
<div className={styles.instructionList}>
<AutoSizer>
@@ -237,7 +360,11 @@ const FunctionView = ({
itemData={itemData}
overscanCount={20}
onScroll={(e) => {
setSymbolScrollOffset(symbolName, e.scrollOffset);
setSymbolScrollOffset(
currentUnitName,
symbolName,
e.scrollOffset,
);
}}
initialScrollOffset={initialScrollOffset}
>
+35 -16
View File
@@ -1,20 +1,39 @@
.header {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
padding: 0.5em;
background-color: var(--vscode-panel-background);
border-bottom: 1px solid var(--vscode-menu-separatorBackground);
> span {
color: light-dark(black, white);
font-family: var(--vscode-editor-font-family, monospace);
font-weight: var(--vscode-editor-font-weight, normal);
font-size: var(--font-size);
text-wrap: nowrap;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
gap: 0.5em;
background-color: var(--panel-background);
border-bottom: 1px solid var(--panel-separator);
}
.column {
display: flex;
flex: 1 1 0;
gap: 0.5em;
flex-direction: column;
overflow: hidden;
}
.row {
display: flex;
gap: 0.5em;
align-items: center;
}
.label {
font-family: var(--code-font-family);
font-weight: var(--code-font-weight);
font-size: var(--code-font-size);
text-wrap: nowrap;
white-space: pre;
overflow: hidden;
text-overflow: ellipsis;
}
.emphasized {
color: light-dark(black, white);
}
.missing {
color: var(--color-blue);
}
+18
View File
@@ -0,0 +1,18 @@
.container {
padding: 0 1em;
}
.header {
margin: 0.5em 0;
}
.category-header {
margin: 0.5em;
}
.property {
margin: 0.5em 1em;
display: flex;
flex-flow: row;
align-items: center;
}
+121
View File
@@ -0,0 +1,121 @@
import headerStyles from './Header.module.css';
import styles from './SettingsView.module.css';
import {
CONFIG_SCHEMA,
type ConfigPropertyBoolean,
type ConfigPropertyChoice,
} from '../shared/config';
import {
openSettings,
setConfigProperty,
useAppStore,
useExtensionStore,
} from './state';
const BooleanProperty = ({
property,
value,
}: { property: ConfigPropertyBoolean; value: boolean }) => (
<div className={styles.property} title={property.description}>
<input
type="checkbox"
id={property.id}
checked={value}
onChange={(e) => {
const value = e.target.checked;
if (value === property.default) {
setConfigProperty(property.id, undefined);
} else {
setConfigProperty(property.id, value);
}
}}
/>
<label htmlFor={property.id}>{property.name}</label>
</div>
);
const ChoiceProperty = ({
property,
value,
}: { property: ConfigPropertyChoice; value: string }) => (
<div className={styles.property} title={property.description}>
<label htmlFor={property.id}>{property.name}</label>
<select
id={property.id}
defaultValue={value}
onChange={(e) => {
const value = e.target.value;
if (value === property.default) {
setConfigProperty(property.id, undefined);
} else {
setConfigProperty(property.id, value);
}
}}
>
{property.items.map((item) => (
<option key={item.value} value={item.value}>
{item.name}
</option>
))}
</select>
</div>
);
const SettingsView = () => {
const setCurrentView = useAppStore((state) => state.setCurrentView);
const configProperties = useExtensionStore((state) => state.configProperties);
const items = [];
for (const group of CONFIG_SCHEMA.groups) {
items.push(
<h2 className={styles.categoryHeader} key={group.id}>
{group.name}
</h2>,
);
for (const id of group.properties) {
const property = CONFIG_SCHEMA.properties.find((p) => p.id === id);
if (!property) {
continue;
}
const value = configProperties[property.id] ?? property.default;
switch (property.type) {
case 'boolean':
items.push(
<BooleanProperty
key={property.id}
property={property}
value={value as boolean}
/>,
);
break;
case 'choice':
items.push(
<ChoiceProperty
key={property.id}
property={property}
value={value as string}
/>,
);
break;
}
}
}
return (
<>
<div className={headerStyles.header}>
<button title="Back" onClick={() => setCurrentView('main')}>
<span className="codicon codicon-chevron-left" />
</button>
<button onClick={() => openSettings()}>Open in Editor</button>
</div>
<div className={styles.container}>
<h1 className={styles.header}>Settings</h1>
{items}
</div>
</>
);
};
export default SettingsView;
+34 -6
View File
@@ -16,33 +16,61 @@
user-select: none;
height: var(--list-row-height);
font-family: var(--vscode-editor-font-family, monospace);
font-weight: var(--vscode-editor-font-weight, normal);
font-size: var(--font-size);
font-family: var(--code-font-family);
font-weight: var(--code-font-weight);
font-size: var(--code-font-size);
text-wrap: nowrap;
white-space: pre;
&:hover {
background-color: var(--vscode-list-hoverBackground);
background-color: var(--list-row-hover-background);
}
}
.section {
padding-left: 0.5em;
&::before {
content: "▼ ";
}
&.collapsed {
opacity: 0.75;
&::before {
content: "▶ ";
}
}
}
.symbol {
padding-left: 2em;
}
.flag-local {
color: inherit;
}
.flag-global {
color: lightgreen;
color: var(--color-green);
}
.flag-weak {
/* todo */
color: inherit;
}
.flag-common {
color: var(--color-blue);
}
.symbol-name {
color: light-dark(black, white);
}
.no-object {
color: var(--color-blue);
font-family: var(--code-font-family);
font-weight: var(--code-font-weight);
font-size: var(--code-font-size);
text-wrap: nowrap;
white-space: pre;
}

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