You've already forked objdiff-web
mirror of
https://github.com/encounter/objdiff-web.git
synced 2026-03-30 11:32:18 -07:00
A gazillion changes
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
NODE_ENV=development
|
||||
DEV_SERVER=false
|
||||
@@ -10,3 +10,6 @@ dist/
|
||||
# IDE
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
# Ignore auto generated CSS declarations
|
||||
*.module.css.d.ts
|
||||
|
||||
Generated
+544
-3
File diff suppressed because it is too large
Load Diff
+211
-14
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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')
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user