Auto-build, full symbols view and diff view & more

This commit is contained in:
Luke Street
2024-12-17 21:49:23 -07:00
parent 6d4711ecc8
commit 4d3800450e
16 changed files with 885 additions and 2282 deletions
-2007
View File
File diff suppressed because it is too large Load Diff
+36 -8
View File
@@ -39,14 +39,30 @@
"engines": {
"vscode": "^1.96.0"
},
"browserslist": ["chrome 128"],
"categories": ["Other"],
"activationEvents": ["*"],
"browserslist": [
"chrome 128"
],
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"contributes": {
"commands": [
{
"command": "objdiff.build",
"title": "objdiff: build"
},
{
"command": "objdiff.copySymbolName",
"title": "Copy name",
"enablement": "webviewId == 'objdiff' && contextType == 'symbol'"
},
{
"command": "objdiff.copySymbolDemangledName",
"title": "Copy demangled name",
"enablement": "webviewId == 'objdiff' && contextType == 'symbol' && symbolDemangledName"
}
],
"configuration": {
@@ -59,6 +75,23 @@
}
}
},
"menus": {
"webview/context": [
{
"command": "objdiff.copySymbolName",
"when": "webviewId == 'objdiff' && contextType == 'symbol'"
},
{
"command": "objdiff.copySymbolDemangledName",
"when": "webviewId == 'objdiff' && contextType == 'symbol'"
}
]
},
"taskDefinitions": [
{
"type": "objdiff"
}
],
"viewsContainers": {
"panel": [
{
@@ -82,11 +115,6 @@
"view": "objdiff",
"contents": "Loading..."
}
],
"taskDefinitions": [
{
"type": "objdiff"
}
]
}
}
+1
View File
@@ -47,6 +47,7 @@ export default defineConfig({
html: {
inject: 'body',
scriptLoading: 'blocking',
title: 'objdiff',
},
plugins: [
pluginReact({
+108 -28
View File
@@ -296,9 +296,22 @@ export interface InstructionBranchTo {
branch_index: number;
}
/**
* @generated from protobuf message objdiff.diff.FunctionDiff
* @generated from protobuf message objdiff.diff.SymbolRef
*/
export interface FunctionDiff {
export interface SymbolRef {
/**
* @generated from protobuf field: optional uint32 section_index = 1;
*/
section_index?: number;
/**
* @generated from protobuf field: uint32 symbol_index = 2;
*/
symbol_index: number;
}
/**
* @generated from protobuf message objdiff.diff.SymbolDiff
*/
export interface SymbolDiff {
/**
* @generated from protobuf field: objdiff.diff.Symbol symbol = 1;
*/
@@ -311,6 +324,12 @@ export interface FunctionDiff {
* @generated from protobuf field: optional float match_percent = 3;
*/
match_percent?: number;
/**
* The symbol ref in the _other_ object that this symbol was diffed against
*
* @generated from protobuf field: optional objdiff.diff.SymbolRef target = 5;
*/
target?: SymbolRef;
}
/**
* @generated from protobuf message objdiff.diff.DataDiff
@@ -352,9 +371,9 @@ export interface SectionDiff {
*/
address: bigint;
/**
* @generated from protobuf field: repeated objdiff.diff.FunctionDiff functions = 5;
* @generated from protobuf field: repeated objdiff.diff.SymbolDiff symbols = 5;
*/
functions: FunctionDiff[];
symbols: SymbolDiff[];
/**
* @generated from protobuf field: repeated objdiff.diff.DataDiff data = 6;
*/
@@ -405,17 +424,17 @@ export enum SymbolFlag {
*/
SYMBOL_LOCAL = 2,
/**
* @generated from protobuf enum value: SYMBOL_WEAK = 3;
* @generated from protobuf enum value: SYMBOL_WEAK = 4;
*/
SYMBOL_WEAK = 3,
SYMBOL_WEAK = 4,
/**
* @generated from protobuf enum value: SYMBOL_COMMON = 4;
* @generated from protobuf enum value: SYMBOL_COMMON = 8;
*/
SYMBOL_COMMON = 4,
SYMBOL_COMMON = 8,
/**
* @generated from protobuf enum value: SYMBOL_HIDDEN = 5;
* @generated from protobuf enum value: SYMBOL_HIDDEN = 16;
*/
SYMBOL_HIDDEN = 5
SYMBOL_HIDDEN = 16
}
/**
* @generated from protobuf enum objdiff.diff.DiffKind
@@ -1196,22 +1215,77 @@ class InstructionBranchTo$Type extends MessageType<InstructionBranchTo> {
*/
export const InstructionBranchTo = new InstructionBranchTo$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FunctionDiff$Type extends MessageType<FunctionDiff> {
class SymbolRef$Type extends MessageType<SymbolRef> {
constructor() {
super("objdiff.diff.FunctionDiff", [
{ no: 1, name: "symbol", kind: "message", T: () => Symbol },
{ no: 2, name: "instructions", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => InstructionDiff },
{ no: 3, name: "match_percent", kind: "scalar", localName: "match_percent", opt: true, T: 2 /*ScalarType.FLOAT*/ }
super("objdiff.diff.SymbolRef", [
{ no: 1, name: "section_index", kind: "scalar", localName: "section_index", opt: true, T: 13 /*ScalarType.UINT32*/ },
{ no: 2, name: "symbol_index", kind: "scalar", localName: "symbol_index", T: 13 /*ScalarType.UINT32*/ }
]);
}
create(value?: PartialMessage<FunctionDiff>): FunctionDiff {
create(value?: PartialMessage<SymbolRef>): SymbolRef {
const message = globalThis.Object.create((this.messagePrototype!));
message.symbol_index = 0;
if (value !== undefined)
reflectionMergePartial<SymbolRef>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SymbolRef): SymbolRef {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* optional uint32 section_index */ 1:
message.section_index = reader.uint32();
break;
case /* uint32 symbol_index */ 2:
message.symbol_index = reader.uint32();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: SymbolRef, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* optional uint32 section_index = 1; */
if (message.section_index !== undefined)
writer.tag(1, WireType.Varint).uint32(message.section_index);
/* uint32 symbol_index = 2; */
if (message.symbol_index !== 0)
writer.tag(2, WireType.Varint).uint32(message.symbol_index);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message objdiff.diff.SymbolRef
*/
export const SymbolRef = new SymbolRef$Type();
// @generated message type with reflection information, may provide speed optimized methods
class SymbolDiff$Type extends MessageType<SymbolDiff> {
constructor() {
super("objdiff.diff.SymbolDiff", [
{ no: 1, name: "symbol", kind: "message", T: () => Symbol },
{ no: 2, name: "instructions", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => InstructionDiff },
{ no: 3, name: "match_percent", kind: "scalar", localName: "match_percent", opt: true, T: 2 /*ScalarType.FLOAT*/ },
{ no: 5, name: "target", kind: "message", T: () => SymbolRef }
]);
}
create(value?: PartialMessage<SymbolDiff>): SymbolDiff {
const message = globalThis.Object.create((this.messagePrototype!));
message.instructions = [];
if (value !== undefined)
reflectionMergePartial<FunctionDiff>(this, message, value);
reflectionMergePartial<SymbolDiff>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FunctionDiff): FunctionDiff {
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SymbolDiff): SymbolDiff {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
@@ -1225,6 +1299,9 @@ class FunctionDiff$Type extends MessageType<FunctionDiff> {
case /* optional float match_percent */ 3:
message.match_percent = reader.float();
break;
case /* optional objdiff.diff.SymbolRef target */ 5:
message.target = SymbolRef.internalBinaryRead(reader, reader.uint32(), options, message.target);
break;
default:
let u = options.readUnknownField;
if (u === "throw")
@@ -1236,7 +1313,7 @@ class FunctionDiff$Type extends MessageType<FunctionDiff> {
}
return message;
}
internalBinaryWrite(message: FunctionDiff, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
internalBinaryWrite(message: SymbolDiff, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* objdiff.diff.Symbol symbol = 1; */
if (message.symbol)
Symbol.internalBinaryWrite(message.symbol, writer.tag(1, WireType.LengthDelimited).fork(), options).join();
@@ -1246,6 +1323,9 @@ class FunctionDiff$Type extends MessageType<FunctionDiff> {
/* optional float match_percent = 3; */
if (message.match_percent !== undefined)
writer.tag(3, WireType.Bit32).float(message.match_percent);
/* optional objdiff.diff.SymbolRef target = 5; */
if (message.target)
SymbolRef.internalBinaryWrite(message.target, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
@@ -1253,9 +1333,9 @@ class FunctionDiff$Type extends MessageType<FunctionDiff> {
}
}
/**
* @generated MessageType for protobuf message objdiff.diff.FunctionDiff
* @generated MessageType for protobuf message objdiff.diff.SymbolDiff
*/
export const FunctionDiff = new FunctionDiff$Type();
export const SymbolDiff = new SymbolDiff$Type();
// @generated message type with reflection information, may provide speed optimized methods
class DataDiff$Type extends MessageType<DataDiff> {
constructor() {
@@ -1327,7 +1407,7 @@ class SectionDiff$Type extends MessageType<SectionDiff> {
{ no: 2, name: "kind", kind: "enum", T: () => ["objdiff.diff.SectionKind", SectionKind] },
{ no: 3, name: "size", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 4, name: "address", kind: "scalar", T: 4 /*ScalarType.UINT64*/, L: 0 /*LongType.BIGINT*/ },
{ no: 5, name: "functions", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => FunctionDiff },
{ no: 5, name: "symbols", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => SymbolDiff },
{ no: 6, name: "data", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => DataDiff },
{ no: 7, name: "match_percent", kind: "scalar", localName: "match_percent", opt: true, T: 2 /*ScalarType.FLOAT*/ }
]);
@@ -1338,7 +1418,7 @@ class SectionDiff$Type extends MessageType<SectionDiff> {
message.kind = 0;
message.size = 0n;
message.address = 0n;
message.functions = [];
message.symbols = [];
message.data = [];
if (value !== undefined)
reflectionMergePartial<SectionDiff>(this, message, value);
@@ -1361,8 +1441,8 @@ class SectionDiff$Type extends MessageType<SectionDiff> {
case /* uint64 address */ 4:
message.address = reader.uint64().toBigInt();
break;
case /* repeated objdiff.diff.FunctionDiff functions */ 5:
message.functions.push(FunctionDiff.internalBinaryRead(reader, reader.uint32(), options));
case /* repeated objdiff.diff.SymbolDiff symbols */ 5:
message.symbols.push(SymbolDiff.internalBinaryRead(reader, reader.uint32(), options));
break;
case /* repeated objdiff.diff.DataDiff data */ 6:
message.data.push(DataDiff.internalBinaryRead(reader, reader.uint32(), options));
@@ -1394,9 +1474,9 @@ class SectionDiff$Type extends MessageType<SectionDiff> {
/* uint64 address = 4; */
if (message.address !== 0n)
writer.tag(4, WireType.Varint).uint64(message.address);
/* repeated objdiff.diff.FunctionDiff functions = 5; */
for (let i = 0; i < message.functions.length; i++)
FunctionDiff.internalBinaryWrite(message.functions[i], writer.tag(5, WireType.LengthDelimited).fork(), options).join();
/* repeated objdiff.diff.SymbolDiff symbols = 5; */
for (let i = 0; i < message.symbols.length; i++)
SymbolDiff.internalBinaryWrite(message.symbols[i], writer.tag(5, WireType.LengthDelimited).fork(), options).join();
/* repeated objdiff.diff.DataDiff data = 6; */
for (let i = 0; i < message.data.length; i++)
DataDiff.internalBinaryWrite(message.data[i], writer.tag(6, WireType.LengthDelimited).fork(), options).join();
+36
View File
@@ -0,0 +1,36 @@
export type DiffMessage = {
type: 'diff';
data: ArrayBuffer;
};
export type TaskMessage = {
type: 'task';
taskType: string;
running: boolean;
};
export type StateMessage = {
type: 'state';
configLoaded: boolean;
currentFile: string | null;
};
// extension -> webview
export type InboundMessage = DiffMessage | TaskMessage | StateMessage;
export type ReadyMessage = {
type: 'ready';
};
export type LineRangesMessage = {
type: 'lineRanges';
data: Array<{ start: number; end: number }>;
};
export type RunTaskMessage = {
type: 'runTask';
taskType: string;
};
// webview -> extension
export type OutboundMessage = ReadyMessage | LineRangesMessage | RunTaskMessage;
+191 -43
View File
@@ -1,5 +1,6 @@
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import type { InboundMessage, OutboundMessage } from '../shared/messages';
import { DEFAULT_WATCH_PATTERNS, type ObjdiffConfiguration } from './config';
const CONFIG_FILENAME = 'objdiff.json';
@@ -7,13 +8,21 @@ const CONFIG_FILENAME = 'objdiff.json';
export class ObjdiffWorkspace extends vscode.Disposable {
public config?: ObjdiffConfiguration;
public configWatcher: vscode.FileSystemWatcher;
public currentFile?: string;
public workspaceWatcher?: vscode.FileSystemWatcher;
public onDidChangeConfig: vscode.Event<ObjdiffConfiguration | undefined>;
public onDidChangeCurrentFile: vscode.Event<string | undefined>;
private subscriptions: vscode.Disposable[] = [];
private wwSubscriptions: vscode.Disposable[] = [];
private currentDiff?: vscode.Uri;
// private currentTask?: () => void;
private pathMatcher?: picomatch.Matcher;
private didChangeConfigEmitter = new vscode.EventEmitter<
ObjdiffConfiguration | undefined
>();
private didChangeCurrentFileEmitter = new vscode.EventEmitter<
string | undefined
>();
constructor(
public readonly chan: vscode.LogOutputChannel,
@@ -23,6 +32,9 @@ export class ObjdiffWorkspace extends vscode.Disposable {
super(() => {
this.disposeImpl();
});
this.onDidChangeConfig = this.didChangeConfigEmitter.event;
this.onDidChangeCurrentFile = this.didChangeCurrentFileEmitter.event;
this.configWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(workspaceFolder, CONFIG_FILENAME),
);
@@ -139,6 +151,7 @@ export class ObjdiffWorkspace extends vscode.Disposable {
this.wwSubscriptions,
);
}
this.didChangeConfigEmitter.fire(this.config);
this.tryDiff();
}
@@ -156,25 +169,22 @@ export class ObjdiffWorkspace extends vscode.Disposable {
this.tryDiff();
}
tryDiff() {
private tryUpdateActiveFile() {
if (!this.config) {
return;
}
if (!this.currentDiff) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
this.chan.warn('No active editor');
return;
}
if (activeEditor.document.uri.scheme !== 'file') {
this.chan.warn('Active editor not a file');
return;
}
this.currentDiff = activeEditor.document.uri;
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
this.chan.warn('No active editor');
return;
}
const fsPath = this.currentDiff.fsPath;
if (activeEditor.document.uri.scheme !== 'file') {
this.chan.warn('Active editor not a file');
return;
}
const fsPath = activeEditor.document.uri.fsPath;
if (!fsPath.startsWith(this.workspaceFolder.uri.fsPath)) {
this.chan.warn('Active editor not in workspace');
this.chan.warn('Active editor not in workspace', fsPath);
return;
}
const relPath = fsPath.slice(this.workspaceFolder.uri.fsPath.length + 1);
@@ -182,7 +192,27 @@ export class ObjdiffWorkspace extends vscode.Disposable {
(obj) => obj.metadata?.source_path === relPath,
);
if (!obj) {
this.chan.warn('No object found for', relPath);
this.chan.warn('No object found for', this.currentFile);
return;
}
this.currentFile = relPath;
this.didChangeCurrentFileEmitter.fire(this.currentFile);
}
tryDiff() {
if (!this.config) {
return;
}
this.tryUpdateActiveFile();
if (!this.currentFile) {
this.chan.warn('No active file');
return;
}
const obj = (this.config.units || this.config.objects)?.find(
(obj) => obj.metadata?.source_path === this.currentFile,
);
if (!obj) {
this.chan.warn('No object found for', this.currentFile);
return;
}
const targetPath =
@@ -252,6 +282,7 @@ export class ObjdiffWorkspace extends vscode.Disposable {
const task = new vscode.Task(
{
type: 'objdiff',
taskType: 'build',
startTime,
},
this.workspaceFolder,
@@ -312,6 +343,11 @@ export function activate(context: vscode.ExtensionContext) {
// const storageDir = storageUri.fsPath;
// chan.info('Storage directory: ' + storageDir);
const webviews: {
webview: vscode.Webview;
subscriptions: vscode.Disposable[];
}[] = [];
let workspace: ObjdiffWorkspace | undefined;
if (vscode.workspace.workspaceFolders?.[0]) {
workspace = new ObjdiffWorkspace(
@@ -319,18 +355,63 @@ export function activate(context: vscode.ExtensionContext) {
vscode.workspace.workspaceFolders[0],
storageUri,
);
workspace.onDidChangeConfig(
(config) => {
for (const view of webviews) {
view.webview.postMessage({
type: 'state',
configLoaded: !!config,
currentFile: workspace?.currentFile || null,
} as InboundMessage);
}
},
undefined,
context.subscriptions,
);
workspace.onDidChangeCurrentFile(
(currentFile) => {
for (const view of webviews) {
view.webview.postMessage({
type: 'state',
configLoaded: !!workspace?.config,
currentFile,
} as InboundMessage);
}
},
undefined,
context.subscriptions,
);
context.subscriptions.push(workspace);
}
chan.info('Workspace folders', vscode.workspace.workspaceFolders);
vscode.workspace.onDidChangeWorkspaceFolders(
(e) => {
chan.info('Workspace folders changed', e);
},
undefined,
context.subscriptions,
);
context.subscriptions.push(
vscode.commands.registerCommand('objdiff.build', () => {
if (workspace) {
workspace.tryDiff();
}
workspace?.tryDiff();
}),
);
const webviews: vscode.Webview[] = [];
context.subscriptions.push(
vscode.commands.registerCommand('objdiff.copySymbolName', (opts) => {
chan.info('Copy command', opts);
vscode.env.clipboard.writeText(opts.symbolName);
}),
);
context.subscriptions.push(
vscode.commands.registerCommand(
'objdiff.copySymbolDemangledName',
(opts) => {
chan.info('Copy demangled command', opts);
vscode.env.clipboard.writeText(opts.symbolDemangledName);
},
),
);
const backgroundColors = [
'rgba(255, 0, 255, 0.3)',
@@ -350,8 +431,42 @@ export function activate(context: vscode.ExtensionContext) {
});
});
context.subscriptions.push(
vscode.tasks.onDidEndTaskProcess(async (e) => {
vscode.tasks.onDidStartTask(
(e) => {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
for (const view of webviews) {
view.webview.postMessage({
type: 'task',
taskType: e.execution.task.definition.taskType,
running: true,
} as InboundMessage);
}
},
undefined,
context.subscriptions,
);
vscode.tasks.onDidEndTask(
(e) => {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
for (const view of webviews) {
view.webview.postMessage({
type: 'task',
taskType: e.execution.task.definition.taskType,
running: false,
} as InboundMessage);
}
},
undefined,
context.subscriptions,
);
let cachedData: Uint8Array | null = null;
vscode.tasks.onDidEndTaskProcess(
async (e) => {
if (e.execution.task.definition.type !== 'objdiff') {
return;
}
@@ -373,18 +488,15 @@ export function activate(context: vscode.ExtensionContext) {
'with size',
data.byteLength,
);
// const diff = diff_pb.DiffResult.fromBinary(data);
// treeDataProvider.update(diff);
// vscode.window.showInformationMessage('Diff complete');
// console.log('webviews', webviews);
for (const webview of webviews) {
webview.postMessage({
for (const view of webviews) {
view.webview.postMessage({
type: 'diff',
data: data.buffer,
});
} as InboundMessage);
}
cachedData = data;
} catch (reason) {
chan.error('Failed to read output file', reason);
workspace?.showError('Failed to read output file', reason);
}
} else {
workspace?.showError(`Build failed with code ${e.exitCode}`);
@@ -392,10 +504,12 @@ export function activate(context: vscode.ExtensionContext) {
vscode.workspace.fs.delete(outputUri).then(
() => {},
(reason) => {
chan.error('Failed to delete output file', reason);
workspace?.showError('Failed to delete output file', reason);
},
);
}),
},
undefined,
context.subscriptions,
);
const textDecoder = new TextDecoder();
@@ -423,10 +537,24 @@ export function activate(context: vscode.ExtensionContext) {
enableScripts: true,
};
view.webview.html = html;
context.subscriptions.push(
view.webview.onDidReceiveMessage((message) => {
const subscriptions: vscode.Disposable[] = [];
view.webview.onDidReceiveMessage(
(untypedMessage) => {
const message = untypedMessage as OutboundMessage;
if (message.type === 'ready') {
chan.info('Webview ready');
view.webview.postMessage({
type: 'state',
configLoaded: !!workspace?.config,
currentFile: workspace?.currentFile || null,
} as InboundMessage);
if (cachedData) {
chan.info('Sending cached diff to webview');
view.webview.postMessage({
type: 'diff',
data: cachedData.buffer,
} as InboundMessage);
}
} else if (message.type === 'lineRanges') {
for (const editor of vscode.window.visibleTextEditors) {
if (editor.document.uri.scheme !== 'file') {
@@ -444,18 +572,38 @@ export function activate(context: vscode.ExtensionContext) {
idx = (idx + 1) % decorationTypes.length;
}
}
} else if (message.type === 'runTask') {
if (message.taskType === 'build') {
workspace?.tryDiff();
} else {
chan.warn('Unknown task type', message.taskType);
}
} else {
chan.warn('Unknown message', message);
}
}),
},
undefined,
subscriptions,
);
context.subscriptions.push(
view.onDidDispose(() => {
const idx = webviews.indexOf(view.webview);
if (idx >= 0) {
webviews.splice(idx, 1);
view.onDidDispose(
() => {
for (const sub of subscriptions) {
sub.dispose();
}
}),
for (let i = 0; i < webviews.length; i++) {
if (webviews[i].webview === view.webview) {
webviews.splice(i, 1);
break;
}
}
},
undefined,
context.subscriptions,
);
webviews.push(view.webview);
webviews.push({
webview: view.webview,
subscriptions,
});
},
}),
);
+1 -1
View File
@@ -19,5 +19,5 @@
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src", "webview", "gen"]
"include": ["src", "webview", "shared"]
}
+57 -20
View File
@@ -1,28 +1,32 @@
:root {
--font-size: var(--vscode-editor-font-size, 13px);
--list-row-height: calc(var(--font-size) * 1.33);
color-scheme: light dark;
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
color: var(--vscode-editor-foreground, #fff);
font-family: var(
--vscode-font-family,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
sans-serif
);
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);
&.vscode-light,
&.vscode-high-contrast-light {
color-scheme: light;
}
&.vscode-dark,
&.vscode-high-contrast-dark {
color-scheme: dark;
}
}
#root {
display: flex;
flex-flow: column;
min-height: 100vh;
}
@@ -34,15 +38,48 @@ body {
text-align: center;
flex-direction: column;
justify-content: center;
h1 {
font-size: 3.6rem;
font-weight: 700;
}
p {
font-size: 1.2rem;
font-weight: 400;
opacity: 0.5;
}
}
.content h1 {
font-size: 3.6rem;
font-weight: 700;
}
button {
background-color: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
border: 1px solid var(--vscode-button-border);
border-radius: 3px;
cursor: pointer;
.content p {
font-size: 1.2rem;
font-weight: 400;
opacity: 0.5;
font-family: var(--vscode-font-family, system-ui);
font-weight: var(--vscode-font-weight, normal);
font-size: var(--vscode-font-size, 13px);
&:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
&:focus {
opacity: 1;
outline-color: var(--vscode-focusBorder);
outline-offset: -1px;
outline-style: solid;
outline-width: 1px;
}
&:active {
outline: 0 !important;
background-color: var(--vscode-toolbar-activeBackground);
}
&:disabled {
color: var(--vscode-disabledForeground);
}
}
+28 -12
View File
@@ -1,30 +1,30 @@
import './App.css';
import { SectionKind } from '../gen/diff_pb';
import { SectionKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
FunctionDiff,
ObjectDiff,
} from '../gen/diff_pb';
SymbolDiff,
} from '../shared/gen/diff_pb';
import FunctionView from './FunctionView';
import SymbolsView from './SymbolsView';
import { useAppStore, useDiffStore } from './state';
import { useAppStore, useExtensionStore, vscode } from './state';
import type { SymbolRefByName } from './state';
const findSymbol = (
obj: ObjectDiff | undefined,
symbolRef: SymbolRefByName | null,
): FunctionDiff | null => {
): SymbolDiff | null => {
if (!obj || !symbolRef) {
return null;
}
for (const section of obj.sections) {
if (section.name === symbolRef.section_name) {
if (section.kind === SectionKind.SECTION_TEXT) {
for (const fn of section.functions) {
const symbol = fn.symbol as DiffSymbol;
for (const diff of section.symbols) {
const symbol = diff.symbol as DiffSymbol;
if (symbol.name === symbolRef.symbol_name) {
return fn;
return diff;
}
}
}
@@ -34,23 +34,39 @@ const findSymbol = (
};
const App = () => {
const { diff } = useDiffStore();
const { diff } = useExtensionStore();
const selectedSymbolRef = useAppStore((state) => state.selectedSymbol);
const buildRunning = useExtensionStore((state) => state.buildRunning);
const configLoaded = useExtensionStore((state) => state.configLoaded);
if (diff) {
const object = diff.left || diff.right || { sections: [] };
const leftSymbol = findSymbol(diff.left, selectedSymbolRef);
const rightSymbol = findSymbol(diff.right, selectedSymbolRef);
if (leftSymbol || rightSymbol) {
return <FunctionView left={leftSymbol} right={rightSymbol} />;
}
return <SymbolsView obj={object} />;
return <SymbolsView diff={diff} />;
}
return (
<div className="content">
<h1>objdiff</h1>
<p>Coming soon to a VS Code near you.</p>
{configLoaded ? (
<p>
Open a source file and{' '}
<button
onClick={() =>
vscode.postMessage({ type: 'runTask', taskType: 'build' })
}
disabled={buildRunning}
>
Build
</button>
.
</p>
) : (
<p>No configuration loaded.</p>
)}
</div>
);
};
+70 -65
View File
@@ -1,4 +1,5 @@
.instruction-list {
flex: 1 1 auto;
}
.instruction-row {
@@ -16,87 +17,91 @@
text-wrap: nowrap;
white-space: pre;
overflow: hidden;
padding-left: 0.5em;
}
.highlightable {
cursor: pointer;
}
.diff_change {
color: #6d6dff;
}
.rotation0 {
color: magenta;
}
.rotation1 {
color: cyan;
}
.rotation2 {
color: rgb(0, 212, 0);
}
.rotation3 {
color: red;
}
.rotation4 {
color: rgb(103, 106, 255);
}
.rotation5 {
color: lightpink;
}
.rotation6 {
color: lightcyan;
}
.rotation7 {
color: lightgreen;
}
.rotation8 {
color: grey;
}
.symbol {
color: white;
}
.line-number {
color: var(--vscode-editorLineNumber-foreground);
}
.diff_any {
background-color: rgba(255, 255, 255, 0.02);
}
.diff_change {
color: #6d6dff;
}
.diff_add {
color: #45bd00;
}
.diff_remove {
color: #c82829;
}
.symbol {
color: light-dark(black, white);
}
:global(body.vscode-light) {
.rotation0 {
.rotation0 {
color: light-dark(
/* hsv(0° 60% 80%) */
color: rgb(205, 82, 82);
}
.rotation1 {
rgb(205, 82, 82),
magenta
);
}
.rotation1 {
color: light-dark(
/* hsv(40° 60% 80%) */
color: rgb(205, 164, 82);
}
.rotation2 {
rgb(205, 164, 82),
cyan
);
}
.rotation2 {
color: light-dark(
/* hsv(80° 60% 80%) */
color: rgb(164, 205, 82);
}
.rotation3 {
rgb(164, 205, 82),
rgb(0, 212, 0)
);
}
.rotation3 {
color: light-dark(
/* hsv(120° 60% 80%) */
color: rgb(82, 205, 82);
}
.rotation4 {
rgb(82, 205, 82),
red
);
}
.rotation4 {
color: light-dark(
/* hsv(160° 60% 80%) */
color: rgb(82, 205, 164);
}
.rotation5 {
rgb(82, 205, 164),
rgb(103, 106, 255)
);
}
.rotation5 {
color: light-dark(
/* hsv(200° 60% 80%) */
color: rgb(82, 164, 205);
}
.rotation6 {
rgb(82, 164, 205),
lightpink
);
}
.rotation6 {
color: light-dark(
/* hsv(240° 60% 80%) */
color: rgb(82, 82, 205);
}
.rotation7 {
rgb(82, 82, 205),
lightcyan
);
}
.rotation7 {
color: light-dark(
/* hsv(280° 60% 80%) */
color: rgb(164, 82, 205);
}
.rotation8 {
rgb(164, 82, 205),
lightgreen
);
}
.rotation8 {
color: light-dark(
/* hsv(320° 60% 80%) */
color: rgb(205, 82, 164);
}
.symbol {
color: black;
}
rgb(205, 82, 164),
grey
);
}
+67 -24
View File
@@ -1,18 +1,20 @@
import styles from './FunctionView.module.css';
import headerStyles from './Header.module.css';
import clsx from 'clsx';
import memoize from 'memoize-one';
import { memo } from 'react';
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 { DiffKind } from '../gen/diff_pb';
import { DiffKind } from '../shared/gen/diff_pb';
import type {
Symbol as DiffSymbol,
FunctionDiff,
InstructionDiff,
} from '../gen/diff_pb';
SymbolDiff,
} from '../shared/gen/diff_pb';
import { displayDiff } from './diff';
import { useAppStore } from './state';
import { useFontSize } from './util';
const AsmCell = ({
@@ -132,13 +134,30 @@ const AsmCell = ({
index++;
}
});
return <div className={styles.instructionCell}>{out}</div>;
const classes = [styles.instructionCell];
if (insDiff.diff_kind) {
classes.push(styles.diff_any);
}
switch (insDiff.diff_kind) {
case DiffKind.DIFF_DELETE:
classes.push(styles.diff_remove);
break;
case DiffKind.DIFF_INSERT:
classes.push(styles.diff_add);
break;
case DiffKind.DIFF_REPLACE:
classes.push(styles.diff_change);
break;
}
return <div className={clsx(classes)}>{out}</div>;
};
type ItemData = {
itemCount: number;
left: FunctionDiff | null;
right: FunctionDiff | null;
left: SymbolDiff | null;
right: SymbolDiff | null;
};
const AsmRow = memo(
@@ -160,7 +179,7 @@ const AsmRow = memo(
);
const createItemData = memoize(
(left: FunctionDiff | null, right: FunctionDiff | null) => {
(left: SymbolDiff | null, right: SymbolDiff | null) => {
const itemCount = Math.max(
left?.instructions.length || 0,
right?.instructions.length || 0,
@@ -172,25 +191,49 @@ const createItemData = memoize(
const FunctionView = ({
left,
right,
}: { left: FunctionDiff | null; right: FunctionDiff | null }) => {
}: { left: SymbolDiff | null; right: SymbolDiff | null }) => {
const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol);
const setSymbolScrollOffset = useAppStore(
(state) => state.setSymbolScrollOffset,
);
const symbolName = left?.symbol?.name || right?.symbol?.name || '';
const initialScrollOffset = useMemo(
() => useAppStore.getState().symbolScrollOffsets[symbolName] || 0,
[symbolName],
);
const itemSize = useFontSize() * 1.33;
const itemData = createItemData(left, right);
const demangledName =
left?.symbol?.demangled_name || right?.symbol?.demangled_name;
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
className={styles.instructionList}
height={height}
itemCount={itemData.itemCount}
itemSize={itemSize}
width={width}
itemData={itemData}
overscanCount={20}
>
{AsmRow}
</FixedSizeList>
)}
</AutoSizer>
<>
<div className={headerStyles.header}>
<button onClick={() => setSelectedSymbol(null)}>Back</button>
<span title={demangledName}>{demangledName}</span>
</div>
<div className={styles.instructionList}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
height={height}
itemCount={itemData.itemCount}
itemSize={itemSize}
width={width}
itemData={itemData}
overscanCount={20}
onScroll={(e) => {
setSymbolScrollOffset(symbolName, e.scrollOffset);
}}
initialScrollOffset={initialScrollOffset}
>
{AsmRow}
</FixedSizeList>
)}
</AutoSizer>
</div>
</>
);
};
+19
View File
@@ -0,0 +1,19 @@
.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 {
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;
}
}
+41 -3
View File
@@ -1,4 +1,14 @@
.symbols {
flex: 1 1 0;
display: flex;
flex-flow: row;
overflow: auto;
gap: 0.5em;
}
.symbol-list {
flex: 1 1 0;
overflow-x: scroll;
list-style-type: none;
margin: 0;
padding: 0;
@@ -6,7 +16,13 @@
li {
cursor: pointer;
user-select: none;
/* margin: 0 0.5rem; */
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);
text-wrap: nowrap;
white-space: pre;
&:hover {
background-color: var(--vscode-list-hoverBackground);
@@ -15,9 +31,31 @@
}
.section {
padding-left: 0;
padding-left: 0.5em;
}
.symbol {
padding-left: 0.5rem;
padding-left: 2em;
}
.flag-global {
color: lightgreen;
}
.flag-weak {
/* todo */
}
.symbol-name {
color: light-dark(black, white);
}
.percent-100 {
color: lightgreen;
}
.percent-50 {
color: lightblue;
}
.percent-0 {
color: lightcoral;
}
+130 -25
View File
@@ -1,42 +1,147 @@
import headerStyles from './Header.module.css';
import styles from './SymbolsView.module.css';
import { SectionKind } from '../gen/diff_pb';
import type { Symbol as DiffSymbol, ObjectDiff } from '../gen/diff_pb';
import { useAppStore } from './state';
import {
type DiffResult,
type Symbol as DiffSymbol,
type ObjectDiff,
SymbolFlag,
} from '../shared/gen/diff_pb';
import { useAppStore, useExtensionStore, vscode } from './state';
const SymbolsView = ({ obj }: { obj: ObjectDiff }) => {
const SymbolsList = ({ obj }: { obj: ObjectDiff }) => {
const setSelectedSymbol = useAppStore((state) => state.setSelectedSymbol);
const items = [];
let sectionIndex = 0;
for (const section of obj.sections) {
const sectionKey = `section-${sectionIndex++}`;
let percentElem = null;
if (section.match_percent != null) {
let className = styles.percent0;
if (section.match_percent === 100) {
className = styles.percent100;
} else if (section.match_percent >= 50) {
className = styles.percent50;
}
percentElem = (
<>
{' ('}
<span className={className}>
{Math.floor(section.match_percent).toFixed(0)}%
</span>
{')'}
</>
);
}
items.push(
<li key={section.name} className={styles.section}>
{section.name}
<li key={sectionKey} className={styles.section}>
{section.name} ({section.size.toString(16)}){percentElem}
</li>,
);
if (section.kind === SectionKind.SECTION_TEXT) {
for (const fn of section.functions) {
const symbol = fn.symbol as DiffSymbol;
items.push(
<li
key={symbol.name}
className={styles.symbol}
onClick={() => {
setSelectedSymbol({
symbol_name: symbol.name,
section_name: section.name,
});
}}
>
{symbol.name}
</li>,
for (const diff of section.symbols) {
const symbol = diff.symbol as DiffSymbol;
const flags = [];
if (symbol.flags & SymbolFlag.SYMBOL_GLOBAL) {
flags.push(
<span key="g" className={styles.flagGlobal}>
g
</span>,
);
}
} else {
// TODO
if (symbol.flags & SymbolFlag.SYMBOL_WEAK) {
flags.push(
<span key="w" className={styles.flagWeak}>
w
</span>,
);
}
if (symbol.flags & SymbolFlag.SYMBOL_LOCAL) {
flags.push(
<span key="l" className={styles.flagLocal}>
l
</span>,
);
}
if (symbol.flags & SymbolFlag.SYMBOL_COMMON) {
flags.push(
<span key="c" className={styles.flagCommon}>
c
</span>,
);
}
let flagsElem = null;
if (flags.length > 0) {
flagsElem = <>[{flags}] </>;
}
let percentElem = null;
if (diff.match_percent != null) {
let className = styles.percent0;
if (diff.match_percent === 100) {
className = styles.percent100;
} else if (diff.match_percent >= 50) {
className = styles.percent50;
}
percentElem = (
<>
{'('}
<span className={className}>
{Math.floor(diff.match_percent).toFixed(0)}%
</span>
{') '}
</>
);
}
items.push(
<li
key={`${sectionKey}-${symbol.name}`}
className={styles.symbol}
onClick={() => {
setSelectedSymbol({
symbol_name: symbol.name,
section_name: section.name,
});
}}
data-vscode-context={JSON.stringify({
contextType: 'symbol',
preventDefaultContextMenuItems: true,
symbolName: symbol.name,
symbolDemangledName: symbol.demangled_name,
})}
>
{flagsElem}
{percentElem}
<span className={styles.symbolName}>
{symbol.demangled_name || symbol.name}
</span>
</li>,
);
}
}
return <ul className={styles.symbolList}>{items}</ul>;
};
const SymbolsView = ({ diff }: { diff: DiffResult }) => {
const buildRunning = useExtensionStore((state) => state.buildRunning);
const currentFile = useExtensionStore((state) => state.currentFile);
return (
<>
<div className={headerStyles.header}>
<button
onClick={() =>
vscode.postMessage({ type: 'runTask', taskType: 'build' })
}
disabled={buildRunning}
>
Build
</button>
{buildRunning ? <span>Building...</span> : <span>{currentFile}</span>}
</div>
<div className={styles.symbols}>
{diff.left && <SymbolsList obj={diff.left} />}
{diff.right && <SymbolsList obj={diff.right} />}
</div>
</>
);
};
export default SymbolsView;
+1 -1
View File
@@ -2,7 +2,7 @@ import type {
ArgumentValue,
InstructionDiff,
RelocationTarget,
} from '../gen/diff_pb';
} from '../shared/gen/diff_pb';
export type DiffText =
| DiffTextBasic
+99 -45
View File
@@ -1,6 +1,6 @@
import type { WebviewApi } from 'vscode-webview';
import { create } from 'zustand';
import { DiffKind, DiffResult } from '../gen/diff_pb';
import { DiffResult } from '../shared/gen/diff_pb';
import type { InboundMessage, OutboundMessage } from '../shared/messages';
export type SymbolRefByName = {
symbol_name: string;
@@ -9,23 +9,48 @@ export type SymbolRefByName = {
export interface AppState {
selectedSymbol: SymbolRefByName | null;
symbolScrollOffsets: Record<string, number>;
setSelectedSymbol: (selectedSymbol: SymbolRefByName | null) => void;
setSymbolScrollOffset: (symbolName: string, offset: number) => void;
}
export const useAppStore = create<AppState>((set) => ({
selectedSymbol: null,
symbolScrollOffsets: {},
setSelectedSymbol: (selectedSymbol) => set({ selectedSymbol }),
setSymbolScrollOffset: (symbolName, offset) =>
set((state) => ({
symbolScrollOffsets: {
...state.symbolScrollOffsets,
[symbolName]: offset,
},
})),
}));
export type DiffState = {
diff?: DiffResult;
export type ExtensionState = {
diff: DiffResult | null;
buildRunning: boolean;
configLoaded: boolean;
currentFile: string | null;
};
export const useDiffStore = create<DiffState>(() => ({}));
export const useExtensionStore = create<ExtensionState>(() => ({
diff: null,
buildRunning: false,
configLoaded: false,
currentFile: null,
}));
let vscode: WebviewApi<DiffState>;
// Copy of vscode.WebviewApi with concrete message types
export interface MyWebviewApi<StateType> {
postMessage(message: OutboundMessage): void;
getState(): StateType | undefined;
setState<T extends StateType | undefined>(newState: T): T;
}
let vscode: MyWebviewApi<AppState>;
if (typeof acquireVsCodeApi === 'function') {
vscode = acquireVsCodeApi<DiffState>();
vscode = acquireVsCodeApi<AppState>();
} else {
let state: DiffState | undefined;
let state: AppState | undefined;
vscode = {
postMessage: () => {},
getState: () => state,
@@ -35,48 +60,77 @@ if (typeof acquireVsCodeApi === 'function') {
},
};
}
const storedState = vscode.getState();
if (storedState) {
useAppStore.setState(storedState);
}
let timeoutId: ReturnType<typeof setTimeout> | undefined;
useAppStore.subscribe((state) => {
// Debounce state updates
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
vscode.setState(state);
timeoutId = undefined;
}, 100);
});
vscode.postMessage({ type: 'ready' });
export { vscode };
window.addEventListener('message', (event) => {
const message = event.data;
if (message && typeof message === 'object') {
console.log('Received message', message);
if (message.type === 'diff') {
const diff = DiffResult.fromBinary(new Uint8Array(message.data));
const message = event.data as InboundMessage;
if (message.type === 'diff') {
const start = performance.now();
const diff = DiffResult.fromBinary(new Uint8Array(message.data));
const end = performance.now();
console.debug('Diff deserialization time:', end - start, 'ms');
const lineRanges = [];
for (const section of diff.right?.sections || []) {
for (const fn of section.functions) {
let currentRange: { start: number; end: number } | null = null;
for (const ins of fn.instructions) {
let lineNumber = ins.instruction?.line_number;
if (lineNumber == null) {
continue;
}
lineNumber = lineNumber - 1;
if (ins.diff_kind !== DiffKind.DIFF_NONE) {
if (currentRange !== null) {
currentRange.end = lineNumber;
} else {
currentRange = {
start: lineNumber,
end: lineNumber,
};
}
} else if (currentRange !== null && lineNumber > currentRange.end) {
lineRanges.push(currentRange);
currentRange = null;
}
}
if (currentRange !== null) {
lineRanges.push(currentRange);
}
}
}
console.log('lineRanges', lineRanges);
vscode.postMessage({ type: 'lineRanges', data: lineRanges });
// const lineRanges = [];
// for (const section of diff.right?.sections || []) {
// for (const diff of section.symbols) {
// let currentRange: { start: number; end: number } | null = null;
// for (const ins of diff.instructions) {
// let lineNumber = ins.instruction?.line_number;
// if (lineNumber == null) {
// continue;
// }
// lineNumber = lineNumber - 1;
// if (ins.diff_kind !== DiffKind.DIFF_NONE) {
// if (currentRange !== null) {
// currentRange.end = lineNumber;
// } else {
// currentRange = {
// start: lineNumber,
// end: lineNumber,
// };
// }
// } else if (currentRange !== null && lineNumber > currentRange.end) {
// lineRanges.push(currentRange);
// currentRange = null;
// }
// }
// if (currentRange !== null) {
// lineRanges.push(currentRange);
// }
// }
// }
// console.log('lineRanges', lineRanges);
// vscode.postMessage({ type: 'lineRanges', data: lineRanges });
useDiffStore.setState({ diff });
useExtensionStore.setState({ diff });
} else if (message.type === 'task') {
if (message.taskType === 'build') {
useExtensionStore.setState({ buildRunning: message.running });
} else {
console.error('Unknown task type', message.taskType);
}
} else if (message.type === 'state') {
useExtensionStore.setState({
configLoaded: message.configLoaded,
currentFile: message.currentFile,
});
} else {
console.error('Unknown message', message);
}
});