Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1fe5adc
decompile using worker
deirn Feb 2, 2026
f4365a3
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 2, 2026
b4b92c4
merge fixes
deirn Feb 2, 2026
b01fe56
fix jar index
deirn Feb 2, 2026
6099a41
decompile whole jar
deirn Feb 2, 2026
6477d98
it now runs on chrome
deirn Feb 2, 2026
89d469b
hack
deirn Feb 4, 2026
607185d
update vf
deirn Feb 4, 2026
4476df8
ui
deirn Feb 5, 2026
ad41d47
fix
deirn Feb 5, 2026
072fd3f
format some files
deirn Feb 6, 2026
d225f5a
show metrics on modal
deirn Feb 6, 2026
5ca7d9e
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 6, 2026
e68f497
remove in memory decompilation cache, fix spinner
deirn Feb 6, 2026
0604344
fix bytecode
deirn Feb 6, 2026
51669b3
fix token collector
deirn Feb 6, 2026
29ee77e
remove log
deirn Feb 6, 2026
7499de5
add setting to vf runtime type
deirn Feb 7, 2026
28e95c2
decompile options
deirn Feb 7, 2026
6e2daf3
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 7, 2026
bafb110
move decompiler settings to logic/Settings
deirn Feb 7, 2026
5283eb7
make sure the jar is cleaned up after decompiling
deirn Feb 7, 2026
198d496
fix line changes not working
deirn Feb 7, 2026
542aa6b
create workers as needed, close after jar decompile
deirn Feb 7, 2026
ebfc8e5
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 7, 2026
6705c79
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 8, 2026
3dfc4e4
Merge remote-tracking branch 'origin/main' into deirn/decompile-worker
deirn Feb 8, 2026
eda7a21
decompile error
deirn Feb 8, 2026
f33aea1
add e2e test decompiling some classes
deirn Feb 8, 2026
23369d1
use data-testid
deirn Feb 8, 2026
aee4624
fix e2e fail on webkit
deirn Feb 8, 2026
22fe48d
i think it deserves a comment
deirn Feb 8, 2026
5a65706
use official vf version
deirn Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
"dependencies": {
"@katana-project/zip": "^0.7.1",
"@monaco-editor/react": "^4.7.0",
"@run-slicer/vf": "^0.3.2-1.11.2",
"@run-slicer/vf": "^0.5.0-1.11.2",
"@types/dagre": "^0.7.53",
"@xyflow/react": "^12.10.0",
"antd": "^5.28.0",
"comlink": "^4.4.2",
"dagre": "^0.8.5",
"dexie": "^4.2.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"rxjs": "^7.8.2"
Expand Down
3 changes: 3 additions & 0 deletions public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
4 changes: 0 additions & 4 deletions src/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
declare module "*/java.wasm-runtime.js" {
export async function load(src: string);
}

declare module "*/vf.runtime.js" {
export function decompile(name: string, config?: Config): Promise<string>;
}
4 changes: 2 additions & 2 deletions src/javadoc/JavadocCmpletionProvider.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { editor, languages, Position, Token, type CancellationToken } from 'monaco-editor';
import type { DecompileResult } from '../logic/Decompiler';
import type { MemberToken } from '../logic/Tokens';
import type { DecompileResult } from '../workers/decompile/types';

export class JavdocCompletionProvider implements languages.CompletionItemProvider {
readonly decompileResult: DecompileResult;
Expand Down Expand Up @@ -105,4 +105,4 @@ export class JavdocCompletionProvider implements languages.CompletionItemProvide

return members;
}
}
}
8 changes: 4 additions & 4 deletions src/javadoc/JavadocCodeExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
type CancellationToken,
type IDisposable,
} from "monaco-editor";
import type { DecompileResult } from "../logic/Decompiler";
import { getTokenLocation, type Token, type TokenLocation } from "../logic/Tokens";
import { activeJavadocToken, getJavadocForToken, javadocData, refreshJavadocDataForClass, type JavadocData, type JavadocString } from "./Javadoc";
import type { DecompileResult } from "../workers/decompile/types";

type monaco = typeof import("monaco-editor");

Expand Down Expand Up @@ -44,7 +44,7 @@ export function applyJavadocCodeExtensions(monaco: monaco, editor: editor.IStand
});

const codeLense = monaco.languages.registerCodeLensProvider("java", {
provideCodeLenses: function (model: editor.ITextModel, token: CancellationToken): languages.ProviderResult<languages.CodeLensList> {
provideCodeLenses: function(model: editor.ITextModel, token: CancellationToken): languages.ProviderResult<languages.CodeLensList> {
const lenses: languages.CodeLens[] = [];

for (const token of decompile.tokens) {
Expand Down Expand Up @@ -79,7 +79,7 @@ export function applyJavadocCodeExtensions(monaco: monaco, editor: editor.IStand
const editJavadocCommand = monaco.editor.addEditorAction({
id: EDIT_JAVADOC_COMMAND_ID,
label: 'Edit Javadoc',
run: function (editor, ...args) {
run: function(editor, ...args) {
const token: Token = args[0];
activeJavadocToken.next(token);
}
Expand Down Expand Up @@ -123,4 +123,4 @@ function cacluateHeightInPx(domNode: HTMLDivElement): number {
domNode.style.visibility = '';

return heightInPx;
}
}
190 changes: 27 additions & 163 deletions src/logic/Decompiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@ import {
combineLatest, distinctUntilChanged, from, map, Observable, of, shareReplay, switchMap, tap, throttleTime
} from "rxjs";
import { minecraftJar, type MinecraftJar } from "./MinecraftApi";
import { decompile, type Options, type TokenCollector } from "./vf";
import type { Jar } from "../utils/Jar";
import type { Token } from "./Tokens";
import { bytecode, displayLambdas } from "./Settings";
import { getBytecode } from "../workers/JarIndex";
import { selectedFile } from "./State";

export interface DecompileResult {
className: string;
source: string;
tokens: Token[];
language: 'java' | 'bytecode';
}
import { bytecode, displayLambdas } from "./Settings";
import type { Options } from "./vf";
import type { DecompileResult } from "../workers/decompile/types";
import * as worker from "../workers/decompile/client";
import type { Jar } from "../utils/Jar";

const decompilerCounter = new BehaviorSubject<number>(0);

Expand All @@ -25,185 +18,56 @@ export const isDecompiling = decompilerCounter.pipe(
distinctUntilChanged()
);

export const DECOMPILER_OPTIONS: Options = {};
const decompilerOptions = combineLatest([
displayLambdas.observable
]).pipe(
distinctUntilChanged(),
switchMap(([displayLambdas]) => {
const options: Options = {};

const decompilationCache = new Map<string, DecompileResult>();
if (displayLambdas) {
options["mark-corresponding-synthetics"] = "1";
}

return of(options);
}),
);
decompilerOptions.subscribe(v => worker.setOptions(v));

export const currentResult = decompileResultPipeline(minecraftJar);
export function decompileResultPipeline(jar: Observable<MinecraftJar>): Observable<DecompileResult> {
return combineLatest([
selectedFile,
jar,
displayLambdas.observable,
bytecode.observable
bytecode.observable,
decompilerOptions,
]).pipe(
distinctUntilChanged(),
throttleTime(250),
switchMap(([className, jar, displayLambdas, bytecode]) => {
switchMap(([className, jar, bytecode]) => {
if (bytecode) {
return from(getClassBytecode(className, jar.jar));
}

let key = `${jar.version}:${className}`;

if (displayLambdas) {
key += ":lambdas";
}

const cached = decompilationCache.get(key);
if (cached) {
// Re-insert at end
decompilationCache.delete(key);
decompilationCache.set(key, cached);
return of(cached);
}

let options = { ...DECOMPILER_OPTIONS };

if (displayLambdas) {
options["mark-corresponding-synthetics"] = "1";
}

return from(decompileClass(className, jar.jar, options)).pipe(
tap(result => {
// Store DecompilationResult in in-memory cache
if (decompilationCache.size >= 75) {
const firstKey = decompilationCache.keys().next().value;
if (firstKey) decompilationCache.delete(firstKey);
}
decompilationCache.set(key, result);
})
);
return from(decompileClass(className, jar.jar));
}),
shareReplay({ bufferSize: 1, refCount: false })
);
}

export const currentSource = currentResult.pipe(
map(result => result.source)
);

export async function decompileClass(className: string, jar: Jar, options: Options): Promise<DecompileResult> {
console.log(`Decompiling class: '${className}'`);

const files = Object.keys(jar.entries);

if (!files.includes(className)) {
console.error(`Class not found in Minecraft jar: ${className}`);
return { className, source: `// Class not found: ${className}`, tokens: [], language: "java" };
}

export async function getClassBytecode(className: string, jar: Jar) {
try {
decompilerCounter.next(decompilerCounter.value + 1);

const tokens: Token[] = [];
const source = await decompile(className.replace(".class", ""), {
source: async (name: string) => {
const file = jar.entries[name + ".class"];
if (file) {
const arrayBuffer = await file.bytes();
return new Uint8Array(arrayBuffer);
}

console.error(`File not found in Minecraft jar: ${name}`);
return null;
},
resources: files.filter(f => f.endsWith('.class')).map(f => f.replace(".class", "")),
options,
tokenCollector: tokenCollector(tokens)
});

tokens.push(...generateImportTokens(source));
tokens.sort((a, b) => a.start - b.start);

return { className, source, tokens, language: "java" };
} catch (e) {
console.error(`Error during decompilation of class '${className}':`, e);
return { className, source: `// Error during decompilation: ${(e as Error).message}`, tokens: [], language: "java" };
return await worker.getClassBytecode(className, jar);
} finally {
decompilerCounter.next(decompilerCounter.value - 1);
}
}

function tokenCollector(tokens: Token[]): TokenCollector {
return {
start: function (content: string): void {
},
visitClass: function (start: number, length: number, declaration: boolean, name: string): void {
tokens.push({ type: "class", start, length, className: name, declaration });
},
visitField: function (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string): void {
tokens.push({ type: "field", start, length, className, declaration, name, descriptor });
},
visitMethod: function (start: number, length: number, declaration: boolean, className: string, name: string, descriptor: string): void {
tokens.push({ type: "method", start, length, className, declaration, name, descriptor });
},
visitParameter: function (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string): void {
tokens.push({ type: "parameter", start, length, className, declaration });
},
visitLocal: function (start: number, length: number, declaration: boolean, className: string, methodName: string, methodDescriptor: string, index: number, name: string): void {
tokens.push({ type: "local", start, length, className, declaration });
},
end: function (): void {
}
};
}

function generateImportTokens(source: string): Token[] {
const importTokens: Token[] = [];

const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm;

let match = null;
while ((match = importRegex.exec(source)) !== null) {
const importPath = match[1].replaceAll('.', '/');
if (importPath.endsWith('*')) {
continue;
}

const className = importPath.substring(importPath.lastIndexOf('/') + 1);

importTokens.push({
type: "class",
start: match.index + match[0].lastIndexOf(className),
length: importPath.length - importPath.lastIndexOf(className),
className: importPath,
declaration: false
});
}
return importTokens;
}

async function getClassBytecode(className: string, jar: Jar): Promise<DecompileResult> {
var classData = [];
const allClasses = Object.keys(jar.entries).filter(f => f.endsWith('.class')).sort();
const baseClassName = className.replace(".class", "");

if (!allClasses.includes(className)) {
console.error(`Class not found in Minecraft jar: ${className}`);
return { className, source: `// Class not found: ${className}`, tokens: [], language: "bytecode" };
}

export async function decompileClass(className: string, jar: Jar) {
try {
decompilerCounter.next(decompilerCounter.value + 1);

const data = await jar.entries[className].bytes();
classData.push(data.buffer);

for (const classFile of allClasses) {
if (!classFile.startsWith(baseClassName + "$")) {
continue;
}

const data = await jar.entries[classFile].bytes();
classData.push(data.buffer);
}

const bytecode = await getBytecode(classData);
return { className, source: bytecode, tokens: [], language: "bytecode" };
} catch (e) {
console.error(`Error during bytecode retrieval of class '${className}':`, e);
return { className, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" };
return await worker.decompileClass(className, jar);
} finally {
decompilerCounter.next(decompilerCounter.value - 1);
}
Expand Down
3 changes: 2 additions & 1 deletion src/logic/Diff.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { BehaviorSubject, combineLatest, from, map, Observable, switchMap, shareReplay } from "rxjs";
import { minecraftJar, minecraftJarPipeline, type MinecraftJar } from "./MinecraftApi";
import { currentResult, decompileResultPipeline, type DecompileResult } from "./Decompiler";
import { currentResult, decompileResultPipeline } from "./Decompiler";
import { calculatedLineChanges } from "./LineChanges";
import { diffLeftselectedMinecraftVersion, selectedMinecraftVersion } from "./State";
import type { DecompileResult } from "../workers/decompile/types";

export const hideUnchangedSizes = new BehaviorSubject<boolean>(false);

Expand Down
6 changes: 3 additions & 3 deletions src/logic/FindUsages.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BehaviorSubject, combineLatest, distinctUntilChanged, from, map, Observable, switchMap, throttleTime } from "rxjs";
import { jarIndex, type UsageKey, type UsageString } from "../workers/JarIndex";
import { openTab } from "./Tabs";
import type { DecompileResult } from "./Decompiler";
import type { Token } from "./Tokens";
import { usageQuery } from "./State";
import type { Token } from "./Tokens";
import type { DecompileResult } from "../workers/decompile/types";

export const useageResults = usageQuery
.pipe(
Expand Down Expand Up @@ -185,4 +185,4 @@ export function getNextJumpToken(decompileResult: DecompileResult): Token | unde
// Just return the declaration that supposedly contains the usage
console.log("Could not find token for", query);
return decompileResult.tokens[usageTokenIndex];
}
}
Loading