From 0378fe6a7309886ecd70bcb73c2cab39f3c41f31 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Sun, 15 Feb 2026 18:40:39 +0000 Subject: [PATCH 1/3] Format with oxfmt --- .github/copilot-instructions.md | 7 +- .github/workflows/build.yml | 3 +- .github/workflows/publish.yml | 2 +- .oxlintrc.json | 8 +- .vscode/extensions.json | 7 +- .vscode/launch.json | 2 +- .vscode/settings.json | 15 +- java/gradle/libs.versions.toml | 2 +- package-lock.json | 374 ++++++++++++ package.json | 7 +- playwright.config.ts | 60 +- src/declarations.d.ts | 2 +- src/index.css | 27 +- src/javadoc/Javadoc.ts | 115 ++-- src/javadoc/JavadocCmpletionProvider.ts | 195 +++--- src/javadoc/JavadocCodeExtensions.ts | 232 ++++---- src/javadoc/JavadocMarkdownEditor.tsx | 71 +-- src/javadoc/JavadocModal.tsx | 194 +++--- src/javadoc/api/JavadocApi.ts | 202 ++++--- src/javadoc/api/LoginModal.tsx | 86 ++- src/logic/Browser.ts | 12 +- src/logic/Decompiler.ts | 94 +-- src/logic/Diff.ts | 279 ++++----- src/logic/FindAllReferences.ts | 298 +++++----- src/logic/Inheritance.ts | 172 +++--- src/logic/JarFile.ts | 42 +- src/logic/Keybinds.ts | 16 +- src/logic/LineChanges.test.ts | 200 +++---- src/logic/LineChanges.ts | 157 ++--- src/logic/MinecraftApi.ts | 409 ++++++------- src/logic/Permalink.test.ts | 284 ++++----- src/logic/Permalink.ts | 206 ++++--- src/logic/Search.test.ts | 335 +++++------ src/logic/Search.ts | 112 ++-- src/logic/Settings.ts | 276 +++++---- src/logic/State.ts | 4 +- src/logic/Tabs.ts | 209 ++++--- src/logic/Tokens.ts | 44 +- src/logic/vf.ts | 41 +- src/main.tsx | 24 +- src/site.ts | 2 +- src/types/java-indexer.d.ts | 10 +- src/types/vf-runtime.d.ts | 10 +- src/ui/AboutModal.tsx | 115 ++-- src/ui/App.tsx | 186 +++--- src/ui/Code.tsx | 754 ++++++++++++------------ src/ui/CodeContextActions.ts | 232 ++++---- src/ui/CodeExtensions.ts | 443 +++++++------- src/ui/CodeHoverProvider.ts | 471 +++++++-------- src/ui/CodeUtils.ts | 66 +-- src/ui/FileList.tsx | 527 +++++++++-------- src/ui/FilepathHeader.tsx | 97 +-- src/ui/Header.tsx | 97 +-- src/ui/IndexProgressNotification.tsx | 51 +- src/ui/JarDecompilerModal.tsx | 244 ++++---- src/ui/Modals.tsx | 28 +- src/ui/ProgressModal.tsx | 24 +- src/ui/ReferenceResults.tsx | 140 ++--- src/ui/SearchResults.tsx | 46 +- src/ui/SettingsModal.tsx | 274 +++++---- src/ui/SideBar.tsx | 107 ++-- src/ui/StructureModal.tsx | 40 +- src/ui/StructureView.tsx | 190 +++--- src/ui/TabsComponent.tsx | 530 ++++++++--------- src/ui/diff/DiffCode.tsx | 123 ++-- src/ui/diff/DiffFileList.tsx | 414 +++++++------ src/ui/diff/DiffVersionSelection.tsx | 68 ++- src/ui/diff/DiffView.tsx | 46 +- src/ui/inheritance/InheritanceGraph.tsx | 406 ++++++------- src/ui/inheritance/InheritanceModal.tsx | 75 ++- src/ui/inheritance/InheritanceTree.tsx | 176 +++--- src/ui/intellij-icons/index.tsx | 104 ++-- src/utils/Classfile.ts | 8 +- src/utils/Jar.ts | 142 ++--- src/utils/JavaBytecode.ts | 140 +++-- src/utils/UseObservable.ts | 18 +- src/workers/JarIndex.ts | 290 ++++----- src/workers/JarIndexWorker.ts | 86 +-- src/workers/decompile/client.ts | 248 ++++---- src/workers/decompile/types.ts | 68 ++- src/workers/decompile/worker.ts | 415 +++++++------ tests/bytecode.spec.ts | 26 +- tests/decompile.spec.ts | 52 +- tests/diff.spec.ts | 82 +-- tests/file-list.spec.ts | 72 +-- tests/find-usages.spec.ts | 30 +- tests/goto-definition.spec.ts | 31 +- tests/inheritance.spec.ts | 52 +- tests/permalink.spec.ts | 88 +-- tests/tabs.spec.ts | 120 ++-- tests/test-utils.ts | 139 ++--- tests/version-switching.spec.ts | 74 +-- tsconfig.json | 5 +- vite.config.ts | 34 +- wrangler.jsonc | 6 +- 95 files changed, 7030 insertions(+), 6117 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 546add2..0762ad8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,5 @@ # How to write code for this repository + - Code should document itself as much as possible. Only use comments where the code is not self-explanatory. - Write as minimal code as possible to achieve the desired functionality. - Embrace TypeScript's type system to ensure type safety and reduce runtime errors, create specific types and interfaces as needed. @@ -7,7 +8,7 @@ - Ensure proper error handling and input validation throughout the codebase. - RXJS should be used for state management and asynchronous operations where appropriate. - # Testing -- Unit tests can be written using vitest in "*.test.ts" files alongside the implementation files. -- Intergration tests can writen using Playwright in the "tests" folder. \ No newline at end of file + +- Unit tests can be written using vitest in "\*.test.ts" files alongside the implementation files. +- Intergration tests can writen using Playwright in the "tests" folder. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b70e2e6..55a708b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,7 @@ jobs: - run: npm run build - run: npm run test - run: npm run lint + - run: npm run format - uses: actions/upload-artifact@v4 with: name: dist @@ -57,4 +58,4 @@ jobs: if: ${{ !cancelled() }} with: name: playwright-report-${{ matrix.os }}-${{ matrix.browser }} - path: playwright-report/ \ No newline at end of file + path: playwright-report/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9944f8c..c68c55f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,4 +27,4 @@ jobs: - run: npx wrangler deploy env: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} \ No newline at end of file + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.oxlintrc.json b/.oxlintrc.json index 7d0389b..cf79429 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,9 +1,7 @@ { "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": [ - "react" - ], + "plugins": ["react"], "rules": { - "eslint/no-unused-vars": "allow", + "eslint/no-unused-vars": "allow" } -} \ No newline at end of file +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 9f1e4b8..0d4efac 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,3 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint", - "oxc.oxc-vscode" - ] -} \ No newline at end of file + "recommendations": ["dbaeumer.vscode-eslint", "oxc.oxc-vscode"] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index f437f0c..d8f247e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,4 +11,4 @@ "trace": false } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 749f2f4..55bde68 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,12 +1,9 @@ { - "[typescript]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "vscode.typescript-language-features" - }, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.formatOnSave": true, + "oxc.typeAware": true, "[typescriptreact]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "oxc.oxc-vscode" }, - "typescript.format.semicolons": "insert", - "oxc.typeAware": true -} \ No newline at end of file + "oxc.enable.oxfmt": true +} diff --git a/java/gradle/libs.versions.toml b/java/gradle/libs.versions.toml index 63de8b4..2a1c055 100644 --- a/java/gradle/libs.versions.toml +++ b/java/gradle/libs.versions.toml @@ -9,4 +9,4 @@ teavm-core = { group = "org.teavm", name = "teavm-core", version.ref = "teavm" } teavm-jso = { group = "org.teavm", name = "teavm-jso-apis", version.ref = "teavm" } [plugins] -teavm = { id = "org.teavm", version.ref = "teavm" } \ No newline at end of file +teavm = { id = "org.teavm", version.ref = "teavm" } diff --git a/package-lock.json b/package-lock.json index b09bc95..16cd31a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "globals": "^17.3.0", + "oxfmt": "^0.32.0", "oxlint": "^1.47.0", "oxlint-tsgolint": "^0.13.0", "typescript": "~5.9.3", @@ -1593,6 +1594,329 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.32.0.tgz", + "integrity": "sha512-DpVyuVzgLH6/MvuB/YD3vXO9CN/o9EdRpA0zXwe/tagP6yfVSFkFWkPqTROdqp0mlzLH5Yl+/m+hOrcM601EbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.32.0.tgz", + "integrity": "sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.32.0.tgz", + "integrity": "sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.32.0.tgz", + "integrity": "sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.32.0.tgz", + "integrity": "sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.32.0.tgz", + "integrity": "sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.32.0.tgz", + "integrity": "sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.32.0.tgz", + "integrity": "sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.32.0.tgz", + "integrity": "sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.32.0.tgz", + "integrity": "sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.32.0.tgz", + "integrity": "sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.32.0.tgz", + "integrity": "sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.32.0.tgz", + "integrity": "sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.32.0.tgz", + "integrity": "sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.32.0.tgz", + "integrity": "sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.32.0.tgz", + "integrity": "sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.32.0.tgz", + "integrity": "sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.32.0.tgz", + "integrity": "sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.32.0.tgz", + "integrity": "sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@oxlint-tsgolint/darwin-arm64": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-0.13.0.tgz", @@ -4883,6 +5207,46 @@ ], "license": "MIT" }, + "node_modules/oxfmt": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.32.0.tgz", + "integrity": "sha512-KArQhGzt/Y8M1eSAX98Y8DLtGYYDQhkR55THUPY5VNcpFQ+9nRZkL3ULXhagHMD2hIvjy8JSeEQEP5/yYJSrLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.32.0", + "@oxfmt/binding-android-arm64": "0.32.0", + "@oxfmt/binding-darwin-arm64": "0.32.0", + "@oxfmt/binding-darwin-x64": "0.32.0", + "@oxfmt/binding-freebsd-x64": "0.32.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.32.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.32.0", + "@oxfmt/binding-linux-arm64-gnu": "0.32.0", + "@oxfmt/binding-linux-arm64-musl": "0.32.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.32.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.32.0", + "@oxfmt/binding-linux-riscv64-musl": "0.32.0", + "@oxfmt/binding-linux-s390x-gnu": "0.32.0", + "@oxfmt/binding-linux-x64-gnu": "0.32.0", + "@oxfmt/binding-linux-x64-musl": "0.32.0", + "@oxfmt/binding-openharmony-arm64": "0.32.0", + "@oxfmt/binding-win32-arm64-msvc": "0.32.0", + "@oxfmt/binding-win32-ia32-msvc": "0.32.0", + "@oxfmt/binding-win32-x64-msvc": "0.32.0" + } + }, "node_modules/oxlint": { "version": "1.47.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.47.0.tgz", @@ -5429,6 +5793,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", diff --git a/package.json b/package.json index 865c23d..acbffd5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mcsrc", - "private": true, "version": "0.0.0", + "private": true, "type": "module", "scripts": { "dev": "vite", @@ -14,7 +14,9 @@ "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", "lint": "oxlint --type-aware --deny-warnings", - "lint:fix": "oxlint --fix --type-aware --deny-warnings" + "lint:fix": "oxlint --fix --type-aware --deny-warnings", + "format": "oxfmt --check", + "format:fix": "oxfmt" }, "dependencies": { "@katana-project/zip": "^0.7.1", @@ -38,6 +40,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.4", "globals": "^17.3.0", + "oxfmt": "^0.32.0", "oxlint": "^1.47.0", "oxlint-tsgolint": "^0.13.0", "typescript": "~5.9.3", diff --git a/playwright.config.ts b/playwright.config.ts index 7505bab..048b8f9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,37 +1,37 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; // https://playwright.dev/docs/test-configuration export default defineConfig({ - testDir: './tests', - timeout: process.env.CI ? 300000 : 0, // 5 minutes - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - reporter: 'html', + testDir: "./tests", + timeout: process.env.CI ? 300000 : 0, // 5 minutes + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: "html", - use: { - baseURL: 'http://localhost:4173', - trace: 'on-first-retry', - }, - - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - ], + use: { + baseURL: "http://localhost:4173", + trace: "on-first-retry", + }, - webServer: { - command: process.env.CI ? 'npm run preview' : 'npm run build && npm run preview', - url: 'http://localhost:4173', - reuseExistingServer: !process.env.CI, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + webServer: { + command: process.env.CI ? "npm run preview" : "npm run build && npm run preview", + url: "http://localhost:4173", + reuseExistingServer: !process.env.CI, + }, }); diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 0dc067d..41eef4e 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -1,3 +1,3 @@ declare module "*/java.wasm-runtime.js" { - export async function load(src: string); + export async function load(src: string); } diff --git a/src/index.css b/src/index.css index 0490eef..c8dc566 100644 --- a/src/index.css +++ b/src/index.css @@ -3,11 +3,16 @@ body { } /* Prevent scrolling the body */ -html, body {margin: 0; height: 100%; overflow: hidden} +html, +body { + margin: 0; + height: 100%; + overflow: hidden; +} /* Webkit adds scrollbars as element selectors rather than allowing the element to control their style */ .webkit-scrollbar-hide::-webkit-scrollbar { - display: none + display: none; } .class-token-decoration { @@ -79,27 +84,27 @@ html, body {margin: 0; height: 100%; overflow: hidden} /* Stop icon & text in file tree from wrapping */ .ant-tree-node-content-wrapper { - display: flex; - flex-wrap: nowrap; - padding-inline: 0 !important; + display: flex; + flex-wrap: nowrap; + padding-inline: 0 !important; } /* Remove extraneous gap in file tree component */ .ant-tree-switcher { - margin-inline-end: 0 !important; + margin-inline-end: 0 !important; } /* Fix file tree icon centering */ .ant-tree-iconEle > .anticon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); } /* Stop text in file tree from wrapping */ .ant-tree-title { - white-space: nowrap; + white-space: nowrap; } /* Don't have text overflow in the structure dialog */ diff --git a/src/javadoc/Javadoc.ts b/src/javadoc/Javadoc.ts index d173bec..b44c85a 100644 --- a/src/javadoc/Javadoc.ts +++ b/src/javadoc/Javadoc.ts @@ -6,82 +6,89 @@ import { selectedMinecraftVersion } from "../logic/State"; export type JavadocString = string; export interface JavadocData { - classes: Record; - fields: Record; - }>; + classes: Record< + string, + { + javadoc: JavadocString | null; + methods: Record; + fields: Record; + } + >; } export const javadocData = new BehaviorSubject({ - classes: {} + classes: {}, }); // Holds the currently active token for which Javadoc is being edited export const activeJavadocToken = new BehaviorSubject(null); export function setTokenJavadoc(token: Token, javadoc: JavadocString | undefined) { - const data = javadocData.getValue(); - const classEntry = data.classes[token.className] || { javadoc: null, methods: {}, fields: {} }; + const data = javadocData.getValue(); + const classEntry = data.classes[token.className] || { javadoc: null, methods: {}, fields: {} }; - if (token.type === 'class') { - classEntry.javadoc = javadoc ?? null; - } else if (token.type === 'method') { - if (javadoc === undefined) { - delete classEntry.methods[token.name + token.descriptor]; - } else { - classEntry.methods[token.name + token.descriptor] = javadoc; - } - } else if (token.type === 'field') { - if (javadoc === undefined) { - delete classEntry.fields[token.name + token.descriptor]; - } else { - classEntry.fields[token.name + token.descriptor] = javadoc; - } + if (token.type === "class") { + classEntry.javadoc = javadoc ?? null; + } else if (token.type === "method") { + if (javadoc === undefined) { + delete classEntry.methods[token.name + token.descriptor]; + } else { + classEntry.methods[token.name + token.descriptor] = javadoc; + } + } else if (token.type === "field") { + if (javadoc === undefined) { + delete classEntry.fields[token.name + token.descriptor]; + } else { + classEntry.fields[token.name + token.descriptor] = javadoc; } + } - data.classes[token.className] = classEntry; - javadocData.next(data); - console.log("Updated Javadoc data:", data); + data.classes[token.className] = classEntry; + javadocData.next(data); + console.log("Updated Javadoc data:", data); } // Refreshes the Javadoc data for a specific class from the server export async function refreshJavadocDataForClass(className: string) { - const minecraftVersion = selectedMinecraftVersion.value; - if (!minecraftVersion) { - throw new Error("No Minecraft version selected"); - } + const minecraftVersion = selectedMinecraftVersion.value; + if (!minecraftVersion) { + throw new Error("No Minecraft version selected"); + } - const data = await javadocApi.getJavadoc(minecraftVersion, className); + const data = await javadocApi.getJavadoc(minecraftVersion, className); - for (const [key, entry] of Object.entries(data.data)) { - const classEntry = javadocData.getValue().classes[key] || { javadoc: null, methods: {}, fields: {} }; - classEntry.javadoc = entry.value || null; - classEntry.methods = entry.methods || {}; - classEntry.fields = entry.fields || {}; - const nextData = { ...javadocData.getValue() }; - nextData.classes[key] = classEntry; - javadocData.next(nextData); - } + for (const [key, entry] of Object.entries(data.data)) { + const classEntry = javadocData.getValue().classes[key] || { + javadoc: null, + methods: {}, + fields: {}, + }; + classEntry.javadoc = entry.value || null; + classEntry.methods = entry.methods || {}; + classEntry.fields = entry.fields || {}; + const nextData = { ...javadocData.getValue() }; + nextData.classes[key] = classEntry; + javadocData.next(nextData); + } } export function observeJavadocForToken(token: Token): Observable { - return javadocData.pipe( - map(data => { - return getJavadocForToken(token, data); - }) - ); + return javadocData.pipe( + map((data) => { + return getJavadocForToken(token, data); + }), + ); } export function getJavadocForToken(token: Token, javadoc: JavadocData): JavadocString | null { - switch (token.type) { - case 'class': - return javadoc.classes[token.className]?.javadoc || null; - case 'method': - return javadoc.classes[token.className]?.methods[token.name + token.descriptor] || null; - case 'field': - return javadoc.classes[token.className]?.fields[token.name + token.descriptor] || null; - } + switch (token.type) { + case "class": + return javadoc.classes[token.className]?.javadoc || null; + case "method": + return javadoc.classes[token.className]?.methods[token.name + token.descriptor] || null; + case "field": + return javadoc.classes[token.className]?.fields[token.name + token.descriptor] || null; + } - return null; -} \ No newline at end of file + return null; +} diff --git a/src/javadoc/JavadocCmpletionProvider.ts b/src/javadoc/JavadocCmpletionProvider.ts index 92a77ed..a32cff2 100644 --- a/src/javadoc/JavadocCmpletionProvider.ts +++ b/src/javadoc/JavadocCmpletionProvider.ts @@ -1,108 +1,117 @@ -import { editor, languages, Position, Token, type CancellationToken } from 'monaco-editor'; -import type { MemberToken } from '../logic/Tokens'; -import type { DecompileResult } from '../workers/decompile/types'; +import { editor, languages, Position, Token, type CancellationToken } from "monaco-editor"; +import type { MemberToken } from "../logic/Tokens"; +import type { DecompileResult } from "../workers/decompile/types"; export class JavdocCompletionProvider implements languages.CompletionItemProvider { - readonly decompileResult: DecompileResult; - - constructor(decompileResult: DecompileResult) { - this.decompileResult = decompileResult; + readonly decompileResult: DecompileResult; + + constructor(decompileResult: DecompileResult) { + this.decompileResult = decompileResult; + } + + triggerCharacters: string[] = ["[", "#"]; + + provideCompletionItems( + model: editor.ITextModel, + position: Position, + context: languages.CompletionContext, + token: CancellationToken, + ): languages.ProviderResult { + if (!this.isCreatingLink(model, position)) { + return { suggestions: [] }; } - triggerCharacters: string[] = ['[', '#']; - - provideCompletionItems(model: editor.ITextModel, position: Position, context: languages.CompletionContext, token: CancellationToken): languages.ProviderResult { - if (!this.isCreatingLink(model, position)) { - return { suggestions: [] }; - } - - const range = { - startLineNumber: position.lineNumber, - startColumn: position.column, - endLineNumber: position.lineNumber, - endColumn: position.column + const range = { + startLineNumber: position.lineNumber, + startColumn: position.column, + endLineNumber: position.lineNumber, + endColumn: position.column, + }; + + if (this.isCreatingMemberLink(model, position)) { + const suggestions: languages.CompletionItem[] = this.getMembers().map((token) => { + return { + label: token.name, + kind: + token.type === "method" + ? languages.CompletionItemKind.Method + : languages.CompletionItemKind.Field, + insertText: token.name, + range, }; + }); - if (this.isCreatingMemberLink(model, position)) { - const suggestions: languages.CompletionItem[] = this.getMembers().map(token => { - return { - label: token.name, - kind: token.type === 'method' ? languages.CompletionItemKind.Method : languages.CompletionItemKind.Field, - insertText: token.name, - range - }; - }); - - return { suggestions }; - } - - const imports = this.getImportedClasses(); - - const suggestions: languages.CompletionItem[] = imports.map(importPath => { - const className = importPath.split('.').pop() || importPath; - - return { - label: className, - kind: languages.CompletionItemKind.Reference, - insertText: className, - detail: importPath, - range - }; - }); - - return { suggestions }; - } - - isCreatingLink(model: editor.ITextModel, position: Position): boolean { // Check if cursor is within [] characters - const lineContent = model.getLineContent(position.lineNumber); - const textBeforeCursor = lineContent.substring(0, position.column - 1); - const textAfterCursor = lineContent.substring(position.column - 1); - - // Find the last '[' before cursor and first ']' after cursor - const lastOpenBracket = textBeforeCursor.lastIndexOf('['); - const firstCloseBracket = textAfterCursor.indexOf(']'); - - // Only provide completions if we're inside brackets - return lastOpenBracket !== -1 && firstCloseBracket !== -1; + return { suggestions }; } - isCreatingMemberLink(model: editor.ITextModel, position: Position): boolean { - const lineContent = model.getLineContent(position.lineNumber); - const textBeforeCursor = lineContent.substring(0, position.column - 1); - const lastOpenBracket = textBeforeCursor.lastIndexOf('['); - const textAfterBracket = textBeforeCursor.substring(lastOpenBracket + 1); - return textAfterBracket.startsWith('#'); + const imports = this.getImportedClasses(); + + const suggestions: languages.CompletionItem[] = imports.map((importPath) => { + const className = importPath.split(".").pop() || importPath; + + return { + label: className, + kind: languages.CompletionItemKind.Reference, + insertText: className, + detail: importPath, + range, + }; + }); + + return { suggestions }; + } + + isCreatingLink(model: editor.ITextModel, position: Position): boolean { + // Check if cursor is within [] characters + const lineContent = model.getLineContent(position.lineNumber); + const textBeforeCursor = lineContent.substring(0, position.column - 1); + const textAfterCursor = lineContent.substring(position.column - 1); + + // Find the last '[' before cursor and first ']' after cursor + const lastOpenBracket = textBeforeCursor.lastIndexOf("["); + const firstCloseBracket = textAfterCursor.indexOf("]"); + + // Only provide completions if we're inside brackets + return lastOpenBracket !== -1 && firstCloseBracket !== -1; + } + + isCreatingMemberLink(model: editor.ITextModel, position: Position): boolean { + const lineContent = model.getLineContent(position.lineNumber); + const textBeforeCursor = lineContent.substring(0, position.column - 1); + const lastOpenBracket = textBeforeCursor.lastIndexOf("["); + const textAfterBracket = textBeforeCursor.substring(lastOpenBracket + 1); + return textAfterBracket.startsWith("#"); + } + + getImportedClasses(): string[] { + const source = this.decompileResult.source; + const importedClasses: string[] = []; + + const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm; + + let match = null; + while ((match = importRegex.exec(source)) !== null) { + const importPath = match[1]; + if (importPath.endsWith("*")) { + continue; + } + + importedClasses.push(importPath); } - getImportedClasses(): string[] { - const source = this.decompileResult.source; - const importedClasses: string[] = []; - - const importRegex = /^\s*import\s+(?!static\b)([^\s;]+)\s*;/gm; - - let match = null; - while ((match = importRegex.exec(source)) !== null) { - const importPath = match[1]; - if (importPath.endsWith('*')) { - continue; - } + return importedClasses; + } - importedClasses.push(importPath); - } + getMembers(): MemberToken[] { + const tokens = this.decompileResult.tokens; + const members: MemberToken[] = []; - return importedClasses; + for (const token of tokens) { + if (token.declaration && (token.type == "method" || token.type == "field")) { + members.push(token); + } } - getMembers(): MemberToken[] { - const tokens = this.decompileResult.tokens; - const members: MemberToken[] = []; - - for (const token of tokens) { - if (token.declaration && (token.type == 'method' || token.type == 'field')) { - members.push(token); - } - } - - return members; - } + return members; + } } diff --git a/src/javadoc/JavadocCodeExtensions.ts b/src/javadoc/JavadocCodeExtensions.ts index 534b2a0..51447f3 100644 --- a/src/javadoc/JavadocCodeExtensions.ts +++ b/src/javadoc/JavadocCodeExtensions.ts @@ -1,126 +1,138 @@ -import { - editor, - languages, - type CancellationToken, - type IDisposable, -} from "monaco-editor"; +import { editor, languages, type CancellationToken, type IDisposable } from "monaco-editor"; import { getTokenLocation, type Token, type TokenLocation } from "../logic/Tokens"; -import { activeJavadocToken, getJavadocForToken, javadocData, refreshJavadocDataForClass, type JavadocData, type JavadocString } from "./Javadoc"; +import { + activeJavadocToken, + getJavadocForToken, + javadocData, + refreshJavadocDataForClass, + type JavadocData, + type JavadocString, +} from "./Javadoc"; import type { DecompileResult } from "../workers/decompile/types"; type monaco = typeof import("monaco-editor"); -const EDIT_JAVADOC_COMMAND_ID = 'editor.action.editJavadoc'; - -export function applyJavadocCodeExtensions(monaco: monaco, editor: editor.IStandaloneCodeEditor, decompile: DecompileResult): IDisposable { - const viewZoneIds: string[] = []; - const javadocDataSub = javadocData.subscribe((javadoc) => { - editor.changeViewZones((accessor) => { - // Remove any existing zones - viewZoneIds.forEach(id => accessor.removeZone(id)); - viewZoneIds.length = 0; - - decompile.tokens - .filter(token => token.declaration) - .forEach(token => { - const mdValue = getJavadocForToken(token, javadoc); - if (mdValue == null) { - return; - } - - const domNode = document.createElement('div'); - domNode.innerHTML = `${formatMarkdownAsHtml(mdValue, token)}`; - - const location = getTokenLocation(decompile, token); - const zoneId = accessor.addZone({ - afterLineNumber: location.line - 1, - heightInPx: cacluateHeightInPx(domNode), - domNode: domNode - }); - - viewZoneIds.push(zoneId); - }); +const EDIT_JAVADOC_COMMAND_ID = "editor.action.editJavadoc"; + +export function applyJavadocCodeExtensions( + monaco: monaco, + editor: editor.IStandaloneCodeEditor, + decompile: DecompileResult, +): IDisposable { + const viewZoneIds: string[] = []; + const javadocDataSub = javadocData.subscribe((javadoc) => { + editor.changeViewZones((accessor) => { + // Remove any existing zones + viewZoneIds.forEach((id) => accessor.removeZone(id)); + viewZoneIds.length = 0; + + decompile.tokens + .filter((token) => token.declaration) + .forEach((token) => { + const mdValue = getJavadocForToken(token, javadoc); + if (mdValue == null) { + return; + } + + const domNode = document.createElement("div"); + domNode.innerHTML = `${formatMarkdownAsHtml(mdValue, token)}`; + + const location = getTokenLocation(decompile, token); + const zoneId = accessor.addZone({ + afterLineNumber: location.line - 1, + heightInPx: cacluateHeightInPx(domNode), + domNode: domNode, + }); + + viewZoneIds.push(zoneId); }); }); - - const codeLense = monaco.languages.registerCodeLensProvider("java", { - provideCodeLenses: function(model: editor.ITextModel, token: CancellationToken): languages.ProviderResult { - const lenses: languages.CodeLens[] = []; - - for (const token of decompile.tokens) { - if (!token.declaration || token.type == 'parameter' || token.type == 'local') { - continue; - } - - const location = getTokenLocation(decompile, token); - lenses.push({ - range: { - startLineNumber: location.line, - startColumn: 0, - endLineNumber: location.line, - endColumn: 0, - }, - command: { - id: EDIT_JAVADOC_COMMAND_ID, - title: "Edit Javadoc", - arguments: [token] - } - }); - } - - return { - lenses, - dispose: () => { } - }; - } - }); - - - const editJavadocCommand = monaco.editor.addEditorAction({ - id: EDIT_JAVADOC_COMMAND_ID, - label: 'Edit Javadoc', - run: function(editor, ...args) { - const token = args[0] as Token; - activeJavadocToken.next(token); + }); + + const codeLense = monaco.languages.registerCodeLensProvider("java", { + provideCodeLenses: function ( + model: editor.ITextModel, + token: CancellationToken, + ): languages.ProviderResult { + const lenses: languages.CodeLens[] = []; + + for (const token of decompile.tokens) { + if (!token.declaration || token.type == "parameter" || token.type == "local") { + continue; } - }); - refreshJavadocDataForClass(decompile.className.replace(".class", "")).catch(err => { - console.error("Failed to refresh Javadoc data for class:", err); - }); - - return { - dispose() { - editJavadocCommand.dispose(); - codeLense.dispose(); - - javadocDataSub.unsubscribe(); - editor.changeViewZones((accessor) => { - viewZoneIds.forEach(id => accessor.removeZone(id)); - }); - } - }; + const location = getTokenLocation(decompile, token); + lenses.push({ + range: { + startLineNumber: location.line, + startColumn: 0, + endLineNumber: location.line, + endColumn: 0, + }, + command: { + id: EDIT_JAVADOC_COMMAND_ID, + title: "Edit Javadoc", + arguments: [token], + }, + }); + } + + return { + lenses, + dispose: () => {}, + }; + }, + }); + + const editJavadocCommand = monaco.editor.addEditorAction({ + id: EDIT_JAVADOC_COMMAND_ID, + label: "Edit Javadoc", + run: function (editor, ...args) { + const token = args[0] as Token; + activeJavadocToken.next(token); + }, + }); + + refreshJavadocDataForClass(decompile.className.replace(".class", "")).catch((err) => { + console.error("Failed to refresh Javadoc data for class:", err); + }); + + return { + dispose() { + editJavadocCommand.dispose(); + codeLense.dispose(); + + javadocDataSub.unsubscribe(); + editor.changeViewZones((accessor) => { + viewZoneIds.forEach((id) => accessor.removeZone(id)); + }); + }, + }; } function formatMarkdownAsHtml(md: string, token: Token): string { - // TODO maybe use a proper markdown parser/renderer here - - const nestingLevel = (token.className.match(/\$/g) || []).length + (token.type == 'method' || token.type == 'field' ? 1 : 0); - const depth = nestingLevel * 6; - - const indent = " ".repeat(depth) + "/// "; - return md.split("\n").map(line => indent + line).join("
"); + // TODO maybe use a proper markdown parser/renderer here + + const nestingLevel = + (token.className.match(/\$/g) || []).length + + (token.type == "method" || token.type == "field" ? 1 : 0); + const depth = nestingLevel * 6; + + const indent = " ".repeat(depth) + "/// "; + return md + .split("\n") + .map((line) => indent + line) + .join("
"); } - function cacluateHeightInPx(domNode: HTMLDivElement): number { - domNode.style.position = 'absolute'; - domNode.style.visibility = 'hidden'; - document.body.appendChild(domNode); - const heightInPx = domNode.offsetHeight * 1.2; // Magic number seems to fix it - document.body.removeChild(domNode); - domNode.style.position = ''; - domNode.style.visibility = ''; - - return heightInPx; + domNode.style.position = "absolute"; + domNode.style.visibility = "hidden"; + document.body.appendChild(domNode); + const heightInPx = domNode.offsetHeight * 1.2; // Magic number seems to fix it + document.body.removeChild(domNode); + domNode.style.position = ""; + domNode.style.visibility = ""; + + return heightInPx; } diff --git a/src/javadoc/JavadocMarkdownEditor.tsx b/src/javadoc/JavadocMarkdownEditor.tsx index d324642..2a15c70 100644 --- a/src/javadoc/JavadocMarkdownEditor.tsx +++ b/src/javadoc/JavadocMarkdownEditor.tsx @@ -6,45 +6,48 @@ import type { editor } from "monaco-editor"; import { JavdocCompletionProvider } from "./JavadocCmpletionProvider"; const JavadocMarkdownEditor = ({ - value, - onChange + value, + onChange, }: { - value: string; - onChange: (newValue: string | undefined) => void; + value: string; + onChange: (newValue: string | undefined) => void; }) => { - const monaco = useMonaco(); - const decompileResult = useObservable(currentResult); - const editorRef = useRef(null); + const monaco = useMonaco(); + const decompileResult = useObservable(currentResult); + const editorRef = useRef(null); - useEffect(() => { - if (!monaco || !decompileResult) return; + useEffect(() => { + if (!monaco || !decompileResult) return; - const completionItemProvider = monaco.languages.registerCompletionItemProvider('markdown', new JavdocCompletionProvider(decompileResult)); + const completionItemProvider = monaco.languages.registerCompletionItemProvider( + "markdown", + new JavdocCompletionProvider(decompileResult), + ); - return () => { - completionItemProvider.dispose(); - }; - }, [monaco, decompileResult]); + return () => { + completionItemProvider.dispose(); + }; + }, [monaco, decompileResult]); - return ( - { - editorRef.current = codeEditor; - }} - /> - ); + return ( + { + editorRef.current = codeEditor; + }} + /> + ); }; -export default JavadocMarkdownEditor; \ No newline at end of file +export default JavadocMarkdownEditor; diff --git a/src/javadoc/JavadocModal.tsx b/src/javadoc/JavadocModal.tsx index ac48ae3..d683994 100644 --- a/src/javadoc/JavadocModal.tsx +++ b/src/javadoc/JavadocModal.tsx @@ -8,109 +8,129 @@ import { useMemo, useState } from "react"; import { javadocApi, type UpdateTarget } from "./api/JavadocApi"; import { selectedMinecraftVersion } from "../logic/State"; -const ModalBody = ({ token, onValueChange }: { token: Token; onValueChange: (value: string | undefined) => void; }) => { - const initialValue = useMemo(() => getJavadocForToken(token, javadocData.value) || "", [token]); +const ModalBody = ({ + token, + onValueChange, +}: { + token: Token; + onValueChange: (value: string | undefined) => void; +}) => { + const initialValue = useMemo(() => getJavadocForToken(token, javadocData.value) || "", [token]); - return ( -
-
-
Type: {token.type}
-
Class: {token.className}
- {token.type === 'field' || token.type === 'method' ? ( - <> -
Name: {token.name}
-
Descriptor: {token.descriptor}
- - ) : null} + return ( +
+
+
+ Type: {token.type} +
+
+ Class: {token.className} +
+ {token.type === "field" || token.type === "method" ? ( + <> +
+ Name: {token.name}
-
- +
+ Descriptor: {token.descriptor}
-
- ); + + ) : null} +
+
+ +
+
+ ); }; const JavadocModal = () => { - if (!IS_JAVADOC_EDITOR) { - return (<>); - } + if (!IS_JAVADOC_EDITOR) { + return <>; + } - const token = useObservable(activeJavadocToken); - const minecraftVersion = useObservable(selectedMinecraftVersion); - const [currentValue, setCurrentValue] = useState(); - const [loading, setLoading] = useState(false); + const token = useObservable(activeJavadocToken); + const minecraftVersion = useObservable(selectedMinecraftVersion); + const [currentValue, setCurrentValue] = useState(); + const [loading, setLoading] = useState(false); - const [messageApi, contextHolder] = message.useMessage(); + const [messageApi, contextHolder] = message.useMessage(); - const handleSave = async () => { - if (!token) { - messageApi.error("No token selected."); - return; - } + const handleSave = async () => { + if (!token) { + messageApi.error("No token selected."); + return; + } - if (!minecraftVersion) { - messageApi.error("No Minecraft version selected."); - return; - } + if (!minecraftVersion) { + messageApi.error("No Minecraft version selected."); + return; + } - var target: UpdateTarget | null = null; - if (token.type == 'method' || token.type == 'field') { - target = { - type: token.type, - name: token.name, - descriptor: token.descriptor - }; - } + var target: UpdateTarget | null = null; + if (token.type == "method" || token.type == "field") { + target = { + type: token.type, + name: token.name, + descriptor: token.descriptor, + }; + } - setLoading(true); - try { - await javadocApi.updateJavadoc(minecraftVersion, { - className: token.className, - target, - documentation: currentValue || "" - }); + setLoading(true); + try { + await javadocApi.updateJavadoc(minecraftVersion, { + className: token.className, + target, + documentation: currentValue || "", + }); - messageApi.success("Javadoc saved successfully."); + messageApi.success("Javadoc saved successfully."); - // Update the local in-memory Javadoc data - setTokenJavadoc(token, currentValue); + // Update the local in-memory Javadoc data + setTokenJavadoc(token, currentValue); - activeJavadocToken.next(null); - } catch (error) { - messageApi.error("Failed to save javadoc."); - console.error("Error saving javadoc:", error); - } finally { - setLoading(false); - } - }; + activeJavadocToken.next(null); + } catch (error) { + messageApi.error("Failed to save javadoc."); + console.error("Error saving javadoc:", error); + } finally { + setLoading(false); + } + }; - const handleCancel = () => { - activeJavadocToken.next(null); - }; + const handleCancel = () => { + activeJavadocToken.next(null); + }; - return ( - - - -
- } - width={750} - > - {token && } - - ); + return ( + + + +
+ } + width={750} + > + {token && } + + ); }; -export default JavadocModal; \ No newline at end of file +export default JavadocModal; diff --git a/src/javadoc/api/JavadocApi.ts b/src/javadoc/api/JavadocApi.ts index c10894e..043b526 100644 --- a/src/javadoc/api/JavadocApi.ts +++ b/src/javadoc/api/JavadocApi.ts @@ -2,138 +2,136 @@ import { BehaviorSubject, map } from "rxjs"; import { IS_JAVADOC_EDITOR } from "../../site"; class JavadocApi { - // The current access token, or null if not authenticated - accessToken = new BehaviorSubject(null); + // The current access token, or null if not authenticated + accessToken = new BehaviorSubject(null); - needsToLogin = this.accessToken.pipe( - map(token => token == null) - ); + needsToLogin = this.accessToken.pipe(map((token) => token == null)); - constructor() { - this.refreshAccessToken().catch((e) => { - // Ignore errors on initial load - }); - } - - async getGithubLoginUrl(): Promise { - const response = await fetch('/v1/auth/github'); + constructor() { + this.refreshAccessToken().catch((e) => { + // Ignore errors on initial load + }); + } - if (!response.ok) { - throw new Error('Failed to get GitHub login URL'); - } + async getGithubLoginUrl(): Promise { + const response = await fetch("/v1/auth/github"); - const data = await response.json(); - return data.url; + if (!response.ok) { + throw new Error("Failed to get GitHub login URL"); } - async checkStatus(): Promise { - const response = await this.fetchWithAuth('/v1/auth/check'); - return response.status == 200; - } + const data = await response.json(); + return data.url; + } - async getJavadoc(version: string, className: string): Promise { - const requestBody = { - className - }; + async checkStatus(): Promise { + const response = await this.fetchWithAuth("/v1/auth/check"); + return response.status == 200; + } - const response = await this.fetchWithAuth(`/v1/javadoc/${encodeURIComponent(version)}`, { - method: 'POST', - body: JSON.stringify(requestBody) - }); + async getJavadoc(version: string, className: string): Promise { + const requestBody = { + className, + }; - if (response.status === 404) { - // No Javadoc found for this class - return { data: {} }; - } + const response = await this.fetchWithAuth(`/v1/javadoc/${encodeURIComponent(version)}`, { + method: "POST", + body: JSON.stringify(requestBody), + }); - if (!response.ok) { - throw new Error('Failed to get Javadoc'); - } + if (response.status === 404) { + // No Javadoc found for this class + return { data: {} }; + } - return await response.json() as JavadocResponse; + if (!response.ok) { + throw new Error("Failed to get Javadoc"); } - async updateJavadoc(version: string, update: UpdateJavadocRequest): Promise { - const response = await this.fetchWithAuth(`/v1/javadoc/${encodeURIComponent(version)}`, { - method: 'PATCH', - body: JSON.stringify(update) - }); + return (await response.json()) as JavadocResponse; + } + + async updateJavadoc(version: string, update: UpdateJavadocRequest): Promise { + const response = await this.fetchWithAuth(`/v1/javadoc/${encodeURIComponent(version)}`, { + method: "PATCH", + body: JSON.stringify(update), + }); - if (!response.ok) { - throw new Error('Failed to update Javadoc'); - } + if (!response.ok) { + throw new Error("Failed to update Javadoc"); } + } - private async fetchWithAuth(path: string, options: RequestInit = {}): Promise { - if (!this.accessToken.value) { - throw new Error('Not authenticated'); - } - - options.headers = { - ...(options.headers as Record), - 'Authorization': `Bearer ${this.accessToken.value}`, - 'Content-Type': 'application/json' - }; - - const response = await fetch(path, options); - - if (response.status == 401) { - // Token expired, try to refresh - await this.refreshAccessToken(); - - // Retry the request with the new token - options.headers = { - ...(options.headers as Record), - 'Authorization': `Bearer ${this.accessToken.value}`, - 'Content-Type': 'application/json' - }; - return fetch(path, options); - } - - return response; + private async fetchWithAuth(path: string, options: RequestInit = {}): Promise { + if (!this.accessToken.value) { + throw new Error("Not authenticated"); } - public async refreshAccessToken(): Promise { - const response = await fetch('/v1/auth/refresh', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.status == 401) { - // Unauthorized, need to log in - this.accessToken.next(null); - throw new Error('Authentication required'); - } else if (!response.ok) { - throw new Error('Failed to refresh access token'); - } - - const data = await response.json(); - this.accessToken.next(data.accessToken); + options.headers = { + ...(options.headers as Record), + Authorization: `Bearer ${this.accessToken.value}`, + "Content-Type": "application/json", + }; + + const response = await fetch(path, options); + + if (response.status == 401) { + // Token expired, try to refresh + await this.refreshAccessToken(); + + // Retry the request with the new token + options.headers = { + ...(options.headers as Record), + Authorization: `Bearer ${this.accessToken.value}`, + "Content-Type": "application/json", + }; + return fetch(path, options); } + + return response; + } + + public async refreshAccessToken(): Promise { + const response = await fetch("/v1/auth/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status == 401) { + // Unauthorized, need to log in + this.accessToken.next(null); + throw new Error("Authentication required"); + } else if (!response.ok) { + throw new Error("Failed to refresh access token"); + } + + const data = await response.json(); + this.accessToken.next(data.accessToken); + } } export const javadocApi = IS_JAVADOC_EDITOR ? new JavadocApi() : null!; export interface JavadocResponse { - data: Record; + data: Record; } export interface JavadocEntry { - value: string; - methods: Record | null; - fields: Record | null; + value: string; + methods: Record | null; + fields: Record | null; } export interface UpdateJavadocRequest { - className: string; - target: UpdateTarget | null; - documentation: string; + className: string; + target: UpdateTarget | null; + documentation: string; } export interface UpdateTarget { - type: "method" | "field"; - name: string; - descriptor: string; -} \ No newline at end of file + type: "method" | "field"; + name: string; + descriptor: string; +} diff --git a/src/javadoc/api/LoginModal.tsx b/src/javadoc/api/LoginModal.tsx index b448414..8d05b96 100644 --- a/src/javadoc/api/LoginModal.tsx +++ b/src/javadoc/api/LoginModal.tsx @@ -7,53 +7,51 @@ import { useState } from "react"; import { agreedEula } from "../../logic/Settings"; const LoginModal = () => { - if (!IS_JAVADOC_EDITOR) { - return (<>); - } + if (!IS_JAVADOC_EDITOR) { + return <>; + } + + const needsToLogin = useObservable(javadocApi.needsToLogin); + const accepted = useObservable(agreedEula.observable); + const [loading, setLoading] = useState(false); + const [messageApi, contextHolder] = message.useMessage(); - const needsToLogin = useObservable(javadocApi.needsToLogin); - const accepted = useObservable(agreedEula.observable); - const [loading, setLoading] = useState(false); - const [messageApi, contextHolder] = message.useMessage(); + const handleGithubLogin = async () => { + try { + setLoading(true); + const loginUrl = await javadocApi.getGithubLoginUrl(); + window.location.href = loginUrl; + } catch (error) { + console.error("Failed to get GitHub login URL:", error); + messageApi.error("Failed to initiate GitHub login. Please try again."); + setLoading(false); + } + }; - const handleGithubLogin = async () => { - try { - setLoading(true); - const loginUrl = await javadocApi.getGithubLoginUrl(); - window.location.href = loginUrl; - } catch (error) { - console.error("Failed to get GitHub login URL:", error); - messageApi.error("Failed to initiate GitHub login. Please try again."); - setLoading(false); - } - }; + return ( + +

Please log in to access the Javadoc editor.

- return ( - + - - - ); + Login with GitHub + + +
+ ); }; -export default LoginModal; \ No newline at end of file +export default LoginModal; diff --git a/src/logic/Browser.ts b/src/logic/Browser.ts index 52c1d08..bbba563 100644 --- a/src/logic/Browser.ts +++ b/src/logic/Browser.ts @@ -1,8 +1,8 @@ import { distinctUntilChanged, fromEvent, map, startWith, throttleTime } from "rxjs"; -export const isThin = fromEvent(window, 'resize').pipe( - startWith(null), - map(() => window.innerWidth < 800), - throttleTime(50), - distinctUntilChanged() -); \ No newline at end of file +export const isThin = fromEvent(window, "resize").pipe( + startWith(null), + map(() => window.innerWidth < 800), + throttleTime(50), + distinctUntilChanged(), +); diff --git a/src/logic/Decompiler.ts b/src/logic/Decompiler.ts index c55bfdb..f79d5dd 100644 --- a/src/logic/Decompiler.ts +++ b/src/logic/Decompiler.ts @@ -1,7 +1,16 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { - BehaviorSubject, - combineLatest, distinctUntilChanged, from, map, Observable, of, shareReplay, switchMap, tap, throttleTime + BehaviorSubject, + combineLatest, + distinctUntilChanged, + from, + map, + Observable, + of, + shareReplay, + switchMap, + tap, + throttleTime, } from "rxjs"; import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; import { selectedFile } from "./State"; @@ -14,61 +23,56 @@ import type { Jar } from "../utils/Jar"; const decompilerCounter = new BehaviorSubject(0); export const isDecompiling = decompilerCounter.pipe( - map(count => count > 0), - distinctUntilChanged() + map((count) => count > 0), + distinctUntilChanged(), ); -const decompilerOptions = combineLatest([ - displayLambdas.observable -]).pipe( - distinctUntilChanged(), - switchMap(([displayLambdas]) => { - const options: Options = {}; +const decompilerOptions = combineLatest([displayLambdas.observable]).pipe( + distinctUntilChanged(), + switchMap(([displayLambdas]) => { + const options: Options = {}; - if (displayLambdas) { - options["mark-corresponding-synthetics"] = "1"; - } + if (displayLambdas) { + options["mark-corresponding-synthetics"] = "1"; + } - return of(options); - }), + return of(options); + }), ); -decompilerOptions.subscribe(v => worker.setOptions(v)); +decompilerOptions.subscribe((v) => worker.setOptions(v)); export const currentResult = decompileResultPipeline(minecraftJar); -export function decompileResultPipeline(jar: Observable): Observable { - return combineLatest([ - selectedFile, - jar, - bytecode.observable, - decompilerOptions, - ]).pipe( - distinctUntilChanged(), - throttleTime(250), - switchMap(([className, jar, bytecode]) => { - if (bytecode) { - return from(getClassBytecode(className, jar.jar)); - } +export function decompileResultPipeline( + jar: Observable, +): Observable { + return combineLatest([selectedFile, jar, bytecode.observable, decompilerOptions]).pipe( + distinctUntilChanged(), + throttleTime(250), + switchMap(([className, jar, bytecode]) => { + if (bytecode) { + return from(getClassBytecode(className, jar.jar)); + } - return from(decompileClass(className, jar.jar)); - }), - shareReplay({ bufferSize: 1, refCount: false }) - ); + return from(decompileClass(className, jar.jar)); + }), + shareReplay({ bufferSize: 1, refCount: false }), + ); } export async function getClassBytecode(className: string, jar: Jar) { - try { - decompilerCounter.next(decompilerCounter.value + 1); - return await worker.getClassBytecode(className, jar); - } finally { - decompilerCounter.next(decompilerCounter.value - 1); - } + try { + decompilerCounter.next(decompilerCounter.value + 1); + return await worker.getClassBytecode(className, jar); + } finally { + decompilerCounter.next(decompilerCounter.value - 1); + } } export async function decompileClass(className: string, jar: Jar) { - try { - decompilerCounter.next(decompilerCounter.value + 1); - return await worker.decompileClass(className, jar); - } finally { - decompilerCounter.next(decompilerCounter.value - 1); - } + try { + decompilerCounter.next(decompilerCounter.value + 1); + return await worker.decompileClass(className, jar); + } finally { + decompilerCounter.next(decompilerCounter.value - 1); + } } diff --git a/src/logic/Diff.ts b/src/logic/Diff.ts index 19dcc4e..9613cf2 100644 --- a/src/logic/Diff.ts +++ b/src/logic/Diff.ts @@ -1,4 +1,12 @@ -import { BehaviorSubject, combineLatest, from, map, Observable, switchMap, shareReplay } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + from, + map, + Observable, + switchMap, + shareReplay, +} from "rxjs"; import { minecraftJar, minecraftJarPipeline, type MinecraftJar } from "./MinecraftApi"; import { currentResult, decompileResultPipeline } from "./Decompiler"; import { calculatedLineChanges } from "./LineChanges"; @@ -8,185 +16,180 @@ import type { DecompileResult } from "../workers/decompile/types"; export const hideUnchangedSizes = new BehaviorSubject(false); export interface EntryInfo { - classCrcs: Map; - totalUncompressedSize: number; + classCrcs: Map; + totalUncompressedSize: number; } export interface DiffSide { - selectedVersion: BehaviorSubject; - jar: Observable; - entries: Observable>; - result: Observable; + selectedVersion: BehaviorSubject; + jar: Observable; + entries: Observable>; + result: Observable; } export const leftDownloadProgress = new BehaviorSubject(undefined); let leftDiff: DiffSide | null = null; export function getLeftDiff(): DiffSide { - if (!leftDiff) { - leftDiff = {} as DiffSide; - leftDiff.selectedVersion = diffLeftselectedMinecraftVersion; - leftDiff.jar = minecraftJarPipeline(leftDiff.selectedVersion); - leftDiff.entries = leftDiff.jar.pipe( - switchMap(jar => from(getEntriesWithCRC(jar))) - ); - leftDiff.result = decompileResultPipeline(leftDiff.jar); - } - return leftDiff; + if (!leftDiff) { + leftDiff = {} as DiffSide; + leftDiff.selectedVersion = diffLeftselectedMinecraftVersion; + leftDiff.jar = minecraftJarPipeline(leftDiff.selectedVersion); + leftDiff.entries = leftDiff.jar.pipe(switchMap((jar) => from(getEntriesWithCRC(jar)))); + leftDiff.result = decompileResultPipeline(leftDiff.jar); + } + return leftDiff; } let rightDiff: DiffSide | null = null; export function getRightDiff(): DiffSide { - if (!rightDiff) { - rightDiff = { - selectedVersion: selectedMinecraftVersion, - jar: minecraftJar, - entries: minecraftJar.pipe( - switchMap(jar => from(getEntriesWithCRC(jar))) - ), - result: currentResult - }; - } - return rightDiff; + if (!rightDiff) { + rightDiff = { + selectedVersion: selectedMinecraftVersion, + jar: minecraftJar, + entries: minecraftJar.pipe(switchMap((jar) => from(getEntriesWithCRC(jar)))), + result: currentResult, + }; + } + return rightDiff; } export interface DiffSummary { - added: number; - deleted: number; - modified: number; + added: number; + deleted: number; + modified: number; } export interface ChangeInfo { - state: ChangeState; - additions?: number; - deletions?: number; + state: ChangeState; + additions?: number; + deletions?: number; } // Clear calculated line changes when diff versions change to prevent stale data setTimeout(() => { - combineLatest([ - getLeftDiff().selectedVersion, - selectedMinecraftVersion - ]).subscribe(() => { - calculatedLineChanges.next(new Map()); - }); + combineLatest([getLeftDiff().selectedVersion, selectedMinecraftVersion]).subscribe(() => { + calculatedLineChanges.next(new Map()); + }); }, 0); let diffChanges: Observable> | null = null; export function getDiffChanges(): Observable> { - if (!diffChanges) { - diffChanges = combineLatest([ - getLeftDiff().entries, - getRightDiff().entries, - hideUnchangedSizes, - calculatedLineChanges - ]).pipe( - map(([leftEntries, rightEntries, skipUnchangedSize, lineChanges]) => { - const changes = getChangedEntries(leftEntries, rightEntries, skipUnchangedSize); - lineChanges.forEach((counts, file) => { - const info = changes.get(file); - if (info) { - info.additions = counts.additions; - info.deletions = counts.deletions; - } - }); - return changes; - }), - shareReplay(1) - ); - } - return diffChanges; + if (!diffChanges) { + diffChanges = combineLatest([ + getLeftDiff().entries, + getRightDiff().entries, + hideUnchangedSizes, + calculatedLineChanges, + ]).pipe( + map(([leftEntries, rightEntries, skipUnchangedSize, lineChanges]) => { + const changes = getChangedEntries(leftEntries, rightEntries, skipUnchangedSize); + lineChanges.forEach((counts, file) => { + const info = changes.get(file); + if (info) { + info.additions = counts.additions; + info.deletions = counts.deletions; + } + }); + return changes; + }), + shareReplay(1), + ); + } + return diffChanges; } let diffSummaryObs: Observable | null = null; export function getDiffSummary(): Observable { - if (!diffSummaryObs) { - diffSummaryObs = getDiffChanges().pipe( - map(changes => { - const summary: DiffSummary = { added: 0, deleted: 0, modified: 0 }; - changes.forEach(info => { - summary[info.state]++; - }); - return summary; - }), - shareReplay(1) - ); - } - return diffSummaryObs; + if (!diffSummaryObs) { + diffSummaryObs = getDiffChanges().pipe( + map((changes) => { + const summary: DiffSummary = { added: 0, deleted: 0, modified: 0 }; + changes.forEach((info) => { + summary[info.state]++; + }); + return summary; + }), + shareReplay(1), + ); + } + return diffSummaryObs; } export type ChangeState = "added" | "deleted" | "modified"; async function getEntriesWithCRC(jar: MinecraftJar): Promise> { - const entries = new Map(); - - for (const [path, file] of Object.entries(jar.jar.entries)) { - if (!path.endsWith('.class')) { - continue; - } - - const className = path.substring(0, path.length - 6); - const lastSlash = path.lastIndexOf('/'); - const folder = lastSlash !== -1 ? path.substring(0, lastSlash + 1) : ''; - const fileName = path.substring(folder.length); - const baseFileName = fileName.includes('$') ? fileName.split('$')[0] : fileName.replace('.class', ''); - const baseClassName = folder + baseFileName + '.class'; - - const existing = entries.get(baseClassName); - if (existing) { - existing.classCrcs.set(className, file.crc32); - existing.totalUncompressedSize += file.uncompressedSize; - } else { - entries.set(baseClassName, { - classCrcs: new Map([[className, file.crc32]]), - totalUncompressedSize: file.uncompressedSize - }); - } + const entries = new Map(); + + for (const [path, file] of Object.entries(jar.jar.entries)) { + if (!path.endsWith(".class")) { + continue; + } + + const className = path.substring(0, path.length - 6); + const lastSlash = path.lastIndexOf("/"); + const folder = lastSlash !== -1 ? path.substring(0, lastSlash + 1) : ""; + const fileName = path.substring(folder.length); + const baseFileName = fileName.includes("$") + ? fileName.split("$")[0] + : fileName.replace(".class", ""); + const baseClassName = folder + baseFileName + ".class"; + + const existing = entries.get(baseClassName); + if (existing) { + existing.classCrcs.set(className, file.crc32); + existing.totalUncompressedSize += file.uncompressedSize; + } else { + entries.set(baseClassName, { + classCrcs: new Map([[className, file.crc32]]), + totalUncompressedSize: file.uncompressedSize, + }); } + } - return entries; + return entries; } function getChangedEntries( - leftEntries: Map, - rightEntries: Map, - skipUnchangedSize: boolean = false + leftEntries: Map, + rightEntries: Map, + skipUnchangedSize: boolean = false, ): Map { - const changes = new Map(); - - const allKeys = new Set([ - ...leftEntries.keys(), - ...rightEntries.keys() - ]); - - for (const key of allKeys) { - const leftInfo = leftEntries.get(key); - const rightInfo = rightEntries.get(key); - - if (leftInfo === undefined) { - changes.set(key, { state: "added" }); - } else if (rightInfo === undefined) { - changes.set(key, { state: "deleted" }); - } else { - const leftClasses = leftInfo.classCrcs; - const rightClasses = rightInfo.classCrcs; - - // Check if any of the classes (including inner classes) have changed by comparing their CRCs. - // A Map is used to track the CRC of each individual class file that belongs to this base class. - const hasChanges = leftClasses.size !== rightClasses.size || - Array.from(leftClasses.entries()).some(([className, leftCrc]) => rightClasses.get(className) !== leftCrc); - - if (!hasChanges) { - continue; - } - - if (skipUnchangedSize && leftInfo.totalUncompressedSize === rightInfo.totalUncompressedSize) { - continue; - } - - changes.set(key, { state: "modified" }); - } + const changes = new Map(); + + const allKeys = new Set([...leftEntries.keys(), ...rightEntries.keys()]); + + for (const key of allKeys) { + const leftInfo = leftEntries.get(key); + const rightInfo = rightEntries.get(key); + + if (leftInfo === undefined) { + changes.set(key, { state: "added" }); + } else if (rightInfo === undefined) { + changes.set(key, { state: "deleted" }); + } else { + const leftClasses = leftInfo.classCrcs; + const rightClasses = rightInfo.classCrcs; + + // Check if any of the classes (including inner classes) have changed by comparing their CRCs. + // A Map is used to track the CRC of each individual class file that belongs to this base class. + const hasChanges = + leftClasses.size !== rightClasses.size || + Array.from(leftClasses.entries()).some( + ([className, leftCrc]) => rightClasses.get(className) !== leftCrc, + ); + + if (!hasChanges) { + continue; + } + + if (skipUnchangedSize && leftInfo.totalUncompressedSize === rightInfo.totalUncompressedSize) { + continue; + } + + changes.set(key, { state: "modified" }); } + } - return changes; + return changes; } diff --git a/src/logic/FindAllReferences.ts b/src/logic/FindAllReferences.ts index e2e288f..243eda6 100644 --- a/src/logic/FindAllReferences.ts +++ b/src/logic/FindAllReferences.ts @@ -1,188 +1,204 @@ -import { BehaviorSubject, combineLatest, distinctUntilChanged, from, map, Observable, switchMap, throttleTime } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + from, + map, + Observable, + switchMap, + throttleTime, +} from "rxjs"; import { jarIndex, type ReferenceKey, type ReferenceString } from "../workers/JarIndex"; import { openTab } from "./Tabs"; import { referencesQuery } from "./State"; import type { Token } from "./Tokens"; import type { DecompileResult } from "../workers/decompile/types"; -export const referenceResults = referencesQuery - .pipe( - throttleTime(200), - distinctUntilChanged(), - switchMap((query) => { - if (!query) { - return from([[]]); - } - return jarIndex.pipe( - switchMap((index) => from(index.getReference(query))) - ); - }) - ); - -export const isViewingReferences = referencesQuery.pipe( - map((query) => query.length > 0) +export const referenceResults = referencesQuery.pipe( + throttleTime(200), + distinctUntilChanged(), + switchMap((query) => { + if (!query) { + return from([[]]); + } + return jarIndex.pipe(switchMap((index) => from(index.getReference(query)))); + }), ); +export const isViewingReferences = referencesQuery.pipe(map((query) => query.length > 0)); + // Format the reference string to be displayed by the user export function formatReference(reference: ReferenceString): string { - if (reference.startsWith("m:")) { - const parts = reference.slice(2).split(":"); - return `${parts[1]}${parts[2]}`; - } - if (reference.startsWith("f:")) { - const parts = reference.slice(2).split(":"); - return parts[1]; - } - if (reference.startsWith("c:")) { - return reference.slice(2); - } - return reference; + if (reference.startsWith("m:")) { + const parts = reference.slice(2).split(":"); + return `${parts[1]}${parts[2]}`; + } + if (reference.startsWith("f:")) { + const parts = reference.slice(2).split(":"); + return parts[1]; + } + if (reference.startsWith("c:")) { + return reference.slice(2); + } + return reference; } export function formatReferenceQuery(query: ReferenceKey): string { - const type = getQueryType(query); - - switch (type) { - case "class": - return query.split("/").pop() || query; - case "method": { - const parts = query.split(":"); - const className = parts[0].split("/").pop() || parts[0]; - return `${className}.${parts[1]}${parts[2]}`; - } - case "field": { - const parts = query.split(":"); - const className = parts[0].split("/").pop() || parts[0]; - return `${className}.${parts[1]}`; - } + const type = getQueryType(query); + + switch (type) { + case "class": + return query.split("/").pop() || query; + case "method": { + const parts = query.split(":"); + const className = parts[0].split("/").pop() || parts[0]; + return `${className}.${parts[1]}${parts[2]}`; } + case "field": { + const parts = query.split(":"); + const className = parts[0].split("/").pop() || parts[0]; + return `${className}.${parts[1]}`; + } + } } function getQueryType(query: ReferenceKey): "class" | "method" | "field" { - if (query.includes(":")) { - const parts = query.split(":"); - if (parts[2].includes("(")) { - return "method"; - } else { - return "field"; - } + if (query.includes(":")) { + const parts = query.split(":"); + if (parts[2].includes("(")) { + return "method"; + } else { + return "field"; } - return "class"; + } + return "class"; } interface ReferenceNavigation { - // The class to navigate to - className: string; - // The reference being navigated to - query: ReferenceKey; - // The location of where the reference is found - reference: ReferenceString; + // The class to navigate to + className: string; + // The reference being navigated to + query: ReferenceKey; + // The location of where the reference is found + reference: ReferenceString; } -export const nextReferenceNavigation = new BehaviorSubject(undefined); +export const nextReferenceNavigation = new BehaviorSubject( + undefined, +); export function goToReference(query: ReferenceKey, reference: ReferenceString) { - const className = reference.slice(2).split(":")[0].split('$')[0]; - openTab(className + ".class"); + const className = reference.slice(2).split(":")[0].split("$")[0]; + openTab(className + ".class"); - if (reference.startsWith("c:")) { - // Nothing to jump to - return; - } + if (reference.startsWith("c:")) { + // Nothing to jump to + return; + } - nextReferenceNavigation.next({ className, query, reference }); + nextReferenceNavigation.next({ className, query, reference }); } export function getNextJumpToken(decompileResult: DecompileResult): Token | undefined { - const referenceNavigation = nextReferenceNavigation.getValue(); + const referenceNavigation = nextReferenceNavigation.getValue(); - if (!referenceNavigation) { - return undefined; - } + if (!referenceNavigation) { + return undefined; + } - const { className, query, reference } = referenceNavigation; + const { className, query, reference } = referenceNavigation; - if (decompileResult.className != className) { - console.log("Decompile result class does not match reference navigation class", decompileResult.className, className); - return undefined; - } + if (decompileResult.className != className) { + console.log( + "Decompile result class does not match reference navigation class", + decompileResult.className, + className, + ); + return undefined; + } - nextReferenceNavigation.next(undefined); - - // This works by first finding the token that matches the reference we are looking for. - // We can then find the token that matches the declaration of the query we are looking for. - // This allows us to jump to the first reference of the query after the reference that was selected. - - let referenceTokenIndex: number | null = null; - - { // First find the reference token - const parts = reference.slice(2).split(":"); - const classname = parts[0]; - const name = parts[1]; - const descriptor = parts[2]; - const expectedType = reference.startsWith("m:") ? "method" : "field"; - - for (let i = 0; i < decompileResult.tokens.length; i++) { - const token = decompileResult.tokens[i]; - - if (!token.declaration) { - // We only want to jump to the declaration - continue; - } - - if (token.type != expectedType) { - continue; - } - - if (token.className == classname && token.name == name && token.descriptor == descriptor) { - if (token.type == "field") { - // For fields, just return the reference as there is only one declaration - return token; - } - - if (!query.includes(":")) { - // If the query is just a class, we can't find a method declaration for it - // Is this even possible? - return undefined; - } - - // For methods we can keep looking for a token that matches the query after this - referenceTokenIndex = i; - break; - } - } - } + nextReferenceNavigation.next(undefined); - if (!referenceTokenIndex) { - console.log("Could not find reference token for", reference); - return undefined; - } + // This works by first finding the token that matches the reference we are looking for. + // We can then find the token that matches the declaration of the query we are looking for. + // This allows us to jump to the first reference of the query after the reference that was selected. - const parts = query.split(":"); + let referenceTokenIndex: number | null = null; + + { + // First find the reference token + const parts = reference.slice(2).split(":"); + const classname = parts[0]; const name = parts[1]; const descriptor = parts[2]; - const queryType = getQueryType(query); + const expectedType = reference.startsWith("m:") ? "method" : "field"; - // Next continue searching from the reference token index to find the actual reference - for (let i = referenceTokenIndex + 1; i < decompileResult.tokens.length; i++) { - const token = decompileResult.tokens[i]; + for (let i = 0; i < decompileResult.tokens.length; i++) { + const token = decompileResult.tokens[i]; - // Special case for constructor reference - if (name == "" && token.type == "class" && token.className == parts[0]) { - return token; - } + if (!token.declaration) { + // We only want to jump to the declaration + continue; + } + + if (token.type != expectedType) { + continue; + } - if (queryType == "method" && token.type == "method" && token.name == name && token.descriptor == descriptor) { - return token; + if (token.className == classname && token.name == name && token.descriptor == descriptor) { + if (token.type == "field") { + // For fields, just return the reference as there is only one declaration + return token; } - if (queryType == "field" && token.type == "field" && token.name == name) { - return token; + if (!query.includes(":")) { + // If the query is just a class, we can't find a method declaration for it + // Is this even possible? + return undefined; } + + // For methods we can keep looking for a token that matches the query after this + referenceTokenIndex = i; + break; + } + } + } + + if (!referenceTokenIndex) { + console.log("Could not find reference token for", reference); + return undefined; + } + + const parts = query.split(":"); + const name = parts[1]; + const descriptor = parts[2]; + const queryType = getQueryType(query); + + // Next continue searching from the reference token index to find the actual reference + for (let i = referenceTokenIndex + 1; i < decompileResult.tokens.length; i++) { + const token = decompileResult.tokens[i]; + + // Special case for constructor reference + if (name == "" && token.type == "class" && token.className == parts[0]) { + return token; + } + + if ( + queryType == "method" && + token.type == "method" && + token.name == name && + token.descriptor == descriptor + ) { + return token; + } + + if (queryType == "field" && token.type == "field" && token.name == name) { + return token; } + } - // Give up if we reach another declaration, it means we didnt find it - // Just return the declaration that supposedly contains the reference - console.log("Could not find token for", query); - return decompileResult.tokens[referenceTokenIndex]; + // Give up if we reach another declaration, it means we didnt find it + // Just return the declaration that supposedly contains the reference + console.log("Could not find token for", query); + return decompileResult.tokens[referenceTokenIndex]; } diff --git a/src/logic/Inheritance.ts b/src/logic/Inheritance.ts index 0e09bf3..56a9443 100644 --- a/src/logic/Inheritance.ts +++ b/src/logic/Inheritance.ts @@ -1,110 +1,118 @@ -import { BehaviorSubject, combineLatest, distinctUntilChanged, map, of, shareReplay, switchMap } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + map, + of, + shareReplay, + switchMap, +} from "rxjs"; import { jarIndex } from "../workers/JarIndex"; import { minecraftJar } from "./MinecraftApi"; export class ClassNode { - readonly name: string; - parents: ClassNode[] = []; - children: ClassNode[] = []; - accessFlags: number = 0; + readonly name: string; + parents: ClassNode[] = []; + children: ClassNode[] = []; + accessFlags: number = 0; - constructor(name: string) { - this.name = name; - } - - getRoot(): ClassNode { - // oxlint-disable-next-line typescript/no-this-alias - let n: ClassNode = this; + constructor(name: string) { + this.name = name; + } - while (n.parents.length > 0) { - n = n.parents[0]; - } + getRoot(): ClassNode { + // oxlint-disable-next-line typescript/no-this-alias + let n: ClassNode = this; - return n; + while (n.parents.length > 0) { + n = n.parents[0]; } + + return n; + } } export class InheritanceIndex { - private readonly index = new Map(); + private readonly index = new Map(); - addClass(className: string): ClassNode { - let node = this.index.get(className); - if (!node) { - node = new ClassNode(className); - this.index.set(className, node); - } - return node; + addClass(className: string): ClassNode { + let node = this.index.get(className); + if (!node) { + node = new ClassNode(className); + this.index.set(className, node); } + return node; + } - addParentChildLink(parentName: string, childName: string): void { - const parent = this.addClass(parentName); - const child = this.addClass(childName); - - // Add parent if not already present - if (!child.parents.includes(parent)) { - child.parents.push(parent); - } + addParentChildLink(parentName: string, childName: string): void { + const parent = this.addClass(parentName); + const child = this.addClass(childName); - // Add to children list if not already present - if (!parent.children.includes(child)) { - parent.children.push(child); - } + // Add parent if not already present + if (!child.parents.includes(parent)) { + child.parents.push(parent); } - addChildParentLink(childName: string, parentName: string): void { - this.addParentChildLink(parentName, childName); + // Add to children list if not already present + if (!parent.children.includes(child)) { + parent.children.push(child); } -} - + } + addChildParentLink(childName: string, parentName: string): void { + this.addParentChildLink(parentName, childName); + } +} export const selectedInheritanceClassName = new BehaviorSubject(null); export const inheritanceIndex = combineLatest([jarIndex, minecraftJar]).pipe( - distinctUntilChanged(), - switchMap(async ([jarIndexInstance, jarInstance]) => { - const index = new InheritanceIndex(); - - const classDataArray = await jarIndexInstance.getClassData(); - - const classNames = new Set( - Object.keys(jarInstance.jar.entries) - .filter(name => name.endsWith(".class")) - .map(name => name.slice(0, -6)) - ); - - for (const classData of classDataArray) { - if (!classNames.has(classData.className)) { - continue; - } - - const node = index.addClass(classData.className); - node.accessFlags = classData.accessFlags; - - if (classData.superName && classData.superName.length > 0 && classNames.has(classData.superName)) { - index.addChildParentLink(classData.className, classData.superName); - } - - for (const interfaceName of classData.interfaces) { - if (classNames.has(interfaceName)) { - index.addChildParentLink(classData.className, interfaceName); - } - } + distinctUntilChanged(), + switchMap(async ([jarIndexInstance, jarInstance]) => { + const index = new InheritanceIndex(); + + const classDataArray = await jarIndexInstance.getClassData(); + + const classNames = new Set( + Object.keys(jarInstance.jar.entries) + .filter((name) => name.endsWith(".class")) + .map((name) => name.slice(0, -6)), + ); + + for (const classData of classDataArray) { + if (!classNames.has(classData.className)) { + continue; + } + + const node = index.addClass(classData.className); + node.accessFlags = classData.accessFlags; + + if ( + classData.superName && + classData.superName.length > 0 && + classNames.has(classData.superName) + ) { + index.addChildParentLink(classData.className, classData.superName); + } + + for (const interfaceName of classData.interfaces) { + if (classNames.has(interfaceName)) { + index.addChildParentLink(classData.className, interfaceName); } + } + } - return index; - }), - shareReplay({ bufferSize: 1, refCount: false }) + return index; + }), + shareReplay({ bufferSize: 1, refCount: false }), ); export const selectedInheritanceClassNode = selectedInheritanceClassName.pipe( - switchMap(className => { - if (className === null) { - return of(null); - } - return inheritanceIndex.pipe( - map(index => index.addClass(className)) - ); - }), - shareReplay({ bufferSize: 1, refCount: false }) -); \ No newline at end of file + switchMap((className) => { + if (className === null) { + return of(null); + } + return inheritanceIndex.pipe(map((index) => index.addClass(className))); + }), + shareReplay({ bufferSize: 1, refCount: false }), +); diff --git a/src/logic/JarFile.ts b/src/logic/JarFile.ts index 81deba7..9f36bc8 100644 --- a/src/logic/JarFile.ts +++ b/src/logic/JarFile.ts @@ -1,29 +1,39 @@ -import { BehaviorSubject, combineLatest, distinct, distinctUntilChanged, map, Observable, switchMap, throttleTime } from 'rxjs'; -import { minecraftJar } from './MinecraftApi'; -import { performSearch } from './Search'; -import { searchQuery } from './State'; +import { + BehaviorSubject, + combineLatest, + distinct, + distinctUntilChanged, + map, + Observable, + switchMap, + throttleTime, +} from "rxjs"; +import { minecraftJar } from "./MinecraftApi"; +import { performSearch } from "./Search"; +import { searchQuery } from "./State"; export const fileList = minecraftJar.pipe( - distinctUntilChanged(), - map(jar => Object.keys(jar.jar.entries)) + distinctUntilChanged(), + map((jar) => Object.keys(jar.jar.entries)), ); // File list that only contains outer class files export const classesList = fileList.pipe( - map(files => files.filter(file => file.endsWith('.class') && !file.includes('$'))) + map((files) => files.filter((file) => file.endsWith(".class") && !file.includes("$"))), ); const debouncedSearchQuery: Observable = searchQuery.pipe( - throttleTime(200), - distinctUntilChanged() + throttleTime(200), + distinctUntilChanged(), ); -export const searchResults: Observable = combineLatest([classesList, debouncedSearchQuery]).pipe( - switchMap(([classes, query]) => { - return [performSearch(query, classes)]; - }) +export const searchResults: Observable = combineLatest([ + classesList, + debouncedSearchQuery, +]).pipe( + switchMap(([classes, query]) => { + return [performSearch(query, classes)]; + }), ); -export const isSearching = searchQuery.pipe( - map((query) => query.length > 0) -); \ No newline at end of file +export const isSearching = searchQuery.pipe(map((query) => query.length > 0)); diff --git a/src/logic/Keybinds.ts b/src/logic/Keybinds.ts index 2488596..a099d93 100644 --- a/src/logic/Keybinds.ts +++ b/src/logic/Keybinds.ts @@ -4,19 +4,17 @@ import { focusSearch, showStructure, type KeybindSetting } from "./Settings"; // Set to true when the user is currently capturing a keybind export const capturingKeybind = new BehaviorSubject(null); -export const rawKeydownEvent = fromEvent(document, 'keydown'); +export const rawKeydownEvent = fromEvent(document, "keydown"); // Keydown events that should be listened to for general operation -export const keyDownEvent = rawKeydownEvent.pipe( - filter(() => capturingKeybind.value === null) -); +export const keyDownEvent = rawKeydownEvent.pipe(filter(() => capturingKeybind.value === null)); function keyBindEvent(setting: KeybindSetting): Observable { - return keyDownEvent.pipe( - filter(event => setting.matches(event)), - tap(event => event.preventDefault()) - ); + return keyDownEvent.pipe( + filter((event) => setting.matches(event)), + tap((event) => event.preventDefault()), + ); } export const focusSearchEvent = keyBindEvent(focusSearch); -export const showStructureEvent = keyBindEvent(showStructure); \ No newline at end of file +export const showStructureEvent = keyBindEvent(showStructure); diff --git a/src/logic/LineChanges.test.ts b/src/logic/LineChanges.test.ts index e65aaaf..b25aed9 100644 --- a/src/logic/LineChanges.test.ts +++ b/src/logic/LineChanges.test.ts @@ -1,104 +1,104 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { countLineDiff, updateLineChanges, calculatedLineChanges } from './LineChanges'; - -describe('LineChanges', () => { - describe('countLineDiff', () => { - it('should return 0 additions and deletions for identical strings', () => { - const text = 'line 1\nline 2'; - expect(countLineDiff(text, text)).toEqual({ additions: 0, deletions: 0 }); - }); - - it('should count a single addition', () => { - const oldText = 'line 1'; - const newText = 'line 1\nline 2'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 0 }); - }); - - it('should count a single deletion', () => { - const oldText = 'line 1\nline 2'; - const newText = 'line 1'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 0, deletions: 1 }); - }); - - it('should count modifications as one deletion and one addition', () => { - const oldText = 'line 1\nold line'; - const newText = 'line 1\nnew line'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 1 }); - }); - - it('should handle completely different strings', () => { - const oldText = 'a\nb\nc'; - const newText = 'd\ne'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 2, deletions: 3 }); - }); - - it('should handle special error prefixes for old text', () => { - const oldText = '// Class not found'; - const newText = 'public class Test {}'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 0 }); - }); - - it('should handle special error prefixes for new text', () => { - const oldText = 'public class Test {}'; - const newText = '// Error during decompilation'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 0, deletions: 1 }); - }); - - it('should handle empty strings', () => { - expect(countLineDiff('', 'line 1')).toEqual({ additions: 1, deletions: 0 }); - expect(countLineDiff('line 1', '')).toEqual({ additions: 0, deletions: 1 }); - expect(countLineDiff('', '')).toEqual({ additions: 0, deletions: 0 }); - }); - - it('should ignore common prefix and suffix', () => { - const oldText = 'start\nmiddle-old\nend'; - const newText = 'start\nmiddle-new\nend'; - expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 1 }); - }); +import { describe, it, expect, beforeEach } from "vitest"; +import { countLineDiff, updateLineChanges, calculatedLineChanges } from "./LineChanges"; + +describe("LineChanges", () => { + describe("countLineDiff", () => { + it("should return 0 additions and deletions for identical strings", () => { + const text = "line 1\nline 2"; + expect(countLineDiff(text, text)).toEqual({ additions: 0, deletions: 0 }); }); - describe('updateLineChanges', () => { - beforeEach(() => { - calculatedLineChanges.next(new Map()); - }); - - it('should update the BehaviorSubject with new changes', () => { - updateLineChanges('file.java', 'line 1', 'line 1\nline 2'); - const result = calculatedLineChanges.value.get('file.java'); - expect(result).toEqual({ additions: 1, deletions: 0 }); - }); - - it('should not update the BehaviorSubject if changes are identical', () => { - const initialMap = new Map([['file.java', { additions: 1, deletions: 0 }]]); - calculatedLineChanges.next(initialMap); - - updateLineChanges('file.java', 'line 1', 'line 1\nline 2'); - - expect(calculatedLineChanges.value).toBe(initialMap); // Reference should stay same - }); - - it('should add multiple files to the map', () => { - updateLineChanges('file1.java', 'a', 'a\nb'); - updateLineChanges('file2.java', 'x', 'y'); - - expect(calculatedLineChanges.value.get('file1.java')).toEqual({ additions: 1, deletions: 0 }); - expect(calculatedLineChanges.value.get('file2.java')).toEqual({ additions: 1, deletions: 1 }); - }); - - it('should evict old entries when reaching MAX_CALCULATED_LINE_CHANGES', () => { - // Fill up to 500 - for (let i = 0; i < 500; i++) { - updateLineChanges(`file${i}.java`, '', 'line'); - } - expect(calculatedLineChanges.value.size).toBe(500); - expect(calculatedLineChanges.value.has('file0.java')).toBe(true); - - // Add 501st entry - updateLineChanges('file501.java', '', 'line'); - - expect(calculatedLineChanges.value.size).toBe(500); - expect(calculatedLineChanges.value.has('file0.java')).toBe(false); // First one should be evicted - expect(calculatedLineChanges.value.has('file501.java')).toBe(true); - }); + it("should count a single addition", () => { + const oldText = "line 1"; + const newText = "line 1\nline 2"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 0 }); }); + + it("should count a single deletion", () => { + const oldText = "line 1\nline 2"; + const newText = "line 1"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 0, deletions: 1 }); + }); + + it("should count modifications as one deletion and one addition", () => { + const oldText = "line 1\nold line"; + const newText = "line 1\nnew line"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 1 }); + }); + + it("should handle completely different strings", () => { + const oldText = "a\nb\nc"; + const newText = "d\ne"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 2, deletions: 3 }); + }); + + it("should handle special error prefixes for old text", () => { + const oldText = "// Class not found"; + const newText = "public class Test {}"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 0 }); + }); + + it("should handle special error prefixes for new text", () => { + const oldText = "public class Test {}"; + const newText = "// Error during decompilation"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 0, deletions: 1 }); + }); + + it("should handle empty strings", () => { + expect(countLineDiff("", "line 1")).toEqual({ additions: 1, deletions: 0 }); + expect(countLineDiff("line 1", "")).toEqual({ additions: 0, deletions: 1 }); + expect(countLineDiff("", "")).toEqual({ additions: 0, deletions: 0 }); + }); + + it("should ignore common prefix and suffix", () => { + const oldText = "start\nmiddle-old\nend"; + const newText = "start\nmiddle-new\nend"; + expect(countLineDiff(oldText, newText)).toEqual({ additions: 1, deletions: 1 }); + }); + }); + + describe("updateLineChanges", () => { + beforeEach(() => { + calculatedLineChanges.next(new Map()); + }); + + it("should update the BehaviorSubject with new changes", () => { + updateLineChanges("file.java", "line 1", "line 1\nline 2"); + const result = calculatedLineChanges.value.get("file.java"); + expect(result).toEqual({ additions: 1, deletions: 0 }); + }); + + it("should not update the BehaviorSubject if changes are identical", () => { + const initialMap = new Map([["file.java", { additions: 1, deletions: 0 }]]); + calculatedLineChanges.next(initialMap); + + updateLineChanges("file.java", "line 1", "line 1\nline 2"); + + expect(calculatedLineChanges.value).toBe(initialMap); // Reference should stay same + }); + + it("should add multiple files to the map", () => { + updateLineChanges("file1.java", "a", "a\nb"); + updateLineChanges("file2.java", "x", "y"); + + expect(calculatedLineChanges.value.get("file1.java")).toEqual({ additions: 1, deletions: 0 }); + expect(calculatedLineChanges.value.get("file2.java")).toEqual({ additions: 1, deletions: 1 }); + }); + + it("should evict old entries when reaching MAX_CALCULATED_LINE_CHANGES", () => { + // Fill up to 500 + for (let i = 0; i < 500; i++) { + updateLineChanges(`file${i}.java`, "", "line"); + } + expect(calculatedLineChanges.value.size).toBe(500); + expect(calculatedLineChanges.value.has("file0.java")).toBe(true); + + // Add 501st entry + updateLineChanges("file501.java", "", "line"); + + expect(calculatedLineChanges.value.size).toBe(500); + expect(calculatedLineChanges.value.has("file0.java")).toBe(false); // First one should be evicted + expect(calculatedLineChanges.value.has("file501.java")).toBe(true); + }); + }); }); diff --git a/src/logic/LineChanges.ts b/src/logic/LineChanges.ts index ea6f52b..d85d911 100644 --- a/src/logic/LineChanges.ts +++ b/src/logic/LineChanges.ts @@ -2,86 +2,101 @@ import { BehaviorSubject } from "rxjs"; const MAX_CALCULATED_LINE_CHANGES = 500; -export const calculatedLineChanges = new BehaviorSubject>(new Map()); +export const calculatedLineChanges = new BehaviorSubject< + Map +>(new Map()); /** * Simple line-based diff to count additions and deletions without external libraries. * Uses an efficient O(N+M) hash-based counting approach. */ -export function countLineDiff(oldText: string, newText: string): { additions: number, deletions: number; } { - if (oldText === newText) return { additions: 0, deletions: 0 }; - - // Ignore error messages or "not found" messages from decompiler when counting - if (oldText.startsWith('// Class not found') || oldText.startsWith('// Error during decompilation')) { - const newLines = newText === "" ? 0 : newText.split(/\r?\n/).length; - return { additions: newLines, deletions: 0 }; - } - if (newText.startsWith('// Class not found') || newText.startsWith('// Error during decompilation')) { - const oldLines = oldText === "" ? 0 : oldText.split(/\r?\n/).length; - return { additions: 0, deletions: oldLines }; - } - - const oldLines = oldText === "" ? [] : oldText.split(/\r?\n/); - const newLines = newText === "" ? [] : newText.split(/\r?\n/); - - let start = 0; - while (start < oldLines.length && start < newLines.length && oldLines[start] === newLines[start]) { - start++; - } - - let oldEnd = oldLines.length - 1; - let newEnd = newLines.length - 1; - while (oldEnd >= start && newEnd >= start && oldLines[oldEnd] === newLines[newEnd]) { - oldEnd--; - newEnd--; - } - - const n = oldEnd - start + 1; - const m = newEnd - start + 1; - - if (n <= 0) return { additions: Math.max(0, m), deletions: 0 }; - if (m <= 0) return { additions: 0, deletions: Math.max(0, n) }; - - const oldDiff = oldLines.slice(start, oldEnd + 1); - const newDiff = newLines.slice(start, newEnd + 1); - - const lineCounts = new Map(); - for (const line of oldDiff) { - lineCounts.set(line, (lineCounts.get(line) || 0) + 1); +export function countLineDiff( + oldText: string, + newText: string, +): { additions: number; deletions: number } { + if (oldText === newText) return { additions: 0, deletions: 0 }; + + // Ignore error messages or "not found" messages from decompiler when counting + if ( + oldText.startsWith("// Class not found") || + oldText.startsWith("// Error during decompilation") + ) { + const newLines = newText === "" ? 0 : newText.split(/\r?\n/).length; + return { additions: newLines, deletions: 0 }; + } + if ( + newText.startsWith("// Class not found") || + newText.startsWith("// Error during decompilation") + ) { + const oldLines = oldText === "" ? 0 : oldText.split(/\r?\n/).length; + return { additions: 0, deletions: oldLines }; + } + + const oldLines = oldText === "" ? [] : oldText.split(/\r?\n/); + const newLines = newText === "" ? [] : newText.split(/\r?\n/); + + let start = 0; + while ( + start < oldLines.length && + start < newLines.length && + oldLines[start] === newLines[start] + ) { + start++; + } + + let oldEnd = oldLines.length - 1; + let newEnd = newLines.length - 1; + while (oldEnd >= start && newEnd >= start && oldLines[oldEnd] === newLines[newEnd]) { + oldEnd--; + newEnd--; + } + + const n = oldEnd - start + 1; + const m = newEnd - start + 1; + + if (n <= 0) return { additions: Math.max(0, m), deletions: 0 }; + if (m <= 0) return { additions: 0, deletions: Math.max(0, n) }; + + const oldDiff = oldLines.slice(start, oldEnd + 1); + const newDiff = newLines.slice(start, newEnd + 1); + + const lineCounts = new Map(); + for (const line of oldDiff) { + lineCounts.set(line, (lineCounts.get(line) || 0) + 1); + } + + let commonInDiff = 0; + for (const line of newDiff) { + const count = lineCounts.get(line); + if (count && count > 0) { + lineCounts.set(line, count - 1); + commonInDiff++; } + } - let commonInDiff = 0; - for (const line of newDiff) { - const count = lineCounts.get(line); - if (count && count > 0) { - lineCounts.set(line, count - 1); - commonInDiff++; - } - } + let deletionsInDiff = 0; + for (const count of lineCounts.values()) { + deletionsInDiff += count; + } - let deletionsInDiff = 0; - for (const count of lineCounts.values()) { - deletionsInDiff += count; - } - - return { - deletions: deletionsInDiff, - additions: newDiff.length - commonInDiff - }; + return { + deletions: deletionsInDiff, + additions: newDiff.length - commonInDiff, + }; } export function updateLineChanges(file: string, leftSource: string, rightSource: string) { - const { additions, deletions } = countLineDiff(leftSource, rightSource); - - const current = calculatedLineChanges.value; - const existing = current.get(file); - if (existing?.additions === additions && existing?.deletions === deletions) return; - - const next = new Map(current); - if (next.size >= MAX_CALCULATED_LINE_CHANGES) { - const firstKey = next.keys().next().value; - if (firstKey) next.delete(firstKey); - } - next.set(file, { additions, deletions }); - calculatedLineChanges.next(next); + const { additions, deletions } = countLineDiff(leftSource, rightSource); + + const current = calculatedLineChanges.value; + const existing = current.get(file); + if (existing?.additions === additions && existing?.deletions === deletions) return; + + const next = new Map(current); + if (next.size >= MAX_CALCULATED_LINE_CHANGES) { + const firstKey = next.keys().next().value; + if (firstKey) next.delete(firstKey); + } + next.set(file, { additions, deletions }); + calculatedLineChanges.next(next); } diff --git a/src/logic/MinecraftApi.ts b/src/logic/MinecraftApi.ts index 16273c2..28266ee 100644 --- a/src/logic/MinecraftApi.ts +++ b/src/logic/MinecraftApi.ts @@ -1,257 +1,272 @@ -import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, from, map, shareReplay, switchMap, tap, Observable, withLatestFrom } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + filter, + from, + map, + shareReplay, + switchMap, + tap, + Observable, + withLatestFrom, +} from "rxjs"; import { agreedEula } from "./Settings"; import { openJar, type Jar } from "../utils/Jar"; import { selectedMinecraftVersion } from "./State"; -const CACHE_NAME = 'mcsrc-v1'; +const CACHE_NAME = "mcsrc-v1"; const VERSIONS_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; interface VersionsList { - versions: VersionListEntry[]; + versions: VersionListEntry[]; } interface VersionListEntry { - id: string; - type: string; - url: string; - time: string; - releaseTime: string; - sha1: string; + id: string; + type: string; + url: string; + time: string; + releaseTime: string; + sha1: string; } interface VersionManifest { - id: string; - downloads: { - [key: string]: { - url: string; - sha1: string; - }; + id: string; + downloads: { + [key: string]: { + url: string; + sha1: string; }; + }; } export interface MinecraftJar { - version: string; - jar: Jar; - blob: Blob; + version: string; + jar: Jar; + blob: Blob; } export const minecraftVersions = agreedEula.observable.pipe( - filter(agreed => agreed), - switchMap(() => from(fetchVersions())), - map(versionsList => versionsList.versions), - tap(versions => { - // On inital load, if we dont have a version selected or the selected version is not valid, default to the latest version. - const currentVersion = selectedMinecraftVersion.value; - const isValid = currentVersion !== null && versions.some(v => v.id === currentVersion); + filter((agreed) => agreed), + switchMap(() => from(fetchVersions())), + map((versionsList) => versionsList.versions), + tap((versions) => { + // On inital load, if we dont have a version selected or the selected version is not valid, default to the latest version. + const currentVersion = selectedMinecraftVersion.value; + const isValid = currentVersion !== null && versions.some((v) => v.id === currentVersion); - if (!isValid && versions.length > 0) { - selectedMinecraftVersion.next(versions[0].id); - } - }), - shareReplay({ bufferSize: 1, refCount: false }) + if (!isValid && versions.length > 0) { + selectedMinecraftVersion.next(versions[0].id); + } + }), + shareReplay({ bufferSize: 1, refCount: false }), ); export const minecraftVersionIds = minecraftVersions.pipe( - map(versions => versions.map(v => v.id)) + map((versions) => versions.map((v) => v.id)), ); export const downloadProgress = new BehaviorSubject(undefined); export const minecraftJar = minecraftJarPipeline(selectedMinecraftVersion); export function minecraftJarPipeline(source$: Observable): Observable { - return combineLatest([ - source$.pipe( - filter(id => id !== null), - distinctUntilChanged() - ), - minecraftVersions - ]).pipe( - map(([version, versions]) => versions.find(v => v.id === version)), - filter((version) => version !== undefined), - tap((version) => console.log(`Opening Minecraft jar ${version.id}`)), - switchMap(version => from(downloadMinecraftJar(version, downloadProgress))), - shareReplay({ bufferSize: 1, refCount: false }) - ); + return combineLatest([ + source$.pipe( + filter((id) => id !== null), + distinctUntilChanged(), + ), + minecraftVersions, + ]).pipe( + map(([version, versions]) => versions.find((v) => v.id === version)), + filter((version) => version !== undefined), + tap((version) => console.log(`Opening Minecraft jar ${version.id}`)), + switchMap((version) => from(downloadMinecraftJar(version, downloadProgress))), + shareReplay({ bufferSize: 1, refCount: false }), + ); } async function getJson(url: string): Promise { - console.log(`Fetching JSON from ${url}`); - const response = await fetch(url); + console.log(`Fetching JSON from ${url}`); + const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch JSON from ${url}: ${response.statusText}`); - } + if (!response.ok) { + throw new Error(`Failed to fetch JSON from ${url}: ${response.statusText}`); + } - return response.json(); + return response.json(); } async function fetchVersions(): Promise { - const mojang = await getJson(VERSIONS_URL); - const filteredMojangVersions = mojang.versions.filter(v => { - const match = v.id.match(/^(\d+)\.(\d+)/); - if (!match) return false; - const major = parseInt(match[1], 10); - return major >= 26; - }); - const versions = filteredMojangVersions - .concat(EXPERIMENTAL_VERSIONS.versions) - .sort((a, b) => b.releaseTime.localeCompare(a.releaseTime)); - return { - versions: versions - }; + const mojang = await getJson(VERSIONS_URL); + const filteredMojangVersions = mojang.versions.filter((v) => { + const match = v.id.match(/^(\d+)\.(\d+)/); + if (!match) return false; + const major = parseInt(match[1], 10); + return major >= 26; + }); + const versions = filteredMojangVersions + .concat(EXPERIMENTAL_VERSIONS.versions) + .sort((a, b) => b.releaseTime.localeCompare(a.releaseTime)); + return { + versions: versions, + }; } async function fetchVersionManifest(version: VersionListEntry): Promise { - return getJson(version.url); + return getJson(version.url); } async function cachedFetch(url: string): Promise { - if (!('caches' in window)) { - return fetch(url); - } + if (!("caches" in window)) { + return fetch(url); + } - const cache = await caches.open(CACHE_NAME); - const cachedResponse = await cache.match(url); - if (cachedResponse) { - return cachedResponse; - }; + const cache = await caches.open(CACHE_NAME); + const cachedResponse = await cache.match(url); + if (cachedResponse) { + return cachedResponse; + } - const response = await fetch(url); - if (response.ok) { - await cache.put(url, response.clone()); - } - return response; + const response = await fetch(url); + if (response.ok) { + await cache.put(url, response.clone()); + } + return response; } -async function downloadMinecraftJar(version: VersionListEntry, progress: BehaviorSubject): Promise { - console.log(`Downloading Minecraft jar for version: ${version.id}`); - const versionManifest = await fetchVersionManifest(version); - const response = await cachedFetch(versionManifest.downloads.client.url); - if (!response.ok) { - throw new Error(`Failed to download Minecraft jar: ${response.statusText}`); - } +async function downloadMinecraftJar( + version: VersionListEntry, + progress: BehaviorSubject, +): Promise { + console.log(`Downloading Minecraft jar for version: ${version.id}`); + const versionManifest = await fetchVersionManifest(version); + const response = await cachedFetch(versionManifest.downloads.client.url); + if (!response.ok) { + throw new Error(`Failed to download Minecraft jar: ${response.statusText}`); + } - const contentLength = response.headers.get('content-length'); - const total = contentLength ? parseInt(contentLength, 10) : 0; + const contentLength = response.headers.get("content-length"); + const total = contentLength ? parseInt(contentLength, 10) : 0; - if (!response.body || total === 0) { - const blob = await response.blob(); - const jar = await openJar(version.id, blob); - progress.next(undefined); - return { version: version.id, jar, blob }; - } + if (!response.body || total === 0) { + const blob = await response.blob(); + const jar = await openJar(version.id, blob); + progress.next(undefined); + return { version: version.id, jar, blob }; + } - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - let receivedLength = 0; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let receivedLength = 0; - while (true) { - const { done, value } = await reader.read(); - if (done) break; + while (true) { + const { done, value } = await reader.read(); + if (done) break; - chunks.push(value); - receivedLength += value.length; + chunks.push(value); + receivedLength += value.length; - const percent = Math.round((receivedLength / total) * 100); - progress.next(percent); - } + const percent = Math.round((receivedLength / total) * 100); + progress.next(percent); + } - const blob = new Blob(chunks); - const jar = await openJar(version.id, blob); - progress.next(undefined); - return { version: version.id, jar, blob }; + const blob = new Blob(chunks); + const jar = await openJar(version.id, blob); + progress.next(undefined); + return { version: version.id, jar, blob }; } // Hardcode as these are never going to change. const EXPERIMENTAL_VERSIONS: VersionsList = { - versions: [ - { - id: "25w45a_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/25w45a_unobfuscated.json", - time: "2025-11-04T14:07:08+00:00", - releaseTime: "2025-11-04T14:07:08+00:00", - sha1: "7a3c149f148b6aa5ac3af48c4f701adea7e5b615", - }, - { - id: "25w46a_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/25w46a_unobfuscated.json", - time: "2025-11-11T13:20:54+00:00", - releaseTime: "2025-11-11T13:20:54+00:00", - sha1: "314ade2afeada364047798e163ef8e82427c69e1", - }, - { - id: "1.21.11-pre1_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre1_unobfuscated.json", - time: "2025-11-19T08:30:46+00:00", - releaseTime: "2025-11-19T08:30:46+00:00", - sha1: "9c267f8dda2728bae55201a753cdd07b584709f1", - }, - { - id: "1.21.11-pre2_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre2_unobfuscated.json", - time: "2025-11-21T12:07:21+00:00", - releaseTime: "2025-11-21T12:07:21+00:00", - sha1: "2955ce0af0512fdfe53ff0740b017344acf6f397", - }, - { - id: "1.21.11-pre3_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre3_unobfuscated.json", - time: "2025-11-25T14:14:30+00:00", - releaseTime: "2025-11-25T14:14:30+00:00", - sha1: "579bf3428f72b5ea04883d202e4831bfdcb2aa8d", - }, - { - id: "1.21.11-pre4_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre4_unobfuscated.json", - time: "2025-12-01T13:40:12+00:00", - releaseTime: "2025-12-01T13:40:12+00:00", - sha1: "410ce37a2506adcfd54ef7d89168cfbe89cac4cb", - }, - { - id: "1.21.11-pre5_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre5_unobfuscated.json", - time: "2025-12-03T13:34:06+00:00", - releaseTime: "2025-12-03T13:34:06+00:00", - sha1: "1028441ca6d288bbf2103e773196bf524f7260fd", - }, - { - id: "1.21.11-rc1_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc1_unobfuscated.json", - time: "2025-12-04T15:56:55+00:00", - releaseTime: "2025-12-04T15:56:55+00:00", - sha1: "5d3ee0ef1f0251cf7e073354ca9e085a884a643d", - }, - { - id: "1.21.11-rc2_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc2_unobfuscated.json", - time: "2025-12-05T11:57:45+00:00", - releaseTime: "2025-12-05T11:57:45+00:00", - sha1: "9282a3fb154d2a425086c62c11827281308bf93b", - }, - { - id: "1.21.11-rc3_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc3_unobfuscated.json", - time: "2025-12-08T13:59:34+00:00", - releaseTime: "2025-12-08T13:59:34+00:00", - sha1: "ce3f7ac6d0e9d23ea4e5f0354b91ff15039d9931", - }, - { - id: "1.21.11_unobfuscated", - type: "unobfuscated", - url: "https://maven.fabricmc.net/net/minecraft/1_21_11_unobfuscated.json", - time: "2025-12-09T12:43:15+00:00", - releaseTime: "2025-12-09T12:43:15+00:00", - sha1: "327be7759157b04495c591dbb721875e341877af", - } - ] + versions: [ + { + id: "25w45a_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/25w45a_unobfuscated.json", + time: "2025-11-04T14:07:08+00:00", + releaseTime: "2025-11-04T14:07:08+00:00", + sha1: "7a3c149f148b6aa5ac3af48c4f701adea7e5b615", + }, + { + id: "25w46a_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/25w46a_unobfuscated.json", + time: "2025-11-11T13:20:54+00:00", + releaseTime: "2025-11-11T13:20:54+00:00", + sha1: "314ade2afeada364047798e163ef8e82427c69e1", + }, + { + id: "1.21.11-pre1_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre1_unobfuscated.json", + time: "2025-11-19T08:30:46+00:00", + releaseTime: "2025-11-19T08:30:46+00:00", + sha1: "9c267f8dda2728bae55201a753cdd07b584709f1", + }, + { + id: "1.21.11-pre2_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre2_unobfuscated.json", + time: "2025-11-21T12:07:21+00:00", + releaseTime: "2025-11-21T12:07:21+00:00", + sha1: "2955ce0af0512fdfe53ff0740b017344acf6f397", + }, + { + id: "1.21.11-pre3_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre3_unobfuscated.json", + time: "2025-11-25T14:14:30+00:00", + releaseTime: "2025-11-25T14:14:30+00:00", + sha1: "579bf3428f72b5ea04883d202e4831bfdcb2aa8d", + }, + { + id: "1.21.11-pre4_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre4_unobfuscated.json", + time: "2025-12-01T13:40:12+00:00", + releaseTime: "2025-12-01T13:40:12+00:00", + sha1: "410ce37a2506adcfd54ef7d89168cfbe89cac4cb", + }, + { + id: "1.21.11-pre5_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-pre5_unobfuscated.json", + time: "2025-12-03T13:34:06+00:00", + releaseTime: "2025-12-03T13:34:06+00:00", + sha1: "1028441ca6d288bbf2103e773196bf524f7260fd", + }, + { + id: "1.21.11-rc1_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc1_unobfuscated.json", + time: "2025-12-04T15:56:55+00:00", + releaseTime: "2025-12-04T15:56:55+00:00", + sha1: "5d3ee0ef1f0251cf7e073354ca9e085a884a643d", + }, + { + id: "1.21.11-rc2_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc2_unobfuscated.json", + time: "2025-12-05T11:57:45+00:00", + releaseTime: "2025-12-05T11:57:45+00:00", + sha1: "9282a3fb154d2a425086c62c11827281308bf93b", + }, + { + id: "1.21.11-rc3_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11-rc3_unobfuscated.json", + time: "2025-12-08T13:59:34+00:00", + releaseTime: "2025-12-08T13:59:34+00:00", + sha1: "ce3f7ac6d0e9d23ea4e5f0354b91ff15039d9931", + }, + { + id: "1.21.11_unobfuscated", + type: "unobfuscated", + url: "https://maven.fabricmc.net/net/minecraft/1_21_11_unobfuscated.json", + time: "2025-12-09T12:43:15+00:00", + releaseTime: "2025-12-09T12:43:15+00:00", + sha1: "327be7759157b04495c591dbb721875e341877af", + }, + ], }; diff --git a/src/logic/Permalink.test.ts b/src/logic/Permalink.test.ts index 7a9b61a..e0297cc 100644 --- a/src/logic/Permalink.test.ts +++ b/src/logic/Permalink.test.ts @@ -1,164 +1,164 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi } from "vitest"; // Mock the Settings module -vi.mock('./Settings', () => ({ - resetPermalinkAffectingSettings: vi.fn(), - supportsPermalinking: { pipe: vi.fn() } +vi.mock("./Settings", () => ({ + resetPermalinkAffectingSettings: vi.fn(), + supportsPermalinking: { pipe: vi.fn() }, })); // Mock the State module to prevent initialization issues -vi.mock('./State', () => ({ - selectedMinecraftVersion: { subscribe: vi.fn() }, - selectedFile: { subscribe: vi.fn() }, - selectedLines: { subscribe: vi.fn() }, - diffView: { subscribe: vi.fn() } +vi.mock("./State", () => ({ + selectedMinecraftVersion: { subscribe: vi.fn() }, + selectedFile: { subscribe: vi.fn() }, + selectedLines: { subscribe: vi.fn() }, + diffView: { subscribe: vi.fn() }, })); // Import the actual parsing function -import { parsePathToState } from './Permalink'; - -describe('Permalink', () => { - describe('parsePathToState', () => { - describe('Default State', () => { - it('should return default state when path is empty', () => { - expect(parsePathToState('')).toEqual(null); - }); - - it('should return default state when path has less than 3 segments', () => { - expect(parsePathToState('1')).toEqual(null); - expect(parsePathToState('1/1.21')).toEqual(null); - }); - - it('should return default state when path is malformed', () => { - expect(parsePathToState('//')).toEqual(null); - }); +import { parsePathToState } from "./Permalink"; + +describe("Permalink", () => { + describe("parsePathToState", () => { + describe("Default State", () => { + it("should return default state when path is empty", () => { + expect(parsePathToState("")).toEqual(null); + }); + + it("should return default state when path has less than 3 segments", () => { + expect(parsePathToState("1")).toEqual(null); + expect(parsePathToState("1/1.21")).toEqual(null); + }); + + it("should return default state when path is malformed", () => { + expect(parsePathToState("//")).toEqual(null); + }); + }); + + describe("Basic Path Parsing", () => { + it("should parse simple permalink with version, mc version, and file", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting")!; + + expect(state.version).toBe(1); + expect(state.minecraftVersion).toBe("1.21"); + expect(state.file).toBe("net/minecraft/ChatFormatting.class"); + expect(state.selectedLines).toBe(null); + }); + + it("should append .class if not present", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting")!; + expect(state.file).toBe("net/minecraft/ChatFormatting.class"); + }); + + it("should not duplicate .class extension", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting.class")!; + expect(state.file).toBe("net/minecraft/ChatFormatting.class"); + }); + + it("should handle nested package paths", () => { + const state = parsePathToState("1/1.21/net/minecraft/world/entity/player/Player")!; + expect(state.file).toBe("net/minecraft/world/entity/player/Player.class"); + }); + }); + + describe("Line Number Parsing", () => { + it("should parse single line number with #", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting#L123")!; + + expect(state.selectedLines).toEqual({ + line: 123, + lineEnd: undefined, }); + }); - describe('Basic Path Parsing', () => { - it('should parse simple permalink with version, mc version, and file', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting')!; - - expect(state.version).toBe(1); - expect(state.minecraftVersion).toBe('1.21'); - expect(state.file).toBe('net/minecraft/ChatFormatting.class'); - expect(state.selectedLines).toBe(null); - }); - - it('should append .class if not present', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting')!; - expect(state.file).toBe('net/minecraft/ChatFormatting.class'); - }); - - it('should not duplicate .class extension', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting.class')!; - expect(state.file).toBe('net/minecraft/ChatFormatting.class'); - }); - - it('should handle nested package paths', () => { - const state = parsePathToState('1/1.21/net/minecraft/world/entity/player/Player')!; - expect(state.file).toBe('net/minecraft/world/entity/player/Player.class'); - }); + it("should parse line range with #", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting#L10-20")!; + + expect(state.selectedLines).toEqual({ + line: 10, + lineEnd: 20, }); + }); + + it("should handle URL-encoded line marker (%23)", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting%23L50")!; + + expect(state.selectedLines).toEqual({ + line: 50, + lineEnd: undefined, + }); + }); + + it("should handle URL-encoded line range (%23)", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting%23L10-20")!; - describe('Line Number Parsing', () => { - it('should parse single line number with #', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting#L123')!; - - expect(state.selectedLines).toEqual({ - line: 123, - lineEnd: undefined - }); - }); - - it('should parse line range with #', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting#L10-20')!; - - expect(state.selectedLines).toEqual({ - line: 10, - lineEnd: 20 - }); - }); - - it('should handle URL-encoded line marker (%23)', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting%23L50')!; - - expect(state.selectedLines).toEqual({ - line: 50, - lineEnd: undefined - }); - }); - - it('should handle URL-encoded line range (%23)', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting%23L10-20')!; - - expect(state.selectedLines).toEqual({ - line: 10, - lineEnd: 20 - }); - }); - - it('should handle line number at end of complex path', () => { - const state = parsePathToState('1/1.21/net/minecraft/world/entity/player/Player#L456')!; - - expect(state.file).toBe('net/minecraft/world/entity/player/Player.class'); - expect(state.selectedLines).toEqual({ - line: 456, - lineEnd: undefined - }); - }); - - it('should return null selectedLines when no line number present', () => { - const state = parsePathToState('1/1.21/net/minecraft/ChatFormatting')!; - expect(state.selectedLines).toBe(null); - }); + expect(state.selectedLines).toEqual({ + line: 10, + lineEnd: 20, }); + }); - describe('URL Decoding', () => { - it('should decode URL-encoded minecraft version', () => { - const state = parsePathToState('1/1.21%2B/net/minecraft/ChatFormatting')!; - expect(state.minecraftVersion).toBe('1.21+'); - }); + it("should handle line number at end of complex path", () => { + const state = parsePathToState("1/1.21/net/minecraft/world/entity/player/Player#L456")!; - it('should handle spaces in version (unlikely but possible)', () => { - const state = parsePathToState('1/test%20version/net/minecraft/ChatFormatting')!; - expect(state.minecraftVersion).toBe('test version'); - }); + expect(state.file).toBe("net/minecraft/world/entity/player/Player.class"); + expect(state.selectedLines).toEqual({ + line: 456, + lineEnd: undefined, }); + }); - describe('Backwards Compatibility', () => { - it('should handle legacy version name 25w45a', () => { - const state = parsePathToState('1/25w45a/net/minecraft/ChatFormatting')!; - expect(state.minecraftVersion).toBe('25w45a_unobfuscated'); - }); - - it('should not modify other version names', () => { - const state = parsePathToState('1/25w46a/net/minecraft/ChatFormatting')!; - expect(state.minecraftVersion).toBe('25w46a'); - }); - - it('should handle the legacy version with line numbers', () => { - const state = parsePathToState('1/25w45a/net/minecraft/ChatFormatting#L100')!; - - expect(state.minecraftVersion).toBe('25w45a_unobfuscated'); - expect(state.selectedLines).toEqual({ - line: 100, - lineEnd: undefined - }); - }); + it("should return null selectedLines when no line number present", () => { + const state = parsePathToState("1/1.21/net/minecraft/ChatFormatting")!; + expect(state.selectedLines).toBe(null); + }); + }); + + describe("URL Decoding", () => { + it("should decode URL-encoded minecraft version", () => { + const state = parsePathToState("1/1.21%2B/net/minecraft/ChatFormatting")!; + expect(state.minecraftVersion).toBe("1.21+"); + }); + + it("should handle spaces in version (unlikely but possible)", () => { + const state = parsePathToState("1/test%20version/net/minecraft/ChatFormatting")!; + expect(state.minecraftVersion).toBe("test version"); + }); + }); + + describe("Backwards Compatibility", () => { + it("should handle legacy version name 25w45a", () => { + const state = parsePathToState("1/25w45a/net/minecraft/ChatFormatting")!; + expect(state.minecraftVersion).toBe("25w45a_unobfuscated"); + }); + + it("should not modify other version names", () => { + const state = parsePathToState("1/25w46a/net/minecraft/ChatFormatting")!; + expect(state.minecraftVersion).toBe("25w46a"); + }); + + it("should handle the legacy version with line numbers", () => { + const state = parsePathToState("1/25w45a/net/minecraft/ChatFormatting#L100")!; + + expect(state.minecraftVersion).toBe("25w45a_unobfuscated"); + expect(state.selectedLines).toEqual({ + line: 100, + lineEnd: undefined, }); + }); + }); + + describe("Real-world Examples", () => { + it("should parse multiline permalink", () => { + const state = parsePathToState("1/1.21.4/net/minecraft/server/MinecraftServer#L250-260")!; - describe('Real-world Examples', () => { - it('should parse multiline permalink', () => { - const state = parsePathToState('1/1.21.4/net/minecraft/server/MinecraftServer#L250-260')!; - - expect(state.version).toBe(1); - expect(state.minecraftVersion).toBe('1.21.4'); - expect(state.file).toBe('net/minecraft/server/MinecraftServer.class'); - expect(state.selectedLines).toEqual({ - line: 250, - lineEnd: 260 - }); - }); + expect(state.version).toBe(1); + expect(state.minecraftVersion).toBe("1.21.4"); + expect(state.file).toBe("net/minecraft/server/MinecraftServer.class"); + expect(state.selectedLines).toEqual({ + line: 250, + lineEnd: 260, }); + }); }); + }); }); diff --git a/src/logic/Permalink.ts b/src/logic/Permalink.ts index 181ad68..831b433 100644 --- a/src/logic/Permalink.ts +++ b/src/logic/Permalink.ts @@ -3,124 +3,122 @@ import { resetPermalinkAffectingSettings, supportsPermalinking } from "./Setting import { diffView, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; export interface State { - version: number; // Allows us to change the permalink structure in the future - minecraftVersion: string; - file: string; - selectedLines: { - line: number; - lineEnd?: number; - } | null; + version: number; // Allows us to change the permalink structure in the future + minecraftVersion: string; + file: string; + selectedLines: { + line: number; + lineEnd?: number; + } | null; } const DEFAULT_STATE: State = { - version: 0, - minecraftVersion: "", - file: "net/minecraft/ChatFormatting.class", - selectedLines: null + version: 0, + minecraftVersion: "", + file: "net/minecraft/ChatFormatting.class", + selectedLines: null, }; export const parsePathToState = (path: string): State | null => { - // Check for line number marker (e.g., #L123 or #L10-20) - let lineNumber: number | null = null; - let lineEnd: number | null = null; - const lineMatch = path.match(/(?:#|%23)L(\d+)(?:-(\d+))?$/); - if (lineMatch) { - lineNumber = parseInt(lineMatch[1], 10); - if (lineMatch[2]) { - lineEnd = parseInt(lineMatch[2], 10); - } - path = path.substring(0, lineMatch.index); - } - - const segments = path.split('/').filter(s => s.length > 0); - - if (segments.length < 3) { - return null; + // Check for line number marker (e.g., #L123 or #L10-20) + let lineNumber: number | null = null; + let lineEnd: number | null = null; + const lineMatch = path.match(/(?:#|%23)L(\d+)(?:-(\d+))?$/); + if (lineMatch) { + lineNumber = parseInt(lineMatch[1], 10); + if (lineMatch[2]) { + lineEnd = parseInt(lineMatch[2], 10); } - - const version = parseInt(segments[0], 10); - let minecraftVersion = decodeURIComponent(segments[1]); - const filePath = segments.slice(2).join('/'); - - // Backwards compatibility with the incorrect version name used previously - if (minecraftVersion == "25w45a") { - minecraftVersion = "25w45a_unobfuscated"; - } - - return { - version, - minecraftVersion, - file: filePath + (filePath.endsWith('.class') ? '' : '.class'), - selectedLines: lineNumber ? { line: lineNumber, lineEnd: lineEnd || undefined } : null - }; + path = path.substring(0, lineMatch.index); + } + + const segments = path.split("/").filter((s) => s.length > 0); + + if (segments.length < 3) { + return null; + } + + const version = parseInt(segments[0], 10); + let minecraftVersion = decodeURIComponent(segments[1]); + const filePath = segments.slice(2).join("/"); + + // Backwards compatibility with the incorrect version name used previously + if (minecraftVersion == "25w45a") { + minecraftVersion = "25w45a_unobfuscated"; + } + + return { + version, + minecraftVersion, + file: filePath + (filePath.endsWith(".class") ? "" : ".class"), + selectedLines: lineNumber ? { line: lineNumber, lineEnd: lineEnd || undefined } : null, + }; }; export const getInitialState = (): State => { - const pathname = window.location.pathname; - const hash = window.location.hash; - - const newStyle = pathname !== '/' && pathname !== ''; - - // Use pathname if it's not just "/" (new style), otherwise use hash (old style) - let path = newStyle - ? pathname.slice(1) // Remove leading / - : (hash.startsWith('#/') ? hash.slice(2) : (hash.startsWith('#') ? hash.slice(1) : '')); - - // For new style (pathname-based), append hash if it contains line number - if (newStyle && hash.startsWith('#L')) { - path += hash; + const pathname = window.location.pathname; + const hash = window.location.hash; + + const newStyle = pathname !== "/" && pathname !== ""; + + // Use pathname if it's not just "/" (new style), otherwise use hash (old style) + let path = newStyle + ? pathname.slice(1) // Remove leading / + : hash.startsWith("#/") + ? hash.slice(2) + : hash.startsWith("#") + ? hash.slice(1) + : ""; + + // For new style (pathname-based), append hash if it contains line number + if (newStyle && hash.startsWith("#L")) { + path += hash; + } + + try { + const state = parsePathToState(path); + if (state === null) { + return DEFAULT_STATE; } - try { - const state = parsePathToState(path); - if (state === null) { - return DEFAULT_STATE; - } - - resetPermalinkAffectingSettings(); - return state; - } catch (e) { - console.error("Error parsing permalink:", e); - return DEFAULT_STATE; - } + resetPermalinkAffectingSettings(); + return state; + } catch (e) { + console.error("Error parsing permalink:", e); + return DEFAULT_STATE; + } }; if (typeof window !== "undefined") { - window.addEventListener('load', () => { - combineLatest([ - selectedMinecraftVersion, - selectedFile, - selectedLines, - supportsPermalinking, - diffView - ]).subscribe(([ - minecraftVersion, - file, - selectedLines, - supported, - diffView - ]) => { - const className = file.split('/').pop()?.replace('.class', '') || file; - document.title = className; - - if (!supported || diffView) { - window.location.hash = ''; - window.history.replaceState({}, '', '/'); - return; - } - - let url = `/1/${minecraftVersion}/${file.replace(".class", "")}`; - - if (selectedLines) { - const { line, lineEnd } = selectedLines; - if (lineEnd && lineEnd !== line) { - url += `#L${Math.min(line, lineEnd)}-${Math.max(line, lineEnd)}`; - } else { - url += `#L${line}`; - } - } - - window.history.replaceState({}, '', url); - }); + window.addEventListener("load", () => { + combineLatest([ + selectedMinecraftVersion, + selectedFile, + selectedLines, + supportsPermalinking, + diffView, + ]).subscribe(([minecraftVersion, file, selectedLines, supported, diffView]) => { + const className = file.split("/").pop()?.replace(".class", "") || file; + document.title = className; + + if (!supported || diffView) { + window.location.hash = ""; + window.history.replaceState({}, "", "/"); + return; + } + + let url = `/1/${minecraftVersion}/${file.replace(".class", "")}`; + + if (selectedLines) { + const { line, lineEnd } = selectedLines; + if (lineEnd && lineEnd !== line) { + url += `#L${Math.min(line, lineEnd)}-${Math.max(line, lineEnd)}`; + } else { + url += `#L${line}`; + } + } + + window.history.replaceState({}, "", url); }); + }); } diff --git a/src/logic/Search.test.ts b/src/logic/Search.test.ts index 4ba91bb..1c3d396 100644 --- a/src/logic/Search.test.ts +++ b/src/logic/Search.test.ts @@ -1,195 +1,180 @@ -import { describe, it, expect } from 'vitest'; -import { performSearch, getCamelCaseAcronym, matchesCamelCase } from './Search'; - -describe('Search Algorithm', () => { - - describe('Exact Match', () => { - it('should prioritize exact matches', () => { - const classes = [ - 'com/example/Player', - 'com/example/PlayerEntity', - 'com/example/MultiPlayer', - ]; - const results = performSearch('player', classes); - expect(results[0]).toBe('com/example/Player'); - }); +import { describe, it, expect } from "vitest"; +import { performSearch, getCamelCaseAcronym, matchesCamelCase } from "./Search"; + +describe("Search Algorithm", () => { + describe("Exact Match", () => { + it("should prioritize exact matches", () => { + const classes = ["com/example/Player", "com/example/PlayerEntity", "com/example/MultiPlayer"]; + const results = performSearch("player", classes); + expect(results[0]).toBe("com/example/Player"); }); + }); + + describe("Starts With", () => { + it("should prioritize starts-with matches", () => { + const classes = [ + "com/example/EntityPlayer", + "com/example/Player", + "com/example/PlayerEntity", + ]; + const results = performSearch("player", classes); + expect(results[0]).toBe("com/example/Player"); + expect(results[1]).toBe("com/example/PlayerEntity"); + }); + }); - describe('Starts With', () => { - it('should prioritize starts-with matches', () => { - const classes = [ - 'com/example/EntityPlayer', - 'com/example/Player', - 'com/example/PlayerEntity', - ]; - const results = performSearch('player', classes); - expect(results[0]).toBe('com/example/Player'); - expect(results[1]).toBe('com/example/PlayerEntity'); - }); + describe("CamelCase Matching", () => { + it("should match CamelCase acronyms", () => { + const classes = ["net/minecraft/server/MinecraftServer", "net/minecraft/util/MathHelper"]; + const results = performSearch("ms", classes); + expect(results).toContain("net/minecraft/server/MinecraftServer"); }); - describe('CamelCase Matching', () => { - it('should match CamelCase acronyms', () => { - const classes = [ - 'net/minecraft/server/MinecraftServer', - 'net/minecraft/util/MathHelper', - ]; - const results = performSearch('ms', classes); - expect(results).toContain('net/minecraft/server/MinecraftServer'); - }); - - it('should prioritize exact CamelCase acronym matches', () => { - const classes = [ - 'net/minecraft/client/renderer/RenderType', - 'net/minecraft/world/entity/player/Player', - ]; - const results = performSearch('rt', classes); - expect(results[0]).toBe('net/minecraft/client/renderer/RenderType'); - }); - - it('should match partial CamelCase acronyms', () => { - const classes = [ - 'net/minecraft/world/entity/player/Player', - 'net/minecraft/core/BlockPos', - 'net/minecraft/world/item/ItemStack', - ]; - const results = performSearch('bp', classes); - expect(results).toContain('net/minecraft/core/BlockPos'); - }); + it("should prioritize exact CamelCase acronym matches", () => { + const classes = [ + "net/minecraft/client/renderer/RenderType", + "net/minecraft/world/entity/player/Player", + ]; + const results = performSearch("rt", classes); + expect(results[0]).toBe("net/minecraft/client/renderer/RenderType"); }); - describe('Contains Match', () => { - it('should find classes containing the query', () => { - const classes = [ - 'com/example/EntityPlayer', - 'com/example/Player', - ]; - const results = performSearch('player', classes); - expect(results).toHaveLength(2); - expect(results).toContain('com/example/EntityPlayer'); - }); + it("should match partial CamelCase acronyms", () => { + const classes = [ + "net/minecraft/world/entity/player/Player", + "net/minecraft/core/BlockPos", + "net/minecraft/world/item/ItemStack", + ]; + const results = performSearch("bp", classes); + expect(results).toContain("net/minecraft/core/BlockPos"); + }); + }); + + describe("Contains Match", () => { + it("should find classes containing the query", () => { + const classes = ["com/example/EntityPlayer", "com/example/Player"]; + const results = performSearch("player", classes); + expect(results).toHaveLength(2); + expect(results).toContain("com/example/EntityPlayer"); }); + }); + + describe("Scoring Priority", () => { + it("should order results by match quality", () => { + const classes = [ + "com/example/EntityBlock", + "com/example/Block", + "com/example/BlockEntity", + "com/example/BedrockLevel", + ]; + const results = performSearch("block", classes); + + // Exact match first + expect(results[0]).toBe("com/example/Block"); + // Starts with second + expect(results[1]).toBe("com/example/BlockEntity"); + // Contains last + expect(results[2]).toBe("com/example/EntityBlock"); + }); + }); + + describe("Case Insensitivity", () => { + it("should match regardless of case", () => { + const classes = ["com/example/Player", "com/example/PLAYER", "com/example/player"]; - describe('Scoring Priority', () => { - it('should order results by match quality', () => { - const classes = [ - 'com/example/EntityBlock', - 'com/example/Block', - 'com/example/BlockEntity', - 'com/example/BedrockLevel', - ]; - const results = performSearch('block', classes); - - // Exact match first - expect(results[0]).toBe('com/example/Block'); - // Starts with second - expect(results[1]).toBe('com/example/BlockEntity'); - // Contains last - expect(results[2]).toBe('com/example/EntityBlock'); - }); + const results1 = performSearch("player", classes); + const results2 = performSearch("PLAYER", classes); + const results3 = performSearch("Player", classes); + + expect(results1).toHaveLength(3); + expect(results2).toHaveLength(3); + expect(results3).toHaveLength(3); }); + }); - describe('Case Insensitivity', () => { - it('should match regardless of case', () => { - const classes = [ - 'com/example/Player', - 'com/example/PLAYER', - 'com/example/player', - ]; - - const results1 = performSearch('player', classes); - const results2 = performSearch('PLAYER', classes); - const results3 = performSearch('Player', classes); - - expect(results1).toHaveLength(3); - expect(results2).toHaveLength(3); - expect(results3).toHaveLength(3); - }); + describe("Empty Query", () => { + it("should return empty array for empty query", () => { + const classes = ["com/example/Player"]; + const results = performSearch("", classes); + expect(results).toHaveLength(0); }); + }); - describe('Empty Query', () => { - it('should return empty array for empty query', () => { - const classes = ['com/example/Player']; - const results = performSearch('', classes); - expect(results).toHaveLength(0); - }); + describe("Result Limit", () => { + it("should limit results to 100 items", () => { + const classes = Array.from({ length: 200 }, (_, i) => `com/example/Class${i}`); + const results = performSearch("class", classes); + expect(results.length).toBeLessThanOrEqual(100); + }); + }); + + describe("Real-world Cases", () => { + it('should prioritize exact match "Items" over classes starting with "Item"', () => { + const classes = [ + "net/minecraft/client/renderer/item/ItemStackRenderState", + "net/minecraft/client/gui/ItemSlotMouseAction", + "net/minecraft/references/Items", + "net/minecraft/util/datafix/fixes/ItemShulkerBoxColorFix", + "net/minecraft/util/datafix/fixes/ItemSpawnEggFix", + ]; + const results = performSearch("Items", classes); + + // Exact match should be first + expect(results[0]).toBe("net/minecraft/references/Items"); }); - describe('Result Limit', () => { - it('should limit results to 100 items', () => { - const classes = Array.from({ length: 200 }, (_, i) => `com/example/Class${i}`); - const results = performSearch('class', classes); - expect(results.length).toBeLessThanOrEqual(100); - }); + it('should prioritize "Items" over "Item" when searching for "Items"', () => { + const classes = [ + "net/minecraft/world/item/Item", + "net/minecraft/references/Items", + "net/minecraft/world/item/Items", + ]; + const results = performSearch("Items", classes); + + // Both exact matches "Items" should come before "Item" + expect(results[0]).toBe("net/minecraft/references/Items"); + expect(results[1]).toBe("net/minecraft/world/item/Items"); + // "Item" should not match at all when searching for "Items" + expect(results).not.toContain("net/minecraft/world/item/Item"); }); - describe('Real-world Cases', () => { - it('should prioritize exact match "Items" over classes starting with "Item"', () => { - const classes = [ - 'net/minecraft/client/renderer/item/ItemStackRenderState', - 'net/minecraft/client/gui/ItemSlotMouseAction', - 'net/minecraft/references/Items', - 'net/minecraft/util/datafix/fixes/ItemShulkerBoxColorFix', - 'net/minecraft/util/datafix/fixes/ItemSpawnEggFix', - ]; - const results = performSearch('Items', classes); - - // Exact match should be first - expect(results[0]).toBe('net/minecraft/references/Items'); - }); - - it('should prioritize "Items" over "Item" when searching for "Items"', () => { - const classes = [ - 'net/minecraft/world/item/Item', - 'net/minecraft/references/Items', - 'net/minecraft/world/item/Items', - ]; - const results = performSearch('Items', classes); - - // Both exact matches "Items" should come before "Item" - expect(results[0]).toBe('net/minecraft/references/Items'); - expect(results[1]).toBe('net/minecraft/world/item/Items'); - // "Item" should not match at all when searching for "Items" - expect(results).not.toContain('net/minecraft/world/item/Item'); - }); - - it('should prioritize shorter class names when scores are equal', () => { - const classes = [ - 'com/example/PlayerController', - 'com/example/Player', - 'com/example/PlayerEntity', - ]; - const results = performSearch('play', classes); - - // All start with "play", but shorter name should come first - expect(results[0]).toBe('com/example/Player'); - }); + it("should prioritize shorter class names when scores are equal", () => { + const classes = [ + "com/example/PlayerController", + "com/example/Player", + "com/example/PlayerEntity", + ]; + const results = performSearch("play", classes); + + // All start with "play", but shorter name should come first + expect(results[0]).toBe("com/example/Player"); + }); + }); + + describe("Helper Functions", () => { + describe("getCamelCaseAcronym", () => { + it("should extract capital letters", () => { + expect(getCamelCaseAcronym("MinecraftServer")).toBe("MS"); + expect(getCamelCaseAcronym("RenderType")).toBe("RT"); + expect(getCamelCaseAcronym("BlockPos")).toBe("BP"); + }); + + it("should return empty string for no capitals", () => { + expect(getCamelCaseAcronym("lowercase")).toBe(""); + }); }); - describe('Helper Functions', () => { - describe('getCamelCaseAcronym', () => { - it('should extract capital letters', () => { - expect(getCamelCaseAcronym('MinecraftServer')).toBe('MS'); - expect(getCamelCaseAcronym('RenderType')).toBe('RT'); - expect(getCamelCaseAcronym('BlockPos')).toBe('BP'); - }); - - it('should return empty string for no capitals', () => { - expect(getCamelCaseAcronym('lowercase')).toBe(''); - }); - }); - - describe('matchesCamelCase', () => { - it('should match CamelCase acronyms case-insensitively', () => { - expect(matchesCamelCase('MinecraftServer', 'ms')).toBe(true); - expect(matchesCamelCase('MinecraftServer', 'MS')).toBe(true); - expect(matchesCamelCase('MinecraftServer', 'M')).toBe(true); - }); - - it('should not match if acronym does not start with query', () => { - expect(matchesCamelCase('MinecraftServer', 'sr')).toBe(false); - expect(matchesCamelCase('MinecraftServer', 's')).toBe(false); - }); - }); + describe("matchesCamelCase", () => { + it("should match CamelCase acronyms case-insensitively", () => { + expect(matchesCamelCase("MinecraftServer", "ms")).toBe(true); + expect(matchesCamelCase("MinecraftServer", "MS")).toBe(true); + expect(matchesCamelCase("MinecraftServer", "M")).toBe(true); + }); + + it("should not match if acronym does not start with query", () => { + expect(matchesCamelCase("MinecraftServer", "sr")).toBe(false); + expect(matchesCamelCase("MinecraftServer", "s")).toBe(false); + }); }); + }); }); diff --git a/src/logic/Search.ts b/src/logic/Search.ts index 89652be..be094b8 100644 --- a/src/logic/Search.ts +++ b/src/logic/Search.ts @@ -1,68 +1,62 @@ export function getCamelCaseAcronym(str: string): string { - return str.replace(/[^A-Z]/g, ''); + return str.replace(/[^A-Z]/g, ""); } export function matchesCamelCase(className: string, query: string): boolean { - const acronym = getCamelCaseAcronym(className); - return acronym.toLowerCase().startsWith(query.toLowerCase()); + const acronym = getCamelCaseAcronym(className); + return acronym.toLowerCase().startsWith(query.toLowerCase()); } // Vibe coded mess that no one other than copilot or should read or touch :D export function performSearch(query: string, classes: string[]): string[] { - if (query.length === 0) { - return []; - } - - const lowerQuery = query.toLowerCase(); - - const results = classes - .filter(className => { - const simpleClassName = className.split('/').pop() || className; - const lowerSimpleName = simpleClassName.toLowerCase(); - - return lowerSimpleName.includes(lowerQuery) || matchesCamelCase(simpleClassName, query); - }) - .map(className => { - const simpleClassName = className.split('/').pop() || className; - const lowerSimpleName = simpleClassName.toLowerCase(); - - let score = 0; - - if (lowerSimpleName === lowerQuery) { - score = 0; - } - else if (lowerSimpleName.startsWith(lowerQuery)) { - score = 1; - } - else if (getCamelCaseAcronym(simpleClassName).toLowerCase() === lowerQuery) { - score = 2; - } - else if (matchesCamelCase(simpleClassName, query)) { - score = 3; - } - else { - score = 4 + lowerSimpleName.indexOf(lowerQuery); - } - - return { className, score }; - }) - .sort((a, b) => { - if (a.score !== b.score) { - return a.score - b.score; - } - - const aSimple = a.className.split('/').pop() || a.className; - const bSimple = b.className.split('/').pop() || b.className; - if (aSimple.length !== bSimple.length) { - return aSimple.length - bSimple.length; - } - - return aSimple.localeCompare(bSimple); - }) - .slice(0, 100) - .map(result => result.className); - - return results; + if (query.length === 0) { + return []; + } + + const lowerQuery = query.toLowerCase(); + + const results = classes + .filter((className) => { + const simpleClassName = className.split("/").pop() || className; + const lowerSimpleName = simpleClassName.toLowerCase(); + + return lowerSimpleName.includes(lowerQuery) || matchesCamelCase(simpleClassName, query); + }) + .map((className) => { + const simpleClassName = className.split("/").pop() || className; + const lowerSimpleName = simpleClassName.toLowerCase(); + + let score = 0; + + if (lowerSimpleName === lowerQuery) { + score = 0; + } else if (lowerSimpleName.startsWith(lowerQuery)) { + score = 1; + } else if (getCamelCaseAcronym(simpleClassName).toLowerCase() === lowerQuery) { + score = 2; + } else if (matchesCamelCase(simpleClassName, query)) { + score = 3; + } else { + score = 4 + lowerSimpleName.indexOf(lowerQuery); + } + + return { className, score }; + }) + .sort((a, b) => { + if (a.score !== b.score) { + return a.score - b.score; + } + + const aSimple = a.className.split("/").pop() || a.className; + const bSimple = b.className.split("/").pop() || b.className; + if (aSimple.length !== bSimple.length) { + return aSimple.length - bSimple.length; + } + + return aSimple.localeCompare(bSimple); + }) + .slice(0, 100) + .map((result) => result.className); + + return results; } - - diff --git a/src/logic/Settings.ts b/src/logic/Settings.ts index 8561eb5..97b2422 100644 --- a/src/logic/Settings.ts +++ b/src/logic/Settings.ts @@ -1,154 +1,186 @@ // oxlint-disable typescript/no-redundant-type-constituents -import { BehaviorSubject, combineLatest, distinctUntilChanged, map, Observable, switchMap } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + distinctUntilChanged, + map, + Observable, + switchMap, +} from "rxjs"; import * as decompiler from "../workers/decompile/client"; - -export type ModifierKey = 'Ctrl' | 'Alt' | 'Shift'; +export type ModifierKey = "Ctrl" | "Alt" | "Shift"; export type Key = string; export type KeybindValue = - | Key - | `${ModifierKey}+${Key}` - | `${ModifierKey}+${ModifierKey}+${Key}` - | `${ModifierKey}+${ModifierKey}+${ModifierKey}+${Key}`; + | Key + | `${ModifierKey}+${Key}` + | `${ModifierKey}+${ModifierKey}+${Key}` + | `${ModifierKey}+${ModifierKey}+${ModifierKey}+${Key}`; abstract class Setting { - protected key: string; - protected subject: BehaviorSubject; - readonly defaultValue: T; - private toString: (t: T) => string; - - constructor(key: string, defaultValue: T, fromString: (s: string) => T, toString: (t: T) => string) { - const stored = localStorage.getItem(`setting_${key}`); - const initialValue = stored ? fromString(stored) : defaultValue; - - this.key = key; - this.subject = new BehaviorSubject(initialValue); - this.defaultValue = defaultValue; - this.toString = toString; - - window.addEventListener('storage', (event) => { - if (event.key === `setting_${this.key}` && event.newValue !== null) { - const newValue = fromString(event.newValue); - if (this.subject.value !== newValue) { - this.subject.next(newValue); - } - } - }); - } - - get observable(): Observable { - return this.subject; - } - - get value(): T { - return this.subject.value; - } - - set value(newValue: T) { - this.subject.next(newValue); - localStorage.setItem(`setting_${this.key}`, this.toString(newValue)); - } + protected key: string; + protected subject: BehaviorSubject; + readonly defaultValue: T; + private toString: (t: T) => string; + + constructor( + key: string, + defaultValue: T, + fromString: (s: string) => T, + toString: (t: T) => string, + ) { + const stored = localStorage.getItem(`setting_${key}`); + const initialValue = stored ? fromString(stored) : defaultValue; + + this.key = key; + this.subject = new BehaviorSubject(initialValue); + this.defaultValue = defaultValue; + this.toString = toString; + + window.addEventListener("storage", (event) => { + if (event.key === `setting_${this.key}` && event.newValue !== null) { + const newValue = fromString(event.newValue); + if (this.subject.value !== newValue) { + this.subject.next(newValue); + } + } + }); + } + + get observable(): Observable { + return this.subject; + } + + get value(): T { + return this.subject.value; + } + + set value(newValue: T) { + this.subject.next(newValue); + localStorage.setItem(`setting_${this.key}`, this.toString(newValue)); + } } export class BooleanSetting extends Setting { - constructor(key: string, defaultValue: boolean) { - super(key, defaultValue, s => s === "true", b => b ? "true" : "false"); - } + constructor(key: string, defaultValue: boolean) { + super( + key, + defaultValue, + (s) => s === "true", + (b) => (b ? "true" : "false"), + ); + } } export class NumberSetting extends Setting { - constructor(key: string, defaultValue: number) { - super(key, defaultValue, (s) => { - const n = Number.parseInt(s); - return Number.isNaN(n) ? defaultValue : n; - }, n => n.toString()); - } + constructor(key: string, defaultValue: number) { + super( + key, + defaultValue, + (s) => { + const n = Number.parseInt(s); + return Number.isNaN(n) ? defaultValue : n; + }, + (n) => n.toString(), + ); + } } export class KeybindSetting extends Setting { - constructor(key: string, defaultValue: KeybindValue) { - super(key, defaultValue, s => s, v => v); + constructor(key: string, defaultValue: KeybindValue) { + super( + key, + defaultValue, + (s) => s, + (v) => v, + ); + } + + reset(): void { + this.value = this.defaultValue; + } + + setFromEvent(event: KeyboardEvent): void { + const parts: string[] = []; + + if (event.ctrlKey) parts.push("Ctrl"); + if (event.altKey) parts.push("Alt"); + if (event.shiftKey) parts.push("Shift"); + if (event.metaKey) parts.push("Cmd"); + + const modifierKeys = ["Control", "Alt", "Shift", "Meta"]; + if (!modifierKeys.includes(event.key)) { + parts.push(event.key); } - reset(): void { - this.value = this.defaultValue; - } - - setFromEvent(event: KeyboardEvent): void { - const parts: string[] = []; - - if (event.ctrlKey) parts.push('Ctrl'); - if (event.altKey) parts.push('Alt'); - if (event.shiftKey) parts.push('Shift'); - if (event.metaKey) parts.push('Cmd'); - - const modifierKeys = ['Control', 'Alt', 'Shift', 'Meta']; - if (!modifierKeys.includes(event.key)) { - parts.push(event.key); - } - - if (parts.length > 0) { - this.value = parts.join('+'); - } - } - - parse(): { ctrl: boolean; alt: boolean; shift: boolean; cmd: boolean; key: string | null; } { - const keys = this.value.split('+').map(k => k.toLowerCase()); - const modifierKeys = ['ctrl', 'alt', 'shift', 'cmd']; - const mainKey = keys.find(k => !modifierKeys.includes(k)) ?? null; - - return { - ctrl: keys.includes('ctrl'), - alt: keys.includes('alt'), - shift: keys.includes('shift'), - cmd: keys.includes('cmd'), - key: mainKey - }; - } - - matches(event: KeyboardEvent): boolean { - const parsed = this.parse(); - if (event.ctrlKey !== parsed.ctrl) return false; - if (event.altKey !== parsed.alt) return false; - if (event.shiftKey !== parsed.shift) return false; - if (event.metaKey !== parsed.cmd) return false; - - if (!parsed.key) return false; - - return event.key.toLowerCase() === parsed.key.toLowerCase(); + if (parts.length > 0) { + this.value = parts.join("+"); } + } + + parse(): { ctrl: boolean; alt: boolean; shift: boolean; cmd: boolean; key: string | null } { + const keys = this.value.split("+").map((k) => k.toLowerCase()); + const modifierKeys = ["ctrl", "alt", "shift", "cmd"]; + const mainKey = keys.find((k) => !modifierKeys.includes(k)) ?? null; + + return { + ctrl: keys.includes("ctrl"), + alt: keys.includes("alt"), + shift: keys.includes("shift"), + cmd: keys.includes("cmd"), + key: mainKey, + }; + } + + matches(event: KeyboardEvent): boolean { + const parsed = this.parse(); + if (event.ctrlKey !== parsed.ctrl) return false; + if (event.altKey !== parsed.alt) return false; + if (event.shiftKey !== parsed.shift) return false; + if (event.metaKey !== parsed.cmd) return false; + + if (!parsed.key) return false; + + return event.key.toLowerCase() === parsed.key.toLowerCase(); + } } -export const agreedEula = new BooleanSetting('eula', false); -export const enableTabs = new BooleanSetting('enable_tabs', true); -export const compactPackages = new BooleanSetting('compact_packages', true); -export const displayLambdas = new BooleanSetting('display_lambdas', false); -export const bytecode = new BooleanSetting('bytecode', false); -export const unifiedDiff = new BooleanSetting('unified_diff', false); -export const focusSearch = new KeybindSetting('focus_search', 'Ctrl+ '); -export const showStructure = new KeybindSetting('show_structure', 'Ctrl+F12'); +export const agreedEula = new BooleanSetting("eula", false); +export const enableTabs = new BooleanSetting("enable_tabs", true); +export const compactPackages = new BooleanSetting("compact_packages", true); +export const displayLambdas = new BooleanSetting("display_lambdas", false); +export const bytecode = new BooleanSetting("bytecode", false); +export const unifiedDiff = new BooleanSetting("unified_diff", false); +export const focusSearch = new KeybindSetting("focus_search", "Ctrl+ "); +export const showStructure = new KeybindSetting("show_structure", "Ctrl+F12"); -export const preferWasmDecompiler = new BooleanSetting('prefer_wasm_decompiler', true); +export const preferWasmDecompiler = new BooleanSetting("prefer_wasm_decompiler", true); preferWasmDecompiler.observable - .pipe(distinctUntilChanged()) - .subscribe((v) => decompiler.setRuntime(v)); + .pipe(distinctUntilChanged()) + .subscribe((v) => decompiler.setRuntime(v)); export const MAX_THREADS = navigator.hardwareConcurrency || 4; -export const decompilerThreads = new NumberSetting("decompiler_threads", Math.max(MAX_THREADS / 2, 1)); +export const decompilerThreads = new NumberSetting( + "decompiler_threads", + Math.max(MAX_THREADS / 2, 1), +); export const decompilerSplits = new NumberSetting("decompiler_splits", 100); -export const supportsPermalinking = combineLatest([displayLambdas.observable, bytecode.observable]).pipe( - map(([lambdaDisplay, bytecode]) => { - if (lambdaDisplay || bytecode) { - // Alters the decompilation output, so permalinks are not stable - return false; - } +export const supportsPermalinking = combineLatest([ + displayLambdas.observable, + bytecode.observable, +]).pipe( + map(([lambdaDisplay, bytecode]) => { + if (lambdaDisplay || bytecode) { + // Alters the decompilation output, so permalinks are not stable + return false; + } - return true; - }) + return true; + }), ); export function resetPermalinkAffectingSettings(): void { - displayLambdas.value = false; - bytecode.value = false; + displayLambdas.value = false; + bytecode.value = false; } diff --git a/src/logic/State.ts b/src/logic/State.ts index 809de41..04a9f8f 100644 --- a/src/logic/State.ts +++ b/src/logic/State.ts @@ -7,7 +7,9 @@ const initialState = getInitialState(); /// All of the user controled global state should be defined here: -export const selectedMinecraftVersion = new BehaviorSubject(initialState.minecraftVersion); +export const selectedMinecraftVersion = new BehaviorSubject( + initialState.minecraftVersion, +); export const mobileDrawerOpen = new BehaviorSubject(false); export const selectedFile = new BehaviorSubject(initialState.file); diff --git a/src/logic/Tabs.ts b/src/logic/Tabs.ts index 8fd5460..377820a 100644 --- a/src/logic/Tabs.ts +++ b/src/logic/Tabs.ts @@ -3,149 +3,146 @@ import { editor } from "monaco-editor"; import { selectedFile, openTabs, tabHistory } from "./State"; export class Tab { - public key: string; - public scroll: number = 0; - - public viewState: editor.ICodeEditorViewState | null = null; - public model: editor.ITextModel | null = null; - - public constructor(key: string) { - this.key = key; + public key: string; + public scroll: number = 0; + + public viewState: editor.ICodeEditorViewState | null = null; + public model: editor.ITextModel | null = null; + + public constructor(key: string) { + this.key = key; + } + + isCachedModelEqualTo(model: editor.ITextModel): boolean { + if (this.model === null || this.model.isDisposed()) return false; + if (model === null || model.isDisposed()) return false; + if (this.model.getLanguageId() !== model.getLanguageId()) return false; + if (this.model.getLineCount() !== model.getLineCount()) return false; + + for (let i = 1; i <= this.model.getLineCount(); i++) { + if (this.model.getLineContent(i) !== model.getLineContent(i)) { + return false; + } } - isCachedModelEqualTo(model: editor.ITextModel): boolean { - if (this.model === null || this.model.isDisposed()) return false; - if (model === null || model.isDisposed()) return false; - if (this.model.getLanguageId() !== model.getLanguageId()) return false; - if (this.model.getLineCount() !== model.getLineCount()) return false; - - for (let i = 1; i <= this.model.getLineCount(); i++) { - if (this.model.getLineContent(i) !== model.getLineContent(i)) { - return false; - } - } - - return true; - } + return true; + } - cacheView( - viewState: editor.ICodeEditorViewState | null, - model: editor.ITextModel | null - ) { - this.viewState = viewState; - this.model = model; - } + cacheView(viewState: editor.ICodeEditorViewState | null, model: editor.ITextModel | null) { + this.viewState = viewState; + this.model = model; + } - invalidateCachedView() { - this.viewState = null; + invalidateCachedView() { + this.viewState = null; - if (!this.model) return; - this.model.dispose(); - this.model = null; - } + if (!this.model) return; + this.model.dispose(); + this.model = null; + } - applyViewToEditor(editor: editor.IStandaloneCodeEditor) { - if (!this.model) return; - editor.setModel(this.model); - if (this.viewState) editor.restoreViewState(this.viewState); - } + applyViewToEditor(editor: editor.IStandaloneCodeEditor) { + if (!this.model) return; + editor.setModel(this.model); + if (this.viewState) editor.restoreViewState(this.viewState); + } } -export const getOpenTab = (): (Tab | null) => { - return openTabs.value.find(o => o.key === selectedFile.value) || null; +export const getOpenTab = (): Tab | null => { + return openTabs.value.find((o) => o.key === selectedFile.value) || null; }; export const openTab = (key: string) => { - if (!enableTabs.value) { - selectedFile.next(key); + if (!enableTabs.value) { + selectedFile.next(key); - const currentTab = openTabs.value[0]; - if (currentTab && currentTab.key !== key) { - currentTab.invalidateCachedView(); - openTabs.next([new Tab(key)]); - } - - return; + const currentTab = openTabs.value[0]; + if (currentTab && currentTab.key !== key) { + currentTab.invalidateCachedView(); + openTabs.next([new Tab(key)]); } - const tabs = [...openTabs.value]; - const activeIndex = tabs.findIndex(tab => tab.key === selectedFile.value); + return; + } - // If class is not already open, open it - if (!tabs.some(tab => tab.key === key)) { - const insertIndex = activeIndex >= 0 ? activeIndex + 1 : tabs.length; - tabs.splice(insertIndex, 0, new Tab(key)); - openTabs.next(tabs); - } + const tabs = [...openTabs.value]; + const activeIndex = tabs.findIndex((tab) => tab.key === selectedFile.value); + + // If class is not already open, open it + if (!tabs.some((tab) => tab.key === key)) { + const insertIndex = activeIndex >= 0 ? activeIndex + 1 : tabs.length; + tabs.splice(insertIndex, 0, new Tab(key)); + openTabs.next(tabs); + } - // Switch to the newly opened tab, if not already open to the right class - if (selectedFile.value !== key) { - selectedFile.next(key); + // Switch to the newly opened tab, if not already open to the right class + if (selectedFile.value !== key) { + selectedFile.next(key); - if (tabHistory.value.length < 50) { - // Limit history to 50 - tabHistory.next([...tabHistory.value, key]); - } + if (tabHistory.value.length < 50) { + // Limit history to 50 + tabHistory.next([...tabHistory.value, key]); } + } }; export const closeTab = (key: string) => { - if (openTabs.value.length <= 1) return; + if (openTabs.value.length <= 1) return; - const tab = openTabs.value.find(o => o.key === key); + const tab = openTabs.value.find((o) => o.key === key); - tab?.invalidateCachedView(); - tabHistory.next(tabHistory.value.filter(v => v != key)); - const modifiedOpenTabs = openTabs.value.filter(v => v.key != key); + tab?.invalidateCachedView(); + tabHistory.next(tabHistory.value.filter((v) => v != key)); + const modifiedOpenTabs = openTabs.value.filter((v) => v.key != key); - if (key === selectedFile.value) { - const history = [...tabHistory.value]; - let newKey = history.pop(); - tabHistory.next(history); + if (key === selectedFile.value) { + const history = [...tabHistory.value]; + let newKey = history.pop(); + tabHistory.next(history); - if (!newKey) { - // If undefined, open tab left of it - let i = openTabs.value.findIndex(tab => tab.key === key) - 1; - i = Math.max(i, 0); - i = Math.min(i, modifiedOpenTabs.length - 1); - newKey = modifiedOpenTabs[i].key; - } - - openTab(newKey); + if (!newKey) { + // If undefined, open tab left of it + let i = openTabs.value.findIndex((tab) => tab.key === key) - 1; + i = Math.max(i, 0); + i = Math.min(i, modifiedOpenTabs.length - 1); + newKey = modifiedOpenTabs[i].key; } - openTabs.next(modifiedOpenTabs); + openTab(newKey); + } + + openTabs.next(modifiedOpenTabs); }; export const setTabPosition = (key: string, placeIndex: number) => { - const tabs = [...openTabs.value]; - const currentIndex = tabs.findIndex(tab => tab.key === key); - if (currentIndex === -1) return; - const currentTab = tabs[currentIndex]; + const tabs = [...openTabs.value]; + const currentIndex = tabs.findIndex((tab) => tab.key === key); + if (currentIndex === -1) return; + const currentTab = tabs[currentIndex]; - tabs.splice(currentIndex, 1); + tabs.splice(currentIndex, 1); - // Adjust index if moving right - let index = placeIndex; - if (placeIndex > currentIndex) index -= 1; + // Adjust index if moving right + let index = placeIndex; + if (placeIndex > currentIndex) index -= 1; - tabs.splice(index, 0, currentTab); - openTabs.next(tabs); + tabs.splice(index, 0, currentTab); + openTabs.next(tabs); }; export const closeOtherTabs = (key: string) => { - const tab = openTabs.value.find(tab => tab.key === key); - if (!tab) return; + const tab = openTabs.value.find((tab) => tab.key === key); + if (!tab) return; - // Invalidate all tabs except the one being kept - openTabs.value.forEach(t => { - if (t.key !== key) t.invalidateCachedView(); - }); + // Invalidate all tabs except the one being kept + openTabs.value.forEach((t) => { + if (t.key !== key) t.invalidateCachedView(); + }); - openTabs.next([tab]); - tabHistory.next([key]); + openTabs.next([tab]); + tabHistory.next([key]); - if (selectedFile.value !== key) { - selectedFile.next(key); - } + if (selectedFile.value !== key) { + selectedFile.next(key); + } }; diff --git a/src/logic/Tokens.ts b/src/logic/Tokens.ts index 1908fb1..0ec0d0e 100644 --- a/src/logic/Tokens.ts +++ b/src/logic/Tokens.ts @@ -1,41 +1,41 @@ import type { DecompileResult } from "../workers/decompile/types.ts"; -export type TokenType = 'class' | 'field' | 'method' | 'parameter' | 'local'; +export type TokenType = "class" | "field" | "method" | "parameter" | "local"; interface BaseToken { - // The number of characters from the start of the source - start: number; - // The length of the token in characters - length: number; - // The name of the class this token represents - className: string; - // Whether this token is a declaration or a reference - declaration: boolean; + // The number of characters from the start of the source + start: number; + // The length of the token in characters + length: number; + // The name of the class this token represents + className: string; + // Whether this token is a declaration or a reference + declaration: boolean; } export interface MemberToken extends BaseToken { - type: 'field' | 'method'; - // The member name - name: string; - // The member descriptor - descriptor: string; + type: "field" | "method"; + // The member name + name: string; + // The member descriptor + descriptor: string; } interface NonMethodToken extends BaseToken { - type: 'class' | 'parameter' | 'local'; + type: "class" | "parameter" | "local"; } export type Token = MemberToken | NonMethodToken; export interface TokenLocation { - line: number, - column: number; - length: number; + line: number; + column: number; + length: number; } export function getTokenLocation(result: DecompileResult, token: Token): TokenLocation { - const sourceUpTo = result.source.slice(0, token.start); - const line = sourceUpTo.match(/\n/g)!.length + 1; - const column = sourceUpTo.length - sourceUpTo.lastIndexOf("\n"); - return { line, column, length: token.length }; + const sourceUpTo = result.source.slice(0, token.start); + const line = sourceUpTo.match(/\n/g)!.length + 1; + const column = sourceUpTo.length - sourceUpTo.lastIndexOf("\n"); + return { line, column, length: token.length }; } diff --git a/src/logic/vf.ts b/src/logic/vf.ts index adc596e..3fd5bd4 100644 --- a/src/logic/vf.ts +++ b/src/logic/vf.ts @@ -8,29 +8,32 @@ let runtime: typeof vf | null = null; let runtimePreferWasm = true; export async function loadRuntime(preferWasm: boolean) { - if (!runtime || runtimePreferWasm !== preferWasm) { - runtimePreferWasm = preferWasm; - console.log(`Loading VineFlower ${preferWasm ? "WASM" : "JavaScript"} runtime`); + if (!runtime || runtimePreferWasm !== preferWasm) { + runtimePreferWasm = preferWasm; + console.log(`Loading VineFlower ${preferWasm ? "WASM" : "JavaScript"} runtime`); - let loadJs = !preferWasm; - if (preferWasm) { - try { - const { exports } = await load(wasmPath, { noAutoImports: true }); - runtime = exports; - loadJs = false; - } catch (e) { - console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); - loadJs = true; - } - } + let loadJs = !preferWasm; + if (preferWasm) { + try { + const { exports } = await load(wasmPath, { noAutoImports: true }); + runtime = exports; + loadJs = false; + } catch (e) { + console.warn( + "Failed to load WASM module (non-compliant browser?), falling back to JS implementation", + e, + ); + loadJs = true; + } + } - if (loadJs) { - runtime = await import("@run-slicer/vf/vf.runtime.js"); - } + if (loadJs) { + runtime = await import("@run-slicer/vf/vf.runtime.js"); } + } } export const decompile: typeof vf.decompile = async (name, options) => { - if (!runtime) throw "No runtime loaded"; - return await runtime.decompile(name, options); + if (!runtime) throw "No runtime loaded"; + return await runtime.decompile(name, options); }; diff --git a/src/main.tsx b/src/main.tsx index a50f324..3f6c56e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,8 +1,8 @@ -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import * as monaco from 'monaco-editor'; -import { loader } from '@monaco-editor/react'; -import App from './ui/App.tsx'; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import * as monaco from "monaco-editor"; +import { loader } from "@monaco-editor/react"; +import App from "./ui/App.tsx"; import "./index.css"; import MonacoWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker"; @@ -11,13 +11,13 @@ import MonacoWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker"; loader.config({ monaco }); globalThis.MonacoEnvironment = { - getWorker() { - return new MonacoWorker(); - } + getWorker() { + return new MonacoWorker(); + }, }; -createRoot(document.getElementById('root')!).render( - - - , +createRoot(document.getElementById("root")!).render( + + + , ); diff --git a/src/site.ts b/src/site.ts index 666b49a..c168b9a 100644 --- a/src/site.ts +++ b/src/site.ts @@ -1 +1 @@ -export const IS_JAVADOC_EDITOR = import.meta.env.VITE_JAVADOC_EDITOR === 'true'; \ No newline at end of file +export const IS_JAVADOC_EDITOR = import.meta.env.VITE_JAVADOC_EDITOR === "true"; diff --git a/src/types/java-indexer.d.ts b/src/types/java-indexer.d.ts index 58fe9c3..0159cd2 100644 --- a/src/types/java-indexer.d.ts +++ b/src/types/java-indexer.d.ts @@ -1,9 +1,9 @@ // Type declarations for TeaVM-generated Java indexer modules declare module "*/java.js" { - export function index(data: ArrayBufferLike): void; - export function getReference(key: string): string[]; - export function getReferenceSize(): number; - export function getBytecode(classData: ArrayBufferLike[]): string; - export function getClassData(): string[]; + export function index(data: ArrayBufferLike): void; + export function getReference(key: string): string[]; + export function getReferenceSize(): number; + export function getBytecode(classData: ArrayBufferLike[]): string; + export function getClassData(): string[]; } diff --git a/src/types/vf-runtime.d.ts b/src/types/vf-runtime.d.ts index 00f472f..2659995 100644 --- a/src/types/vf-runtime.d.ts +++ b/src/types/vf-runtime.d.ts @@ -1,10 +1,10 @@ declare module "@run-slicer/vf/vf.wasm-runtime.js" { - export function load( - wasmPath: string, - options?: { noAutoImports?: boolean; } - ): Promise<{ exports: typeof import("@run-slicer/vf"); }>; + export function load( + wasmPath: string, + options?: { noAutoImports?: boolean }, + ): Promise<{ exports: typeof import("@run-slicer/vf") }>; } declare module "@run-slicer/vf/vf.runtime.js" { - export * from "@run-slicer/vf"; + export * from "@run-slicer/vf"; } diff --git a/src/ui/AboutModal.tsx b/src/ui/AboutModal.tsx index bdcaacc..20a75c4 100644 --- a/src/ui/AboutModal.tsx +++ b/src/ui/AboutModal.tsx @@ -1,74 +1,91 @@ import { Button, Checkbox, Modal } from "antd"; import { useState } from "react"; import { agreedEula } from "../logic/Settings"; -import { InfoCircleOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined } from "@ant-design/icons"; import { useObservable } from "../utils/UseObservable"; import { BehaviorSubject } from "rxjs"; export const aboutModalOpen = new BehaviorSubject(false); export const AboutModalButton = () => { - return ( - - ); + return ( + + ); }; const AboutModal = () => { - const accepted = useObservable(agreedEula.observable); - const isModalOpen = useObservable(aboutModalOpen); + const accepted = useObservable(agreedEula.observable); + const isModalOpen = useObservable(aboutModalOpen); - // Open modal automatically if EULA not accepted - useState(() => { - if (!agreedEula.value) { - aboutModalOpen.next(true); - } - }); + // Open modal automatically if EULA not accepted + useState(() => { + if (!agreedEula.value) { + aboutModalOpen.next(true); + } + }); - const handleCancel = () => { - if (!accepted) { - return; - } + const handleCancel = () => { + if (!accepted) { + return; + } - aboutModalOpen.next(false); - }; + aboutModalOpen.next(false); + }; - return ( - -

NOTE! This website is not redistributing any Minecraft code or compiled bytecode. The minecraft jar is downloaded directly from Mojang's servers to your device when you use this tool. Check your browser's network requests!

-

The Vineflower decompiler is used after being compiled to wasm as part of the @run-slicer/vf project.

+ return ( + +

+ NOTE! This website is not redistributing any Minecraft code or compiled bytecode. The + minecraft jar is downloaded directly from Mojang's servers to your device when you use this + tool. Check your browser's network requests! +

+

+ The Vineflower decompiler is used + after being compiled to wasm as part of the{" "} + @run-slicer/vf project. +

-

GitHub

+

+ GitHub +

- aboutModalOpen.next(false)} /> -
- ); + aboutModalOpen.next(false)} /> +
+ ); }; -const Eula = ({ onAccept }: { onAccept: () => void; }) => { - const accepted = useObservable(agreedEula.observable); +const Eula = ({ onAccept }: { onAccept: () => void }) => { + const accepted = useObservable(agreedEula.observable); - if (accepted) { - return <>; - } + if (accepted) { + return <>; + } - return ( - { - agreedEula.value = e.target.checked; - if (e.target.checked) { - onAccept(); - } - }}> - I agree to the Minecraft EULA before using this website. - ); + return ( + { + agreedEula.value = e.target.checked; + if (e.target.checked) { + onAccept(); + } + }} + > + I agree to the Minecraft{" "} + + EULA + {" "} + before using this website. + + ); }; - export default AboutModal; diff --git a/src/ui/App.tsx b/src/ui/App.tsx index fac082e..4fac1ff 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,104 +1,116 @@ -import { Button, ConfigProvider, Drawer, Flex, Splitter, theme } from 'antd'; +import { Button, ConfigProvider, Drawer, Flex, Splitter, theme } from "antd"; import Code from "./Code.tsx"; -import SideBar from './SideBar.tsx'; -import { useState } from 'react'; -import { useObservable } from '../utils/UseObservable.ts'; -import { isThin } from '../logic/Browser.ts'; -import { diffView, mobileDrawerOpen } from '../logic/State'; -import DiffView from './diff/DiffView.tsx'; -import { FilepathHeader } from './FilepathHeader.tsx'; -import { enableTabs } from '../logic/Settings.ts'; -import { MenuFoldOutlined } from '@ant-design/icons'; -import { TabsComponent } from './TabsComponent.tsx'; -import Modals from './Modals.tsx'; +import SideBar from "./SideBar.tsx"; +import { useState } from "react"; +import { useObservable } from "../utils/UseObservable.ts"; +import { isThin } from "../logic/Browser.ts"; +import { diffView, mobileDrawerOpen } from "../logic/State"; +import DiffView from "./diff/DiffView.tsx"; +import { FilepathHeader } from "./FilepathHeader.tsx"; +import { enableTabs } from "../logic/Settings.ts"; +import { MenuFoldOutlined } from "@ant-design/icons"; +import { TabsComponent } from "./TabsComponent.tsx"; +import Modals from "./Modals.tsx"; const App = () => { - const isSmall = useObservable(isThin); - const enableDiff = useObservable(diffView); + const isSmall = useObservable(isThin); + const enableDiff = useObservable(diffView); - return ( - - - {enableDiff ? : isSmall ? : } - - ); + return ( + + + {enableDiff ? : isSmall ? : } + + ); }; const LargeApp = () => { - const [sizes, setSizes] = useState<(number | string)[]>(['25%', '75%']); - const tabsEnabled = useObservable(enableTabs.observable); + const [sizes, setSizes] = useState<(number | string)[]>(["25%", "75%"]); + const tabsEnabled = useObservable(enableTabs.observable); - return ( - - - - - - - {tabsEnabled && } - -
-
-
-
- ); + return ( + + + + + + + {tabsEnabled && } + +
+ +
+
+
+
+ ); }; const MobileApp = () => { - const open = useObservable(mobileDrawerOpen); - const tabsEnabled = useObservable(enableTabs.observable); + const open = useObservable(mobileDrawerOpen); + const tabsEnabled = useObservable(enableTabs.observable); - const showDrawer = () => { - mobileDrawerOpen.next(true); - }; + const showDrawer = () => { + mobileDrawerOpen.next(true); + }; - const onClose = () => { - mobileDrawerOpen.next(false); - }; + const onClose = () => { + mobileDrawerOpen.next(false); + }; - return ( - - - - - - + ); export const JarDecompilerModal = () => { - const jar = useObservable(minecraftJar); - const isModalOpen = useObservable(modalOpen); + const jar = useObservable(minecraftJar); + const isModalOpen = useObservable(modalOpen); - const [messageApi, messageCtx] = message.useMessage(); - const [modalApi, modalCtx] = Modal.useModal(); + const [messageApi, messageCtx] = message.useMessage(); + const [modalApi, modalCtx] = Modal.useModal(); - const onOk = () => { - modalOpen.next(false); - if (!jar) return; + const onOk = () => { + modalOpen.next(false); + if (!jar) return; - const task = decompileEntireJar(jar.jar, { - threads: decompilerThreads.value, - splits: decompilerSplits.value, - logger(progress, current, total) { - progressSubject.next([progress, current, total]); - }, - }); + const task = decompileEntireJar(jar.jar, { + threads: decompilerThreads.value, + splits: decompilerSplits.value, + logger(progress, current, total) { + progressSubject.next([progress, current, total]); + }, + }); - const start = performance.now(); - taskSubject.next(task); - void task.start().then((total) => { - const elapsed = (performance.now() - start) / 1000; - modalApi.info({ - bodyProps: { "data-testid": "jar-decompiler-result" }, - content: `Decompiled ${total} new classes in ${elapsed.toFixed(3)} s.`, - closable: true, - keyboard: true, - mask: { closable: true }, - }); - }).finally(() => { - taskSubject.next(undefined); - progressSubject.next(undefined); + const start = performance.now(); + taskSubject.next(task); + void task + .start() + .then((total) => { + const elapsed = (performance.now() - start) / 1000; + modalApi.info({ + bodyProps: { "data-testid": "jar-decompiler-result" }, + content: `Decompiled ${total} new classes in ${elapsed.toFixed(3)} s.`, + closable: true, + keyboard: true, + mask: { closable: true }, }); - }; - - const clearCache = () => { - if (!jar) return; - void deleteCache().then(c => messageApi.open({ type: "success", content: `Deleted ${c} clasess from cache.` })); - }; - - return ( - modalOpen.next(false)} - onOk={onOk} - okButtonProps={{ "data-testid": "jar-decompiler-ok" }} - > - {messageCtx} - {modalCtx} - -
-
- - - - - - - - - + }) + .finally(() => { + taskSubject.next(undefined); + progressSubject.next(undefined); + }); + }; -
+ const clearCache = () => { + if (!jar) return; + void deleteCache().then((c) => + messageApi.open({ type: "success", content: `Deleted ${c} clasess from cache.` }), ); + }; + + return ( + modalOpen.next(false)} + onOk={onOk} + okButtonProps={{ "data-testid": "jar-decompiler-ok" }} + > + {messageCtx} + {modalCtx} + +
+
+ + + + + + + + + +
+ ); }; const progressSubject = new BehaviorSubject<[string, number, number] | undefined>(undefined); const taskSubject = new BehaviorSubject(undefined); export const JarDecompilerProgressModal = () => { - const [text, current, total] = useObservable(progressSubject) ?? []; - const task = useObservable(taskSubject); + const [text, current, total] = useObservable(progressSubject) ?? []; + const task = useObservable(taskSubject); - const percent = (current ?? 0) / (total ?? 1) * 100; + const percent = ((current ?? 0) / (total ?? 1)) * 100; - return ( - { - if (task) task.stop(); - taskSubject.next(undefined); - }} - okText={task ? "Stop" : "Stopping..."} - footer={(_, { OkBtn }) => ( - - )} + return ( + { + if (task) task.stop(); + taskSubject.next(undefined); + }} + okText={task ? "Stop" : "Stopping..."} + footer={(_, { OkBtn }) => } + > + +
- -
- {text} -
- `${current}/${total}`} /> -
- - ); + {text} +
+ `${current}/${total}`} /> +
+
+ ); }; diff --git a/src/ui/Modals.tsx b/src/ui/Modals.tsx index 296246e..d63a130 100644 --- a/src/ui/Modals.tsx +++ b/src/ui/Modals.tsx @@ -9,20 +9,20 @@ import { JarDecompilerModal, JarDecompilerProgressModal } from "./JarDecompilerM import IndexProgressNotification from "./IndexProgressNotification"; const Modals = () => { - return ( - <> - - - - - - - - - - - - ); + return ( + <> + + + + + + + + + + + + ); }; export default Modals; diff --git a/src/ui/ProgressModal.tsx b/src/ui/ProgressModal.tsx index b07557e..c98e296 100644 --- a/src/ui/ProgressModal.tsx +++ b/src/ui/ProgressModal.tsx @@ -3,18 +3,18 @@ import { downloadProgress } from "../logic/MinecraftApi"; import { useObservable } from "../utils/UseObservable"; const ProcesModal = () => { - const progress = useObservable(downloadProgress); + const progress = useObservable(downloadProgress); - return ( - - - - ); + return ( + + + + ); }; -export default ProcesModal; \ No newline at end of file +export default ProcesModal; diff --git a/src/ui/ReferenceResults.tsx b/src/ui/ReferenceResults.tsx index 2e3f0d7..9e9f5ea 100644 --- a/src/ui/ReferenceResults.tsx +++ b/src/ui/ReferenceResults.tsx @@ -6,94 +6,96 @@ import { openTab } from "../logic/Tabs"; import { referencesQuery } from "../logic/State"; function getUsageClass(usage: ReferenceString): string { - if (usage.startsWith("m:") || usage.startsWith("f:")) { - const parts = usage.slice(2).split(":"); - return parts[0]; - } + if (usage.startsWith("m:") || usage.startsWith("f:")) { + const parts = usage.slice(2).split(":"); + return parts[0]; + } - // class usage - return usage; + // class usage + return usage; } interface ReferenceGroup { - className: string; - references: ReferenceString[]; + className: string; + references: ReferenceString[]; } const groupedResults: Observable = referenceResults.pipe( - map(results => { - const groups: Record = {}; + map((results) => { + const groups: Record = {}; - for (const usage of results) { - const className = getUsageClass(usage); - if (!groups[className]) { - groups[className] = []; - } - groups[className].push(usage); - } + for (const usage of results) { + const className = getUsageClass(usage); + if (!groups[className]) { + groups[className] = []; + } + groups[className].push(usage); + } - return Object.entries(groups).map(([className, references]) => ({ - className, - references - })); - }) + return Object.entries(groups).map(([className, references]) => ({ + className, + references, + })); + }), ); interface UsageGroupItemProps { - group: ReferenceGroup; + group: ReferenceGroup; } const UsageGroupItem = ({ group }: UsageGroupItemProps) => { - const query = useObservable(referencesQuery)!; + const query = useObservable(referencesQuery)!; - return ( -
-
openTab(group.className + ".class")} - style={{ - cursor: "pointer", - fontSize: "13px", - fontWeight: "bold", - transition: "background-color 0.2s", - borderRadius: "4px" - }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} - > - {group.className} -
-
- {group.references.map((reference, index) => ( -
goToReference(query, reference)} - style={{ - cursor: "pointer", - fontSize: "12px", - transition: "background-color 0.2s", - color: "rgba(255, 255, 255, 0.7)" - }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.05)'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} - > - {formatReference(reference)} -
- ))} -
-
- ); + return ( +
+
openTab(group.className + ".class")} + style={{ + cursor: "pointer", + fontSize: "13px", + fontWeight: "bold", + transition: "background-color 0.2s", + borderRadius: "4px", + }} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)")} + onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + > + {group.className} +
+
+ {group.references.map((reference, index) => ( +
goToReference(query, reference)} + style={{ + cursor: "pointer", + fontSize: "12px", + transition: "background-color 0.2s", + color: "rgba(255, 255, 255, 0.7)", + }} + onMouseEnter={(e) => + (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.05)") + } + onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + > + {formatReference(reference)} +
+ ))} +
+
+ ); }; const UsageResults = () => { - const results = useObservable(groupedResults) || []; + const results = useObservable(groupedResults) || []; - return ( -
- {results.map((group, index) => ( - - ))} -
- ); + return ( +
+ {results.map((group, index) => ( + + ))} +
+ ); }; -export default UsageResults; \ No newline at end of file +export default UsageResults; diff --git a/src/ui/SearchResults.tsx b/src/ui/SearchResults.tsx index a9c8fc9..36e9435 100644 --- a/src/ui/SearchResults.tsx +++ b/src/ui/SearchResults.tsx @@ -4,29 +4,29 @@ import { useObservable } from "../utils/UseObservable"; import { openTab } from "../logic/Tabs"; const SearchResults = () => { - const results = useObservable(searchResults); + const results = useObservable(searchResults); - return ( - ( - openTab(item)} - style={{ - cursor: "pointer", - padding: "2px 8px", - fontSize: "12px", - transition: "background-color 0.2s" - }} - onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'} - onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'} - > - {item.replace(/\.class$/, '')} - - )} - /> - ); + return ( + ( + openTab(item)} + style={{ + cursor: "pointer", + padding: "2px 8px", + fontSize: "12px", + transition: "background-color 0.2s", + }} + onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)")} + onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")} + > + {item.replace(/\.class$/, "")} + + )} + /> + ); }; -export default SearchResults; \ No newline at end of file +export default SearchResults; diff --git a/src/ui/SettingsModal.tsx b/src/ui/SettingsModal.tsx index 4c778ae..53aa9f4 100644 --- a/src/ui/SettingsModal.tsx +++ b/src/ui/SettingsModal.tsx @@ -1,8 +1,28 @@ -import { Button, Modal, type CheckboxProps, Form, Tooltip, InputNumber, type InputNumberProps } from "antd"; -import { SettingOutlined } from '@ant-design/icons'; -import { Checkbox } from 'antd'; +import { + Button, + Modal, + type CheckboxProps, + Form, + Tooltip, + InputNumber, + type InputNumberProps, +} from "antd"; +import { SettingOutlined } from "@ant-design/icons"; +import { Checkbox } from "antd"; import { useObservable } from "../utils/UseObservable"; -import { BooleanSetting, enableTabs, displayLambdas, focusSearch, KeybindSetting, type KeybindValue, bytecode, showStructure, NumberSetting, preferWasmDecompiler, compactPackages } from "../logic/Settings"; +import { + BooleanSetting, + enableTabs, + displayLambdas, + focusSearch, + KeybindSetting, + type KeybindValue, + bytecode, + showStructure, + NumberSetting, + preferWasmDecompiler, + compactPackages, +} from "../logic/Settings"; import { capturingKeybind, rawKeydownEvent } from "../logic/Keybinds"; import { BehaviorSubject } from "rxjs"; import type React from "react"; @@ -10,135 +30,159 @@ import type React from "react"; export const settingsModalOpen = new BehaviorSubject(false); export const SettingsModalButton = () => { - return ( - - ); + return ( + + ); }; const SettingsModal = () => { - const isModalOpen = useObservable(settingsModalOpen); - const displayLambdasValue = useObservable(displayLambdas.observable); - const bytecodeValue = useObservable(bytecode.observable); - - return ( - settingsModalOpen.next(false)} - footer={null} - > -
- - - - - - - - -
- ); + const isModalOpen = useObservable(settingsModalOpen); + const displayLambdasValue = useObservable(displayLambdas.observable); + const bytecodeValue = useObservable(bytecode.observable); + + return ( + settingsModalOpen.next(false)} + footer={null} + > +
+ + + + + + + + +
+ ); }; export interface BooleanOptionProps { - setting: BooleanSetting; - title: string; - tooltip?: string; - disabled?: boolean; + setting: BooleanSetting; + title: string; + tooltip?: string; + disabled?: boolean; } -export const BooleanOption: React.FC = ({ setting, title, tooltip, disabled }) => { - const value = useObservable(setting.observable); - const onChange: CheckboxProps['onChange'] = (e) => { - setting.value = e.target.checked; - }; - - const checkbox = ; - - return ( - - {tooltip ? {checkbox} : checkbox} - - ); +export const BooleanOption: React.FC = ({ + setting, + title, + tooltip, + disabled, +}) => { + const value = useObservable(setting.observable); + const onChange: CheckboxProps["onChange"] = (e) => { + setting.value = e.target.checked; + }; + + const checkbox = ; + + return ( + + {tooltip ? {checkbox} : checkbox} + + ); }; export interface NumberOptionProps { - setting: NumberSetting; - title: string; - min?: number; - max?: number; - testid?: string; + setting: NumberSetting; + title: string; + min?: number; + max?: number; + testid?: string; } -export const NumberOption: React.FC = ({ setting, title, min, max, testid}) => { - const value = useObservable(setting.observable); - const onChange: InputNumberProps["onChange"] = (e) => { - setting.value = e ?? setting.defaultValue; - } - - return ( - - - - ); -} +export const NumberOption: React.FC = ({ setting, title, min, max, testid }) => { + const value = useObservable(setting.observable); + const onChange: InputNumberProps["onChange"] = (e) => { + setting.value = e ?? setting.defaultValue; + }; + + return ( + + + + ); +}; interface KeybindOptionProps { - setting: KeybindSetting; - title: string; - captureId: string; + setting: KeybindSetting; + title: string; + captureId: string; } const KeybindOption: React.FC = ({ setting, title, captureId }) => { - const value = useObservable(setting.observable); - const capturing = useObservable(capturingKeybind); - const isCapturing = capturing === captureId; - - const startCapture = () => { - if (capturingKeybind.value !== null) { - return; - } - capturingKeybind.next(captureId); - const subscription = rawKeydownEvent.subscribe((event) => { - event.preventDefault(); - - // Only capture if a non-modifier key is pressed - const modifierKeys = ['Control', 'Alt', 'Shift', 'Meta']; - if (!modifierKeys.includes(event.key)) { - setting.setFromEvent(event); - capturingKeybind.next(null); - subscription.unsubscribe(); - } - }); - }; - - const formatKeybind = (keybind: KeybindValue | undefined): string => { - if (!keybind) return 'Not set'; - return keybind.split('+').map(k => { - if (k == ' ') return ''; - const key = k.trim(); - return key.charAt(0).toUpperCase() + key.slice(1); - }).join('+'); - }; - - return ( - - - - - ); + const value = useObservable(setting.observable); + const capturing = useObservable(capturingKeybind); + const isCapturing = capturing === captureId; + + const startCapture = () => { + if (capturingKeybind.value !== null) { + return; + } + capturingKeybind.next(captureId); + const subscription = rawKeydownEvent.subscribe((event) => { + event.preventDefault(); + + // Only capture if a non-modifier key is pressed + const modifierKeys = ["Control", "Alt", "Shift", "Meta"]; + if (!modifierKeys.includes(event.key)) { + setting.setFromEvent(event); + capturingKeybind.next(null); + subscription.unsubscribe(); + } + }); + }; + + const formatKeybind = (keybind: KeybindValue | undefined): string => { + if (!keybind) return "Not set"; + return keybind + .split("+") + .map((k) => { + if (k == " ") return ""; + const key = k.trim(); + return key.charAt(0).toUpperCase() + key.slice(1); + }) + .join("+"); + }; + + return ( + + + + + ); }; export default SettingsModal; diff --git a/src/ui/SideBar.tsx b/src/ui/SideBar.tsx index a91538a..e18adbf 100644 --- a/src/ui/SideBar.tsx +++ b/src/ui/SideBar.tsx @@ -15,66 +15,71 @@ import { searchQuery, referencesQuery } from "../logic/State"; const { Search } = Input; const SideBar = () => { - const showReference = useObservable(isViewingReferences); - const currentReferenceQuery = useObservable(referencesQuery); - const focusSearch = useObservable(focusSearchEvent); - const searchRef = useRef(null); + const showReference = useObservable(isViewingReferences); + const currentReferenceQuery = useObservable(referencesQuery); + const focusSearch = useObservable(focusSearchEvent); + const searchRef = useRef(null); - useEffect(() => { - if (focusSearch) { - referencesQuery.next(""); - searchRef?.current?.focus(); - } - }, [focusSearch]); + useEffect(() => { + if (focusSearch) { + referencesQuery.next(""); + searchRef?.current?.focus(); + } + }, [focusSearch]); - useEffect(() => { - if (focusSearch && !showReference) { - searchRef?.current?.focus(); - } - }, [focusSearch, showReference]); + useEffect(() => { + if (focusSearch && !showReference) { + searchRef?.current?.focus(); + } + }, [focusSearch, showReference]); - const onChange: SearchProps['onChange'] = (e) => { - searchQuery.next(e.target.value); - }; + const onChange: SearchProps["onChange"] = (e) => { + searchQuery.next(e.target.value); + }; - const onBackClick = () => { - referencesQuery.next(""); - }; + const onBackClick = () => { + referencesQuery.next(""); + }; - return ( - -
- {showReference ? ( - <> - -
- References of: {formatReferenceQuery(currentReferenceQuery || "")} -
- - ) : ( - - )} - -
- -
- - ); + return ( + +
+ {showReference ? ( + <> + +
+ References of: {formatReferenceQuery(currentReferenceQuery || "")} +
+ + ) : ( + + )} + +
+ +
+ + ); }; const FileListOrSearchResults = () => { - const showSearchResults = useObservable(isSearching); - const showReference = useObservable(isViewingReferences); + const showSearchResults = useObservable(isSearching); + const showReference = useObservable(isViewingReferences); - if (showReference) { - return ; - } else if (showSearchResults) { - return ; - } else { - return ; - } + if (showReference) { + return ; + } else if (showSearchResults) { + return ; + } else { + return ; + } }; export default SideBar; diff --git a/src/ui/StructureModal.tsx b/src/ui/StructureModal.tsx index e4e8f14..cca2e28 100644 --- a/src/ui/StructureModal.tsx +++ b/src/ui/StructureModal.tsx @@ -5,27 +5,27 @@ import { showStructureEvent } from "../logic/Keybinds"; import StructureView from "./StructureView"; const StructureModal = () => { - const showEvent = useObservable(showStructureEvent); - const [open, setOpen] = useState(false); + const showEvent = useObservable(showStructureEvent); + const [open, setOpen] = useState(false); - useEffect(() => { - if (showEvent) { - setOpen(true); - } - }, [showEvent]); + useEffect(() => { + if (showEvent) { + setOpen(true); + } + }, [showEvent]); - return ( - setOpen(false)} - footer={null} - width={"fit-content"} - styles={{ body: { maxHeight: "70vh", overflow: "auto" } }} - > - setOpen(false)} /> - - ); + return ( + setOpen(false)} + footer={null} + width={"fit-content"} + styles={{ body: { maxHeight: "70vh", overflow: "auto" } }} + > + setOpen(false)} /> + + ); }; -export default StructureModal; \ No newline at end of file +export default StructureModal; diff --git a/src/ui/StructureView.tsx b/src/ui/StructureView.tsx index afbe881..fcaa312 100644 --- a/src/ui/StructureView.tsx +++ b/src/ui/StructureView.tsx @@ -7,113 +7,121 @@ import { parseDescriptor } from "./CodeHoverProvider"; import { getTokenLocation, type MemberToken, type Token } from "../logic/Tokens"; import { selectedLines } from "../logic/State"; -type StructureNode = TreeDataNode & { token?: Token; }; +type StructureNode = TreeDataNode & { token?: Token }; const formatClassDisplayName = (className: string) => { - const simpleName = className.split("/").pop(); - return simpleName || className; + const simpleName = className.split("/").pop(); + return simpleName || className; }; const formatMethodDisplayName = (token: MemberToken, classDisplayName: string) => { - const methodName = token.name === "" ? classDisplayName : token.name; - const signature = parseDescriptor(token.descriptor); - return `${methodName}${signature}`; + const methodName = token.name === "" ? classDisplayName : token.name; + const signature = parseDescriptor(token.descriptor); + return `${methodName}${signature}`; }; type StructureViewProps = { - onNavigate?: () => void; + onNavigate?: () => void; }; const StructureView = ({ onNavigate }: StructureViewProps) => { - const decompileResult = useObservable(currentResult); - - const { treeData, tokenByKey } = useMemo(() => { - const tokenMap = new Map(); - - if (!decompileResult || decompileResult.language !== "java") { - return { treeData: [] as StructureNode[], tokenByKey: tokenMap }; - } - - const classTokens = new Map(); - const methodsByClass = new Map(); - - for (const token of decompileResult.tokens) { - if (token.type === "class" && token.declaration) { - classTokens.set(token.className, token); - } - - if (token.type === "method" && token.declaration) { - const bucket = methodsByClass.get(token.className) || []; - bucket.push(token); - methodsByClass.set(token.className, bucket); - } - } - - const classes = Array.from(methodsByClass.keys()).sort(); - const nodes: StructureNode[] = classes.map((className) => { - const classDisplayName = formatClassDisplayName(className); - const classKey = `class:${className}`; - const classNode: StructureNode = { - key: classKey, - title: {classDisplayName}, - children: [] - }; - - const classToken = classTokens.get(className); - if (classToken) { - classNode.token = classToken; - tokenMap.set(classKey, classToken); - } - - const methods = methodsByClass.get(className) || []; - methods.sort((a, b) => a.start - b.start); - - classNode.children = methods.map((method) => { - const methodKey = `method:${className}:${method.start}`; - tokenMap.set(methodKey, method); - return { - key: methodKey, - title: {formatMethodDisplayName(method, classDisplayName)}, - isLeaf: true, - token: method - } satisfies StructureNode; - }); - - return classNode; - }); - - return { treeData: nodes, tokenByKey: tokenMap }; - }, [decompileResult]); - - const onSelect: TreeProps["onSelect"] = (_, info) => { - if (!decompileResult) return; - const node = info.node as StructureNode; - const token = node.token || (node.key ? tokenByKey.get(String(node.key)) : undefined); - if (!token) return; - - const location = getTokenLocation(decompileResult, token); - selectedLines.next({ line: location.line }); - onNavigate?.(); - }; + const decompileResult = useObservable(currentResult); + + const { treeData, tokenByKey } = useMemo(() => { + const tokenMap = new Map(); if (!decompileResult || decompileResult.language !== "java") { - return ; + return { treeData: [] as StructureNode[], tokenByKey: tokenMap }; } - if (treeData.length === 0) { - return ; + const classTokens = new Map(); + const methodsByClass = new Map(); + + for (const token of decompileResult.tokens) { + if (token.type === "class" && token.declaration) { + classTokens.set(token.className, token); + } + + if (token.type === "method" && token.declaration) { + const bucket = methodsByClass.get(token.className) || []; + bucket.push(token); + methodsByClass.set(token.className, bucket); + } } - return ( - - ); + const classes = Array.from(methodsByClass.keys()).sort(); + const nodes: StructureNode[] = classes.map((className) => { + const classDisplayName = formatClassDisplayName(className); + const classKey = `class:${className}`; + const classNode: StructureNode = { + key: classKey, + title: ( + + {classDisplayName} + + ), + children: [], + }; + + const classToken = classTokens.get(className); + if (classToken) { + classNode.token = classToken; + tokenMap.set(classKey, classToken); + } + + const methods = methodsByClass.get(className) || []; + methods.sort((a, b) => a.start - b.start); + + classNode.children = methods.map((method) => { + const methodKey = `method:${className}:${method.start}`; + tokenMap.set(methodKey, method); + return { + key: methodKey, + title: ( + + {formatMethodDisplayName(method, classDisplayName)} + + ), + isLeaf: true, + token: method, + } satisfies StructureNode; + }); + + return classNode; + }); + + return { treeData: nodes, tokenByKey: tokenMap }; + }, [decompileResult]); + + const onSelect: TreeProps["onSelect"] = (_, info) => { + if (!decompileResult) return; + const node = info.node as StructureNode; + const token = node.token || (node.key ? tokenByKey.get(String(node.key)) : undefined); + if (!token) return; + + const location = getTokenLocation(decompileResult, token); + selectedLines.next({ line: location.line }); + onNavigate?.(); + }; + + if (!decompileResult || decompileResult.language !== "java") { + return ; + } + + if (treeData.length === 0) { + return ; + } + + return ( + + ); }; -export default StructureView; \ No newline at end of file +export default StructureView; diff --git a/src/ui/TabsComponent.tsx b/src/ui/TabsComponent.tsx index 77a4249..48e2413 100644 --- a/src/ui/TabsComponent.tsx +++ b/src/ui/TabsComponent.tsx @@ -5,270 +5,274 @@ import React, { useEffect, useRef, useState } from "react"; import { selectedFile, openTabs } from "../logic/State"; export const TabsComponent = () => { - // variables - tabs - const activeKey = useObservable(selectedFile); - const tabs = useObservable(openTabs); - const tabRefs = useRef>({}); - - // variables - context menu - const [contextMenu, setContextMenu] = useState<{ x: number; y: number; key: string; } | null>(null); - - // variables - dragging - const draggingKey = useRef(""); - const [placeIndex, _setPlaceIndex] = useState(-1); // state to render border - const placeIndexRef = useRef(-1); // additional ref because mouseup and mousemove happen in same tick and would not be in sync - - // variables - dragging (mouse positioning) - const mouseMovementDelta = useRef(0); // tracks how far the mouse has moved - const lastMousePos = useRef({ x: -1, y: -1 }); - const threshold = 50; // mouse movement threshold after which tab dragging will activate - - // variables - tab ghost image - const ghostImage = useRef(null); - - // helpers - const getRects = () => { - return Object.entries(tabRefs.current).map(([k, el]) => { - const rect = el?.getBoundingClientRect(); - return ({ - key: k, - x: rect?.x ?? -1000, - width: rect?.width ?? 0 - }); - }); + // variables - tabs + const activeKey = useObservable(selectedFile); + const tabs = useObservable(openTabs); + const tabRefs = useRef>({}); + + // variables - context menu + const [contextMenu, setContextMenu] = useState<{ x: number; y: number; key: string } | null>( + null, + ); + + // variables - dragging + const draggingKey = useRef(""); + const [placeIndex, _setPlaceIndex] = useState(-1); // state to render border + const placeIndexRef = useRef(-1); // additional ref because mouseup and mousemove happen in same tick and would not be in sync + + // variables - dragging (mouse positioning) + const mouseMovementDelta = useRef(0); // tracks how far the mouse has moved + const lastMousePos = useRef({ x: -1, y: -1 }); + const threshold = 50; // mouse movement threshold after which tab dragging will activate + + // variables - tab ghost image + const ghostImage = useRef(null); + + // helpers + const getRects = () => { + return Object.entries(tabRefs.current).map(([k, el]) => { + const rect = el?.getBoundingClientRect(); + return { + key: k, + x: rect?.x ?? -1000, + width: rect?.width ?? 0, + }; + }); + }; + + const borderStyle = (key: string) => { + if (!tabs) return; + + const style = { + borderLeft: "2px solid transparent", + borderRight: "2px solid transparent", }; - const borderStyle = (key: string) => { - if (!tabs) return; - - const style = { - borderLeft: "2px solid transparent", - borderRight: "2px solid transparent" - }; - - const tabIndex = tabs.findIndex(tab => tab.key === key); - - if (tabIndex === placeIndex && mouseMovementDelta.current >= threshold) { - style.borderLeft = "2px solid white"; - return style; + const tabIndex = tabs.findIndex((tab) => tab.key === key); + + if (tabIndex === placeIndex && mouseMovementDelta.current >= threshold) { + style.borderLeft = "2px solid white"; + return style; + } + + if ( + tabIndex === tabs.length - 1 && + placeIndex > tabIndex && + mouseMovementDelta.current >= threshold + ) { + style.borderRight = "2px solid white"; + return style; + } + + return style; + }; + + const setGhostImage = (element: HTMLDivElement) => { + if (!tabRefs || !tabRefs.current) return; + + // Not the best, but it works :> + const ghost = document.createElement("div"); + ghost.textContent = element.textContent || ""; + ghost.style.position = "absolute"; + ghost.style.background = "black"; + ghost.style.color = "white"; + ghost.style.padding = ".5rem 1rem"; + ghost.style.whiteSpace = "nowrap"; + ghost.style.pointerEvents = "none"; + ghost.style.border = "1px solid #303030"; + ghost.style.borderRadius = "8px"; + ghost.style.visibility = "hidden"; + document.body.appendChild(ghost); + ghostImage.current = ghost; + }; + + const setPlaceIndexSync = (v: number) => { + placeIndexRef.current = v; + _setPlaceIndex(v); + }; + + // listeners + type TargetKey = React.MouseEvent | React.KeyboardEvent | string; + const onEdit = (targetKey: TargetKey, action: "add" | "remove") => { + if (action === "add") return; + if (!(typeof targetKey === "string")) return; + closeTab(targetKey); + }; + + const handleMouseDown = (e: React.MouseEvent, key: string) => { + // Middle click (button 1) closes the tab + if (e.button === 1) { + e.preventDefault(); + closeTab(key); + return; + } + + // Only proceed with drag for left click (button 0) + if (e.button !== 0) return; + + mouseMovementDelta.current = 0; + lastMousePos.current = { x: e.clientX, y: e.clientY }; + + draggingKey.current = key; + + setGhostImage(e.currentTarget); + + // Add listeners to global document + document.addEventListener("mouseup", handleMouseUp); + document.addEventListener("mousemove", handleMouseMove); + }; + + const handleMouseMove = (e: MouseEvent) => { + if (draggingKey.current === "") return; + + const rects = getRects(); + if (!rects[0]) return; + + const dx = e.clientX - lastMousePos.current.x; + const dy = e.clientY - lastMousePos.current.y; + mouseMovementDelta.current += Math.sqrt(dx * dx + dy * dy); + + lastMousePos.current = { x: e.clientX, y: e.clientY }; + + if (mouseMovementDelta.current < threshold) return; + + if (ghostImage.current) { + ghostImage.current.style.visibility = "visible"; + ghostImage.current.style.left = e.clientX + "px"; + ghostImage.current.style.top = e.clientY + "px"; + } + + const startX = rects[0].x; + + const { clientX } = e; + const mouseX = clientX - startX; + + const closest = rects + .map((r) => { + const mid = r.x + r.width / 2 - startX; + return { + key: r.key, + dist: Math.abs(mid - mouseX), + before: mouseX < mid, // before midpoint? }; - - if (tabIndex === tabs.length - 1 && placeIndex > tabIndex && mouseMovementDelta.current >= threshold) { - style.borderRight = "2px solid white"; - return style; - } - - return style; - }; - - const setGhostImage = (element: HTMLDivElement) => { - if (!tabRefs || !tabRefs.current) return; - - // Not the best, but it works :> - const ghost = document.createElement("div"); - ghost.textContent = element.textContent || ""; - ghost.style.position = "absolute"; - ghost.style.background = "black"; - ghost.style.color = "white"; - ghost.style.padding = ".5rem 1rem"; - ghost.style.whiteSpace = "nowrap"; - ghost.style.pointerEvents = "none"; - ghost.style.border = "1px solid #303030"; - ghost.style.borderRadius = "8px"; - ghost.style.visibility = "hidden"; - document.body.appendChild(ghost); - ghostImage.current = ghost; - }; - - const setPlaceIndexSync = (v: number) => { - placeIndexRef.current = v; - _setPlaceIndex(v); - }; - - // listeners - type TargetKey = React.MouseEvent | React.KeyboardEvent | string; - const onEdit = (targetKey: TargetKey, action: "add" | "remove") => { - if (action === "add") return; - if (!(typeof targetKey === "string")) return; - closeTab(targetKey); - }; - - const handleMouseDown = (e: React.MouseEvent, key: string) => { - // Middle click (button 1) closes the tab - if (e.button === 1) { - e.preventDefault(); - closeTab(key); - return; - } - - // Only proceed with drag for left click (button 0) - if (e.button !== 0) return; - - mouseMovementDelta.current = 0; - lastMousePos.current = { x: e.clientX, y: e.clientY }; - - draggingKey.current = key; - - setGhostImage(e.currentTarget); - - // Add listeners to global document - document.addEventListener("mouseup", handleMouseUp); - document.addEventListener("mousemove", handleMouseMove); - }; - - const handleMouseMove = (e: MouseEvent) => { - if (draggingKey.current === "") return; - - const rects = getRects(); - if (!rects[0]) return; - - - const dx = e.clientX - lastMousePos.current.x; - const dy = e.clientY - lastMousePos.current.y; - mouseMovementDelta.current += Math.sqrt(dx * dx + dy * dy); - - lastMousePos.current = { x: e.clientX, y: e.clientY }; - - if (mouseMovementDelta.current < threshold) return; - - if (ghostImage.current) { - ghostImage.current.style.visibility = "visible"; - ghostImage.current.style.left = e.clientX + "px"; - ghostImage.current.style.top = e.clientY + "px"; - }; - - - const startX = rects[0].x; - - const { clientX } = e; - const mouseX = clientX - startX; - - const closest = rects - .map(r => { - const mid = r.x + r.width / 2 - startX; - return { - key: r.key, - dist: Math.abs(mid - mouseX), - before: mouseX < mid // before midpoint? - }; - }) - .sort((a, b) => a.dist - b.dist)[0]; - - const index = (tabs?.findIndex(tab => tab.key === closest.key) ?? -Infinity) + (closest.before ? 0 : 1); - if (index < 0) return; - - setPlaceIndexSync(index); - }; - - const handleMouseUp = () => { - document.removeEventListener("mouseup", handleMouseUp); - document.removeEventListener("mouseover", handleMouseMove); - - const currentIndex = tabs ? tabs.findIndex(tab => tab.key === draggingKey.current) : -1; - if ( - placeIndexRef.current >= 0 && - placeIndexRef.current !== currentIndex - ) { - setTabPosition(draggingKey.current, placeIndexRef.current); - openTab(draggingKey.current); - } - - draggingKey.current = ""; - setPlaceIndexSync(-1); - - if (ghostImage.current) document.body.removeChild(ghostImage.current); - }; - - const handleContextMenu = (e: React.MouseEvent, key: string) => { - e.preventDefault(); - setContextMenu({ x: e.clientX, y: e.clientY, key }); - }; - - const handleCloseContextMenu = () => { - setContextMenu(null); - }; - - const handleCloseOtherTabs = () => { - if (contextMenu) { - closeOtherTabs(contextMenu.key); - } - setContextMenu(null); - }; - - useEffect(() => { - if (contextMenu) { - document.addEventListener("click", handleCloseContextMenu); - return () => document.removeEventListener("click", handleCloseContextMenu); - } - }, [contextMenu]); - - return ( - <> - openTab(key)} - items={tabs?.map(({ key }) => ({ - key, - label: ( -
{ handleMouseDown(e, key); }} - onContextMenu={(e) => { handleContextMenu(e, key); }} - ref={(el) => { tabRefs.current[key] = el; }} - style={{ userSelect: "none", }} - > - {key.replace(".class", "").split("/").pop()} -
- ) - }))} - renderTabBar={(tabBarProps, DefaultTabBar) => ( - - {(node) => ( -
- {node} -
- )} -
- )} - /> - - {contextMenu && ( -
e.stopPropagation()} - > -
{ - e.currentTarget.style.background = "#2a2a2a"; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = "transparent"; - }} - > - Close Other Tabs -
-
- )} - - ); + }) + .sort((a, b) => a.dist - b.dist)[0]; + + const index = + (tabs?.findIndex((tab) => tab.key === closest.key) ?? -Infinity) + (closest.before ? 0 : 1); + if (index < 0) return; + + setPlaceIndexSync(index); + }; + + const handleMouseUp = () => { + document.removeEventListener("mouseup", handleMouseUp); + document.removeEventListener("mouseover", handleMouseMove); + + const currentIndex = tabs ? tabs.findIndex((tab) => tab.key === draggingKey.current) : -1; + if (placeIndexRef.current >= 0 && placeIndexRef.current !== currentIndex) { + setTabPosition(draggingKey.current, placeIndexRef.current); + openTab(draggingKey.current); + } + + draggingKey.current = ""; + setPlaceIndexSync(-1); + + if (ghostImage.current) document.body.removeChild(ghostImage.current); + }; + + const handleContextMenu = (e: React.MouseEvent, key: string) => { + e.preventDefault(); + setContextMenu({ x: e.clientX, y: e.clientY, key }); + }; + + const handleCloseContextMenu = () => { + setContextMenu(null); + }; + + const handleCloseOtherTabs = () => { + if (contextMenu) { + closeOtherTabs(contextMenu.key); + } + setContextMenu(null); + }; + + useEffect(() => { + if (contextMenu) { + document.addEventListener("click", handleCloseContextMenu); + return () => document.removeEventListener("click", handleCloseContextMenu); + } + }, [contextMenu]); + + return ( + <> + openTab(key)} + items={tabs?.map(({ key }) => ({ + key, + label: ( +
{ + handleMouseDown(e, key); + }} + onContextMenu={(e) => { + handleContextMenu(e, key); + }} + ref={(el) => { + tabRefs.current[key] = el; + }} + style={{ userSelect: "none" }} + > + {key.replace(".class", "").split("/").pop()} +
+ ), + }))} + renderTabBar={(tabBarProps, DefaultTabBar) => ( + + {(node) =>
{node}
} +
+ )} + /> + + {contextMenu && ( +
e.stopPropagation()} + > +
{ + e.currentTarget.style.background = "#2a2a2a"; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = "transparent"; + }} + > + Close Other Tabs +
+
+ )} + + ); }; diff --git a/src/ui/diff/DiffCode.tsx b/src/ui/diff/DiffCode.tsx index a99e135..06fc571 100644 --- a/src/ui/diff/DiffCode.tsx +++ b/src/ui/diff/DiffCode.tsx @@ -1,37 +1,37 @@ -import { DiffEditor } from '@monaco-editor/react'; -import { useObservable } from '../../utils/UseObservable'; -import { getLeftDiff, getRightDiff } from '../../logic/Diff'; -import { updateLineChanges } from '../../logic/LineChanges'; -import { useEffect, useRef } from 'react'; -import type { editor } from 'monaco-editor'; +import { DiffEditor } from "@monaco-editor/react"; +import { useObservable } from "../../utils/UseObservable"; +import { getLeftDiff, getRightDiff } from "../../logic/Diff"; +import { updateLineChanges } from "../../logic/LineChanges"; +import { useEffect, useRef } from "react"; +import type { editor } from "monaco-editor"; import { Spin } from "antd"; -import { LoadingOutlined } from '@ant-design/icons'; +import { LoadingOutlined } from "@ant-design/icons"; import { isDecompiling } from "../../logic/Decompiler.ts"; -import { unifiedDiff } from '../../logic/Settings'; -import { selectedFile } from '../../logic/State.ts'; +import { unifiedDiff } from "../../logic/Settings"; +import { selectedFile } from "../../logic/State.ts"; const DiffCode = () => { - const leftResult = useObservable(getLeftDiff().result); - const rightResult = useObservable(getRightDiff().result); - const editorRef = useRef(null); - const loading = useObservable(isDecompiling); - const currentPath = useObservable(selectedFile); - const isUnified = useObservable(unifiedDiff.observable); + const leftResult = useObservable(getLeftDiff().result); + const rightResult = useObservable(getRightDiff().result); + const editorRef = useRef(null); + const loading = useObservable(isDecompiling); + const currentPath = useObservable(selectedFile); + const isUnified = useObservable(unifiedDiff.observable); - useEffect(() => { - if (loading) return; - if (!currentPath) return; - if (!leftResult) return; - if (!rightResult) return; + useEffect(() => { + if (loading) return; + if (!currentPath) return; + if (!leftResult) return; + if (!rightResult) return; - const currentClass = currentPath.replace(".class", ""); - if (leftResult.className !== currentClass) return; - if (rightResult.className !== currentClass) return; + const currentClass = currentPath.replace(".class", ""); + if (leftResult.className !== currentClass) return; + if (rightResult.className !== currentClass) return; - updateLineChanges(currentPath, leftResult.source, rightResult.source); - }, [leftResult, rightResult, loading, currentPath]); + updateLineChanges(currentPath, leftResult.source, rightResult.source); + }, [leftResult, rightResult, loading, currentPath]); - /* Disabled as it jumps to the line of the previous change when switching files + /* Disabled as it jumps to the line of the previous change when switching files useEffect(() => { if (!editorRef.current) { return; @@ -46,41 +46,42 @@ const DiffCode = () => { }, [leftResult, rightResult]); */ - return ( - } - size={"large"} - spinning={!!loading} - description="Decompiling..." - styles={{ - root: { - height: '100%', - color: 'white' - }, - container: { - height: '100%', - } - }} - > - { - editorRef.current = editor; - }} - options={{ - readOnly: true, - domReadOnly: true, - renderSideBySide: !isUnified, - scrollBeyondLastLine: false, - //tabSize: 3, - }} /> - - ); + return ( + } + size={"large"} + spinning={!!loading} + description="Decompiling..." + styles={{ + root: { + height: "100%", + color: "white", + }, + container: { + height: "100%", + }, + }} + > + { + editorRef.current = editor; + }} + options={{ + readOnly: true, + domReadOnly: true, + renderSideBySide: !isUnified, + scrollBeyondLastLine: false, + //tabSize: 3, + }} + /> + + ); }; export default DiffCode; diff --git a/src/ui/diff/DiffFileList.tsx b/src/ui/diff/DiffFileList.tsx index e578322..a32b1cc 100644 --- a/src/ui/diff/DiffFileList.tsx +++ b/src/ui/diff/DiffFileList.tsx @@ -1,219 +1,253 @@ -import { Table, Tag, Input, Button, Flex, theme, Checkbox, Tooltip, Layout, Space } from 'antd'; -import { SplitCellsOutlined, AlignLeftOutlined, EyeOutlined, EyeInvisibleOutlined, CodeOutlined, FileTextOutlined } from '@ant-design/icons'; -import DiffVersionSelection from './DiffVersionSelection'; +import { Table, Tag, Input, Button, Flex, theme, Checkbox, Tooltip, Layout, Space } from "antd"; import { - getDiffChanges, - type ChangeState, - type ChangeInfo, - hideUnchangedSizes, - getDiffSummary, - type DiffSummary, -} from '../../logic/Diff'; -import { BehaviorSubject, map, combineLatest } from 'rxjs'; -import { useObservable } from '../../utils/UseObservable'; -import type { SearchProps } from 'antd/es/input'; + SplitCellsOutlined, + AlignLeftOutlined, + EyeOutlined, + EyeInvisibleOutlined, + CodeOutlined, + FileTextOutlined, +} from "@ant-design/icons"; +import DiffVersionSelection from "./DiffVersionSelection"; +import { + getDiffChanges, + type ChangeState, + type ChangeInfo, + hideUnchangedSizes, + getDiffSummary, + type DiffSummary, +} from "../../logic/Diff"; +import { BehaviorSubject, map, combineLatest } from "rxjs"; +import { useObservable } from "../../utils/UseObservable"; +import type { SearchProps } from "antd/es/input"; import { isDecompiling } from "../../logic/Decompiler.ts"; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from "react"; import { bytecode, unifiedDiff } from "../../logic/Settings.ts"; -import { selectedFile, diffView } from '../../logic/State.ts'; +import { selectedFile, diffView } from "../../logic/State.ts"; const statusColors: Record = { - modified: 'gold', - added: 'green', - deleted: 'red', + modified: "gold", + added: "green", + deleted: "red", }; const searchQuery = new BehaviorSubject(""); interface DiffEntry { - key: string; - file: string; - statusInfo: ChangeInfo; + key: string; + file: string; + statusInfo: ChangeInfo; } const entries = combineLatest([getDiffChanges(), searchQuery]).pipe( - map(([changesMap, query]) => { - const entriesArray: DiffEntry[] = []; - const lowerQuery = query.toLowerCase(); - changesMap.forEach((info, file) => { - if (!query || file.toLowerCase().includes(lowerQuery)) { - entriesArray.push({ - key: file, - file, - statusInfo: info, - }); - } + map(([changesMap, query]) => { + const entriesArray: DiffEntry[] = []; + const lowerQuery = query.toLowerCase(); + changesMap.forEach((info, file) => { + if (!query || file.toLowerCase().includes(lowerQuery)) { + entriesArray.push({ + key: file, + file, + statusInfo: info, }); - return entriesArray; - }) + } + }); + return entriesArray; + }), ); const DiffFileList = () => { - const dataSource = useObservable(entries) || []; - const currentFile = useObservable(selectedFile); - const loading = useObservable(isDecompiling); - const hideUnchanged = useObservable(hideUnchangedSizes) || false; - const summary = useObservable(useMemo(() => getDiffSummary(), [])); - const isUnifiedDiff = useObservable(unifiedDiff.observable); - const isBytecode = useObservable(bytecode.observable); - const { token } = theme.useToken(); - - const columns = useMemo(() => [ - { - title: 'File', - dataIndex: 'file', - key: 'file', - render: (file: string) => {file.replace('.class', '')}, - }, - { - title: 'Status', - dataIndex: 'statusInfo', - key: 'status', - render: (info: ChangeInfo) => ( - - - {info.state.toUpperCase()} - - {info.deletions !== undefined && info.deletions > 0 && ( - -{info.deletions} - )} - {info.additions !== undefined && info.additions > 0 && ( - +{info.additions} - )} - {info.state === 'modified' && info.additions === 0 && info.deletions === 0 && ( - None - )} - - ), - }, - ], [token]); + const dataSource = useObservable(entries) || []; + const currentFile = useObservable(selectedFile); + const loading = useObservable(isDecompiling); + const hideUnchanged = useObservable(hideUnchangedSizes) || false; + const summary = useObservable(useMemo(() => getDiffSummary(), [])); + const isUnifiedDiff = useObservable(unifiedDiff.observable); + const isBytecode = useObservable(bytecode.observable); + const { token } = theme.useToken(); - const onChange: SearchProps['onChange'] = (e) => { - searchQuery.next(e.target.value); - }; + const columns = useMemo( + () => [ + { + title: "File", + dataIndex: "file", + key: "file", + render: (file: string) => ( + {file.replace(".class", "")} + ), + }, + { + title: "Status", + dataIndex: "statusInfo", + key: "status", + render: (info: ChangeInfo) => ( + + + {info.state.toUpperCase()} + + {info.deletions !== undefined && info.deletions > 0 && ( + + -{info.deletions} + + )} + {info.additions !== undefined && info.additions > 0 && ( + + +{info.additions} + + )} + {info.state === "modified" && info.additions === 0 && info.deletions === 0 && ( + + None + + )} + + ), + }, + ], + [token], + ); - const handleExitDiff = () => { - diffView.next(false); - }; + const onChange: SearchProps["onChange"] = (e) => { + searchQuery.next(e.target.value); + }; - useEffect(() => { - if (dataSource.length > 500 && !hideUnchanged) { - hideUnchangedSizes.next(true); - } - }, [dataSource.length, hideUnchanged]); + const handleExitDiff = () => { + diffView.next(false); + }; - return ( - - - - - - {summary && ( - - {summary.added === 0 && summary.deleted === 0 && summary.modified === 0 ? ( - None - ) : ( - <> - - +{summary.added} new files - - - -{summary.deleted} deleted - - - {summary.modified} modified - - - )} - - )} - + useEffect(() => { + if (dataSource.length > 500 && !hideUnchanged) { + hideUnchangedSizes.next(true); + } + }, [dataSource.length, hideUnchanged]); - - - + return ( + + + + + + {summary && ( + + {summary.added === 0 && summary.deleted === 0 && summary.modified === 0 ? ( + None + ) : ( + <> + +{summary.added} new files + -{summary.deleted} deleted + {summary.modified} modified + + )} + + )} + - - - - - - + + + - + - None }} - rowClassName={(record) => - currentFile === record.file ? 'ant-table-row-selected' : '' - } - onRow={(record) => ({ - onClick: () => { - if (loading) return; - if (currentFile === record.file) return; + + + + + + +
None }} + rowClassName={(record) => (currentFile === record.file ? "ant-table-row-selected" : "")} + onRow={(record) => ({ + onClick: () => { + if (loading) return; + if (currentFile === record.file) return; - selectedFile.next(record.file); - } - })} - style={{ - cursor: loading ? 'not-allowed' : 'pointer' - }} - /> - - - ); + selectedFile.next(record.file); + }, + })} + style={{ + cursor: loading ? "not-allowed" : "pointer", + }} + /> + + + ); }; export default DiffFileList; diff --git a/src/ui/diff/DiffVersionSelection.tsx b/src/ui/diff/DiffVersionSelection.tsx index adc1292..de07e3c 100644 --- a/src/ui/diff/DiffVersionSelection.tsx +++ b/src/ui/diff/DiffVersionSelection.tsx @@ -4,40 +4,44 @@ import { minecraftVersionIds } from "../../logic/MinecraftApi"; import { getLeftDiff, getRightDiff } from "../../logic/Diff"; const DiffVersionSelection = () => { - const versions = useObservable(minecraftVersionIds); - const leftVersion = useObservable(getLeftDiff().selectedVersion); - const rightVersion = useObservable(getRightDiff().selectedVersion); + const versions = useObservable(minecraftVersionIds); + const leftVersion = useObservable(getLeftDiff().selectedVersion); + const rightVersion = useObservable(getRightDiff().selectedVersion); - if (!leftVersion) { - // This will trigger the jar to load - getLeftDiff().selectedVersion.next(versions?.[1] || null); - } + if (!leftVersion) { + // This will trigger the jar to load + getLeftDiff().selectedVersion.next(versions?.[1] || null); + } - return ( - - - → - - - ); + return ( + + + → + + + ); }; export default DiffVersionSelection; diff --git a/src/ui/diff/DiffView.tsx b/src/ui/diff/DiffView.tsx index b2183b9..eef2350 100644 --- a/src/ui/diff/DiffView.tsx +++ b/src/ui/diff/DiffView.tsx @@ -5,29 +5,29 @@ import DiffCode from "./DiffCode"; import { FilepathHeader } from "../FilepathHeader"; const DiffView = () => { - const [sizes, setSizes] = useState<(number | string)[]>(['70%', '30%']); - return ( - <> - - - - - - - - - - - ); + const [sizes, setSizes] = useState<(number | string)[]>(["70%", "30%"]); + return ( + <> + + + + + + + + + + + ); }; export default DiffView; diff --git a/src/ui/inheritance/InheritanceGraph.tsx b/src/ui/inheritance/InheritanceGraph.tsx index e4d7193..2ebe972 100644 --- a/src/ui/inheritance/InheritanceGraph.tsx +++ b/src/ui/inheritance/InheritanceGraph.tsx @@ -1,4 +1,11 @@ -import { ReactFlow, type Node, type Edge, Background, useReactFlow, ReactFlowProvider } from "@xyflow/react"; +import { + ReactFlow, + type Node, + type Edge, + Background, + useReactFlow, + ReactFlowProvider, +} from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { ClassNode, selectedInheritanceClassName } from "../../logic/Inheritance"; import { isInterface, isAbstract } from "../../utils/Classfile"; @@ -6,219 +13,218 @@ import { useMemo, useCallback, useEffect } from "react"; import dagre from "dagre"; import { openTab } from "../../logic/Tabs"; -function buildGraphData(classNode: ClassNode): { nodes: Node[]; edges: Edge[]; } { - const nodes: Node[] = []; - const edges: Edge[] = []; - const visited = new Set(); - - const getSimpleClassName = (fullName: string) => { - const i = fullName.lastIndexOf('/'); - return i === -1 ? fullName : fullName.substring(i + 1); - }; - - function addNodeWithParents(node: ClassNode): void { - if (visited.has(node.name)) return; - visited.add(node.name); - - const isSelected = node.name === classNode.name; - const nodeIsInterface = isInterface(node.accessFlags); - const nodeIsAbstract = isAbstract(node.accessFlags); - let background = "#fff"; - let color = "#000"; - let borderStyle = "1px solid #1890ff"; - - if (isSelected) { - background = "#1890ff"; - color = "#fff"; - } else if (nodeIsInterface) { - background = "#e6f7ff"; - borderStyle = "2px dashed #1890ff"; - } else if (nodeIsAbstract) { - background = "#fff7e6"; - borderStyle = "1px dashed #fa8c16"; - } - - nodes.push({ - id: node.name, - data: { label: getSimpleClassName(node.name) }, - position: { x: 0, y: 0 }, // Will be calculated by dagre - style: { - background, - color, - border: borderStyle, - borderRadius: "5px", - padding: "10px", - cursor: "pointer", - fontStyle: nodeIsInterface || nodeIsAbstract ? "italic" : "normal", - }, - }); - - // Add all parents - node.parents.forEach((parent) => { - edges.push({ - id: `${parent.name}-${node.name}`, - source: parent.name, - target: node.name, - animated: false, - }); - addNodeWithParents(parent); - }); - } - - function addNodeWithChildren(node: ClassNode): void { - if (visited.has(node.name)) return; - visited.add(node.name); - - const isSelected = node.name === classNode.name; - const nodeIsInterface = isInterface(node.accessFlags); - const nodeIsAbstract = isAbstract(node.accessFlags); - let background = "#fff"; - let color = "#000"; - let borderStyle = "1px solid #1890ff"; - - if (isSelected) { - background = "#1890ff"; - color = "#fff"; - } else if (nodeIsInterface) { - background = "#e6f7ff"; - borderStyle = "2px dashed #1890ff"; - } else if (nodeIsAbstract) { - background = "#fff7e6"; - borderStyle = "1px dashed #fa8c16"; - } - - nodes.push({ - id: node.name, - data: { label: getSimpleClassName(node.name) }, - position: { x: 0, y: 0 }, // Will be calculated by dagre - style: { - background, - color, - border: borderStyle, - borderRadius: "5px", - padding: "10px", - cursor: "pointer", - fontStyle: nodeIsInterface || nodeIsAbstract ? "italic" : "normal", - }, - }); - - // Add all children - node.children.forEach((child) => { - edges.push({ - id: `${node.name}-${child.name}`, - source: node.name, - target: child.name, - animated: false, - }); - addNodeWithChildren(child); - }); +function buildGraphData(classNode: ClassNode): { nodes: Node[]; edges: Edge[] } { + const nodes: Node[] = []; + const edges: Edge[] = []; + const visited = new Set(); + + const getSimpleClassName = (fullName: string) => { + const i = fullName.lastIndexOf("/"); + return i === -1 ? fullName : fullName.substring(i + 1); + }; + + function addNodeWithParents(node: ClassNode): void { + if (visited.has(node.name)) return; + visited.add(node.name); + + const isSelected = node.name === classNode.name; + const nodeIsInterface = isInterface(node.accessFlags); + const nodeIsAbstract = isAbstract(node.accessFlags); + let background = "#fff"; + let color = "#000"; + let borderStyle = "1px solid #1890ff"; + + if (isSelected) { + background = "#1890ff"; + color = "#fff"; + } else if (nodeIsInterface) { + background = "#e6f7ff"; + borderStyle = "2px dashed #1890ff"; + } else if (nodeIsAbstract) { + background = "#fff7e6"; + borderStyle = "1px dashed #fa8c16"; } - // First add the selected node and its parents - addNodeWithParents(classNode); - - // Then add the children of the selected node - classNode.children.forEach((child) => { - edges.push({ - id: `${classNode.name}-${child.name}`, - source: classNode.name, - target: child.name, - animated: false, - }); - addNodeWithChildren(child); + nodes.push({ + id: node.name, + data: { label: getSimpleClassName(node.name) }, + position: { x: 0, y: 0 }, // Will be calculated by dagre + style: { + background, + color, + border: borderStyle, + borderRadius: "5px", + padding: "10px", + cursor: "pointer", + fontStyle: nodeIsInterface || nodeIsAbstract ? "italic" : "normal", + }, }); - // Use dagre to calculate positions - const dagreGraph = new dagre.graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ - rankdir: 'TB', // Top to Bottom - nodesep: 100, - ranksep: 100, - edgesep: 50 + // Add all parents + node.parents.forEach((parent) => { + edges.push({ + id: `${parent.name}-${node.name}`, + source: parent.name, + target: node.name, + animated: false, + }); + addNodeWithParents(parent); }); + } + + function addNodeWithChildren(node: ClassNode): void { + if (visited.has(node.name)) return; + visited.add(node.name); + + const isSelected = node.name === classNode.name; + const nodeIsInterface = isInterface(node.accessFlags); + const nodeIsAbstract = isAbstract(node.accessFlags); + let background = "#fff"; + let color = "#000"; + let borderStyle = "1px solid #1890ff"; + + if (isSelected) { + background = "#1890ff"; + color = "#fff"; + } else if (nodeIsInterface) { + background = "#e6f7ff"; + borderStyle = "2px dashed #1890ff"; + } else if (nodeIsAbstract) { + background = "#fff7e6"; + borderStyle = "1px dashed #fa8c16"; + } - // Add nodes to dagre graph - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: 200, height: 50 }); + nodes.push({ + id: node.name, + data: { label: getSimpleClassName(node.name) }, + position: { x: 0, y: 0 }, // Will be calculated by dagre + style: { + background, + color, + border: borderStyle, + borderRadius: "5px", + padding: "10px", + cursor: "pointer", + fontStyle: nodeIsInterface || nodeIsAbstract ? "italic" : "normal", + }, }); - // Add edges to dagre graph - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); + // Add all children + node.children.forEach((child) => { + edges.push({ + id: `${node.name}-${child.name}`, + source: node.name, + target: child.name, + animated: false, + }); + addNodeWithChildren(child); }); - - // Calculate layout - dagre.layout(dagreGraph); - - // Apply calculated positions to nodes - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - return { - ...node, - position: { - x: nodeWithPosition.x - 100, - y: nodeWithPosition.y - 25, - }, - }; + } + + // First add the selected node and its parents + addNodeWithParents(classNode); + + // Then add the children of the selected node + classNode.children.forEach((child) => { + edges.push({ + id: `${classNode.name}-${child.name}`, + source: classNode.name, + target: child.name, + animated: false, }); + addNodeWithChildren(child); + }); + + // Use dagre to calculate positions + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ + rankdir: "TB", // Top to Bottom + nodesep: 100, + ranksep: 100, + edgesep: 50, + }); + + // Add nodes to dagre graph + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { width: 200, height: 50 }); + }); + + // Add edges to dagre graph + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + // Calculate layout + dagre.layout(dagreGraph); + + // Apply calculated positions to nodes + const layoutedNodes = nodes.map((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + return { + ...node, + position: { + x: nodeWithPosition.x - 100, + y: nodeWithPosition.y - 25, + }, + }; + }); - console.log(`Graph built: ${layoutedNodes.length} nodes, ${edges.length} edges`); - return { nodes: layoutedNodes, edges }; + console.log(`Graph built: ${layoutedNodes.length} nodes, ${edges.length} edges`); + return { nodes: layoutedNodes, edges }; } -const InheritanceGraphInner = ({ data }: { data: ClassNode; }) => { - const { nodes, edges } = useMemo(() => { - if (!data) return { nodes: [], edges: [] }; - return buildGraphData(data); - }, [data]); - - const { setCenter, getNode } = useReactFlow(); - - useEffect(() => { - if (!data) return; - - const timer = setTimeout(() => { - const selectedNode = getNode(data.name); - if (selectedNode) { - void setCenter( - selectedNode.position.x + 100, - selectedNode.position.y + 25, - { zoom: 1, duration: 300 } - ); - } - }, 0); - return () => clearTimeout(timer); - }, [data, setCenter, getNode]); - - const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { - // Convert internal class name format (e.g., "net/minecraft/ChatFormatting") to file path - const filePath = node.id + ".class"; - openTab(filePath); - selectedInheritanceClassName.next(null); - }, []); - - return ( - - - - ); +const InheritanceGraphInner = ({ data }: { data: ClassNode }) => { + const { nodes, edges } = useMemo(() => { + if (!data) return { nodes: [], edges: [] }; + return buildGraphData(data); + }, [data]); + + const { setCenter, getNode } = useReactFlow(); + + useEffect(() => { + if (!data) return; + + const timer = setTimeout(() => { + const selectedNode = getNode(data.name); + if (selectedNode) { + void setCenter(selectedNode.position.x + 100, selectedNode.position.y + 25, { + zoom: 1, + duration: 300, + }); + } + }, 0); + return () => clearTimeout(timer); + }, [data, setCenter, getNode]); + + const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { + // Convert internal class name format (e.g., "net/minecraft/ChatFormatting") to file path + const filePath = node.id + ".class"; + openTab(filePath); + selectedInheritanceClassName.next(null); + }, []); + + return ( + + + + ); }; -const InheritanceGraph = ({ data }: { data: ClassNode; }) => { - return ( -
- - - -
- ); +const InheritanceGraph = ({ data }: { data: ClassNode }) => { + return ( +
+ + + +
+ ); }; -export default InheritanceGraph; \ No newline at end of file +export default InheritanceGraph; diff --git a/src/ui/inheritance/InheritanceModal.tsx b/src/ui/inheritance/InheritanceModal.tsx index 44ceb2d..1202785 100644 --- a/src/ui/inheritance/InheritanceModal.tsx +++ b/src/ui/inheritance/InheritanceModal.tsx @@ -1,46 +1,59 @@ import { lazy, Suspense } from "react"; import { Modal, Spin, Tabs } from "antd"; import { useObservable } from "../../utils/UseObservable"; -import { ClassNode, selectedInheritanceClassName, selectedInheritanceClassNode } from "../../logic/Inheritance"; +import { + ClassNode, + selectedInheritanceClassName, + selectedInheritanceClassNode, +} from "../../logic/Inheritance"; const InheritanceTree = lazy(() => import("./InheritanceTree")); const InheritanceGraph = lazy(() => import("./InheritanceGraph")); -const Inheritance = ({ data }: { data: ClassNode; }) => { - const items = [{ - key: "tree", - label: "Tree", - children: , - }, { - key: "graph", - label: "Graph", - children: , - }]; +const Inheritance = ({ data }: { data: ClassNode }) => { + const items = [ + { + key: "tree", + label: "Tree", + children: , + }, + { + key: "graph", + label: "Graph", + children: , + }, + ]; - return ; + return ; }; const InheritanceModal = () => { - const data = useObservable(selectedInheritanceClassNode); + const data = useObservable(selectedInheritanceClassNode); - return ( - selectedInheritanceClassName.next(null)} - width="90%" - style={{ top: 20 }} + return ( + selectedInheritanceClassName.next(null)} + width="90%" + style={{ top: 20 }} + > + {data ? ( + + + + } > - {data ? ( - }> - - - ) : ( -

No class selected.

- )} -
- ); + + + ) : ( +

No class selected.

+ )} +
+ ); }; -export default InheritanceModal; \ No newline at end of file +export default InheritanceModal; diff --git a/src/ui/inheritance/InheritanceTree.tsx b/src/ui/inheritance/InheritanceTree.tsx index b4bec14..1c9addb 100644 --- a/src/ui/inheritance/InheritanceTree.tsx +++ b/src/ui/inheritance/InheritanceTree.tsx @@ -6,105 +6,109 @@ import { isEnum, isInterface } from "../../utils/Classfile"; import { openTab } from "../../logic/Tabs"; function getSimpleClassName(fullName: string): string { - const i = fullName.lastIndexOf('/'); - return i === -1 ? fullName : fullName.substring(i + 1); + const i = fullName.lastIndexOf("/"); + return i === -1 ? fullName : fullName.substring(i + 1); } function renderIcon(node: ClassNode) { - if (isEnum(node.accessFlags)) return ; - if (isInterface(node.accessFlags)) return ; - return ; + if (isEnum(node.accessFlags)) return ; + if (isInterface(node.accessFlags)) return ; + return ; } function renderTitle(node: ClassNode) { - const fullName = node.name.replaceAll('/', '.'); - - return ( - - {getSimpleClassName(node.name)} - {fullName} - - ); + const fullName = node.name.replaceAll("/", "."); + + return ( + + {getSimpleClassName(node.name)} + {fullName} + + ); } -function buildTreeData(root: ClassNode, selectedName: string): { nodes: TreeDataNode[]; expanded: string[]; } { - const visited = new Set(); - - interface WalkResult { - dataNode: TreeDataNode; - expanded: string[]; +function buildTreeData( + root: ClassNode, + selectedName: string, +): { nodes: TreeDataNode[]; expanded: string[] } { + const visited = new Set(); + + interface WalkResult { + dataNode: TreeDataNode; + expanded: string[]; + } + + function walk(node: ClassNode): WalkResult | null { + // This shouldn't happen, but just in case to prevent infinite loops. + if (visited.has(node.name)) return null; + visited.add(node.name); + + const childResults = node.children + .map((child) => walk(child)) + .filter((child) => child !== null); + + childResults.sort((a, b) => { + if (a.expanded.length !== b.expanded.length) return b.expanded.length - a.expanded.length; + const aName = getSimpleClassName(a.dataNode.key as string); + const bName = getSimpleClassName(b.dataNode.key as string); + return aName.localeCompare(bName); + }); + + // Expand all nodes that either have expanded children, or are the selected node. + const hasSelected = + node.name === selectedName || childResults.some((child) => child.expanded.length > 0); + const expanded = childResults.flatMap((child) => child.expanded); + + if (hasSelected && childResults.length > 0) { + expanded.push(node.name); } - function walk(node: ClassNode): WalkResult | null { - // This shouldn't happen, but just in case to prevent infinite loops. - if (visited.has(node.name)) return null; - visited.add(node.name); - - const childResults = node.children - .map(child => walk(child)) - .filter(child => child !== null); - - childResults.sort((a, b) => { - if (a.expanded.length !== b.expanded.length) return b.expanded.length - a.expanded.length; - const aName = getSimpleClassName(a.dataNode.key as string); - const bName = getSimpleClassName(b.dataNode.key as string); - return aName.localeCompare(bName); - }); - - // Expand all nodes that either have expanded children, or are the selected node. - const hasSelected = node.name === selectedName || childResults.some(child => child.expanded.length > 0); - const expanded = childResults.flatMap(child => child.expanded); - - if (hasSelected && childResults.length > 0) { - expanded.push(node.name); - } - - const dataNode: TreeDataNode = { - key: node.name, - title: renderTitle(node), - icon: renderIcon(node), - children: childResults.map(child => child.dataNode) - }; - - return { dataNode, expanded }; - } + const dataNode: TreeDataNode = { + key: node.name, + title: renderTitle(node), + icon: renderIcon(node), + children: childResults.map((child) => child.dataNode), + }; - const result = walk(root); - if (!result) return { nodes: [], expanded: [] }; + return { dataNode, expanded }; + } - return { - nodes: [result.dataNode], - expanded: Array.from(new Set(result.expanded)) - }; + const result = walk(root); + if (!result) return { nodes: [], expanded: [] }; + + return { + nodes: [result.dataNode], + expanded: Array.from(new Set(result.expanded)), + }; } -const InheritanceTree = ({ data }: { data: ClassNode; }) => { - const { nodes, expanded } = useMemo(() => { - if (!data) return { nodes: [], expanded: [] }; - return buildTreeData(data.getRoot(), data.name); - }, [data]); - - const onSelect = useCallback((selectedKeys: Key[]) => { - const selected = selectedKeys[0]; - if (!selected) return; - - // Convert internal class name format (e.g., "net/minecraft/ChatFormatting") to file path - openTab(`${selected}.class`); - selectedInheritanceClassName.next(null); - }, []); - - return ( - - ); +const InheritanceTree = ({ data }: { data: ClassNode }) => { + const { nodes, expanded } = useMemo(() => { + if (!data) return { nodes: [], expanded: [] }; + return buildTreeData(data.getRoot(), data.name); + }, [data]); + + const onSelect = useCallback((selectedKeys: Key[]) => { + const selected = selectedKeys[0]; + if (!selected) return; + + // Convert internal class name format (e.g., "net/minecraft/ChatFormatting") to file path + openTab(`${selected}.class`); + selectedInheritanceClassName.next(null); + }, []); + + return ( + + ); }; -export default InheritanceTree; \ No newline at end of file +export default InheritanceTree; diff --git a/src/ui/intellij-icons/index.tsx b/src/ui/intellij-icons/index.tsx index 67b0150..4cff547 100644 --- a/src/ui/intellij-icons/index.tsx +++ b/src/ui/intellij-icons/index.tsx @@ -1,42 +1,48 @@ -import Icon from '@ant-design/icons'; -import type React from 'react'; -import type { SVGProps } from 'react'; -import type { ClassData } from '../../workers/JarIndex'; -import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon'; +import Icon from "@ant-design/icons"; +import type React from "react"; +import type { SVGProps } from "react"; +import type { ClassData } from "../../workers/JarIndex"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; -import AnnotationSvg from './annotation_dark.svg?react'; -import ClassAbstractSvg from './classAbstract_dark.svg?react'; -import ClassSvg from './class_dark.svg?react'; -import EnumSvg from './enum_dark.svg?react'; -import ExceptionSvg from './exception_dark.svg?react'; -import FinalMarkSvg from './finalMark_dark.svg?react'; -import InterfaceSvg from './interface_dark.svg?react'; -import JavaSvg from './java_dark.svg?react'; -import RecordSvg from './record_dark.svg?react'; -import PackageSvg from './package_dark.svg?react'; +import AnnotationSvg from "./annotation_dark.svg?react"; +import ClassAbstractSvg from "./classAbstract_dark.svg?react"; +import ClassSvg from "./class_dark.svg?react"; +import EnumSvg from "./enum_dark.svg?react"; +import ExceptionSvg from "./exception_dark.svg?react"; +import FinalMarkSvg from "./finalMark_dark.svg?react"; +import InterfaceSvg from "./interface_dark.svg?react"; +import JavaSvg from "./java_dark.svg?react"; +import RecordSvg from "./record_dark.svg?react"; +import PackageSvg from "./package_dark.svg?react"; type SVGFC = React.FC>; -const stack = (...svgs: SVGFC[]): SVGFC => (props) => ( -
- {svgs.map((Svg, i) => - - )} +const stack = + (...svgs: SVGFC[]): SVGFC => + (props) => ( +
+ {svgs.map((Svg, i) => ( + + ))}
-); + ); const ClassFinalSvg = stack(ClassSvg, FinalMarkSvg); type IconProps = Partial; type IconFC = React.FC; -const icon = (fc: SVGFC): IconFC => (props) => ; +const icon = + (fc: SVGFC): IconFC => + (props) => ; export const AnnotationIcon = icon(AnnotationSvg); export const ClassAbstractIcon = icon(ClassAbstractSvg); export const ClassIcon = icon(ClassSvg); @@ -58,29 +64,31 @@ const ACC_RECORD = 65536; const ACC_ABSTRACT = 1024; const ACC_FINAL = 16; -export type ClassDataIconProps = IconProps & { data: ClassData; }; +export type ClassDataIconProps = IconProps & { data: ClassData }; export const ClassDataIcon: React.FC = (p) => { - const { className, accessFlags, superName } = p.data; + const { className, accessFlags, superName } = p.data; - // oxlint-disable-next-line no-constant-binary-expression - if (false - || /^(.*\/)?package-info$/.test(className) - || /^(.*\/)?module-info$/.test(className)) return ; + // oxlint-disable-next-line no-constant-binary-expression + if (false || /^(.*\/)?package-info$/.test(className) || /^(.*\/)?module-info$/.test(className)) + return ; - if ((accessFlags & ACC_ANNOTATION) !== 0) return ; - if ((accessFlags & ACC_INTERFACE) !== 0) return ; - if ((accessFlags & ACC_ENUM) !== 0) return ; + if ((accessFlags & ACC_ANNOTATION) !== 0) return ; + if ((accessFlags & ACC_INTERFACE) !== 0) return ; + if ((accessFlags & ACC_ENUM) !== 0) return ; - // oxlint-disable-next-line no-constant-binary-expression - if (false - || superName === 'java/lang/Exception' - || superName === 'java/lang/RuntimeException' - || superName === 'java/lang/Throwable' - || /^(.*\/)?\w+Exception$/.test(className)) return ; + // oxlint-disable-next-line no-constant-binary-expression + if ( + false || + superName === "java/lang/Exception" || + superName === "java/lang/RuntimeException" || + superName === "java/lang/Throwable" || + /^(.*\/)?\w+Exception$/.test(className) + ) + return ; - if ((accessFlags & ACC_RECORD) !== 0) return ; - if ((accessFlags & ACC_ABSTRACT) !== 0) return ; - if ((accessFlags & ACC_FINAL) !== 0) return ; + if ((accessFlags & ACC_RECORD) !== 0) return ; + if ((accessFlags & ACC_ABSTRACT) !== 0) return ; + if ((accessFlags & ACC_FINAL) !== 0) return ; - return ; + return ; }; diff --git a/src/utils/Classfile.ts b/src/utils/Classfile.ts index 492bd5b..d0d1d35 100644 --- a/src/utils/Classfile.ts +++ b/src/utils/Classfile.ts @@ -1,11 +1,11 @@ export function isInterface(accessFlags: number): boolean { - return (accessFlags & 0x0200) !== 0; + return (accessFlags & 0x0200) !== 0; } export function isAbstract(accessFlags: number): boolean { - return (accessFlags & 0x0400) !== 0; + return (accessFlags & 0x0400) !== 0; } export function isEnum(accessFlags: number): boolean { - return (accessFlags & 0x4000) !== 0; -} \ No newline at end of file + return (accessFlags & 0x4000) !== 0; +} diff --git a/src/utils/Jar.ts b/src/utils/Jar.ts index 9389dde..f400595 100644 --- a/src/utils/Jar.ts +++ b/src/utils/Jar.ts @@ -1,16 +1,16 @@ import { read, type Entry, type Reader, type Zip, readBlob } from "@katana-project/zip"; export interface Jar { - name: string; - blob: Blob; - entries: { [key: string]: Entry; }; + name: string; + blob: Blob; + entries: { [key: string]: Entry }; } export async function openJar(name: string, blob: Blob): Promise { - const zip = await readBlob(blob, { - naive: true - }); - return new JarImpl(name, blob, zip); + const zip = await readBlob(blob, { + naive: true, + }); + return new JarImpl(name, blob, zip); } // TODO: fix @@ -23,81 +23,87 @@ export async function openJar(name: string, blob: Blob): Promise { // } class JarImpl implements Jar { - private zip: Zip; - public name: string; - public blob: Blob; - public entries: { [key: string]: Entry; } = {}; - - constructor(name: string, blob: Blob, zip: Zip) { - this.name = name; - this.blob = blob; - this.zip = zip; - zip.entries.forEach(entry => { - this.entries[entry.name] = entry; - }); - } + private zip: Zip; + public name: string; + public blob: Blob; + public entries: { [key: string]: Entry } = {}; + + constructor(name: string, blob: Blob, zip: Zip) { + this.name = name; + this.blob = blob; + this.zip = zip; + zip.entries.forEach((entry) => { + this.entries[entry.name] = entry; + }); + } } class HttpStreamReader implements Reader { - private url: string; + private url: string; - private _lengthCache: number | null = null; - - constructor(url: string) { - this.url = url; - } + private _lengthCache: number | null = null; - async length(): Promise { - if (this._lengthCache !== null) { - return Promise.resolve(this._lengthCache); - } + constructor(url: string) { + this.url = url; + } - const response = await fetch(this.url, { method: 'HEAD' }); + async length(): Promise { + if (this._lengthCache !== null) { + return Promise.resolve(this._lengthCache); + } - if (!response.ok) { - throw new Error(`Failed to fetch HEAD for ${this.url}: ${response.status} ${response.statusText}`); - } + const response = await fetch(this.url, { method: "HEAD" }); - const lengthHeader = response.headers.get('Content-Length'); + if (!response.ok) { + throw new Error( + `Failed to fetch HEAD for ${this.url}: ${response.status} ${response.statusText}`, + ); + } - if (!lengthHeader) { - throw new Error(`Content-Length header is missing for ${this.url}`); - } + const lengthHeader = response.headers.get("Content-Length"); - return Promise.resolve(this._lengthCache = parseInt(lengthHeader)); + if (!lengthHeader) { + throw new Error(`Content-Length header is missing for ${this.url}`); } - async read(offset: number, size: number): Promise { - const response = await this.fetchRange(offset, size); - const arrayBuffer = await response.arrayBuffer(); - return new Uint8Array(arrayBuffer); - } + return Promise.resolve((this._lengthCache = parseInt(lengthHeader))); + } + + async read(offset: number, size: number): Promise { + const response = await this.fetchRange(offset, size); + const arrayBuffer = await response.arrayBuffer(); + return new Uint8Array(arrayBuffer); + } + + async slice(offset: number, size: number): Promise { + const response = await this.fetchRange(offset, size); + return response.blob(); + } + + async fetchRange(offset: number, size: number): Promise { + const request = await fetch(this.url, { + headers: { + Range: `bytes=${offset}-${offset + size - 1}`, + }, + cache: "no-store", + }); - async slice(offset: number, size: number): Promise { - const response = await this.fetchRange(offset, size); - return response.blob(); + if (!request.ok && request.status !== 206) { + throw new Error( + `Failed to fetch range ${offset}-${offset + size - 1} for ${this.url}: ${request.status} ${request.statusText}`, + ); } - async fetchRange(offset: number, size: number): Promise { - const request = await fetch(this.url, { - headers: { - 'Range': `bytes=${offset}-${offset + size - 1}`, - }, - cache: 'no-store' - }); - - if (!request.ok && request.status !== 206) { - throw new Error(`Failed to fetch range ${offset}-${offset + size - 1} for ${this.url}: ${request.status} ${request.statusText}`); - } - - // check size - if (request.headers.has('Content-Length')) { - const contentLength = parseInt(request.headers.get('Content-Length')!); - if (contentLength !== size) { - console.warn(`Fetched range size mismatch for ${this.url}: expected ${size}, got ${contentLength}`); - } - } - - return request; + // check size + if (request.headers.has("Content-Length")) { + const contentLength = parseInt(request.headers.get("Content-Length")!); + if (contentLength !== size) { + console.warn( + `Fetched range size mismatch for ${this.url}: expected ${size}, got ${contentLength}`, + ); + } } + + return request; + } } diff --git a/src/utils/JavaBytecode.ts b/src/utils/JavaBytecode.ts index 7c73300..67d9b2e 100644 --- a/src/utils/JavaBytecode.ts +++ b/src/utils/JavaBytecode.ts @@ -1,69 +1,81 @@ -import * as monaco_editor from 'monaco-editor'; +import * as monaco_editor from "monaco-editor"; type monaco = typeof monaco_editor; -const LANGUAGE_ID = 'bytecode'; +const LANGUAGE_ID = "bytecode"; -// For a asm textifier string +// For a asm textifier string export function setupJavaBytecodeLanguage(monaco: monaco): monaco_editor.IDisposable { - monaco.languages.register({ id: LANGUAGE_ID }); - - const tokensProvider = monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, { - tokenizer: { - root: [ - // Comments - [/\/\/.*$/, 'comment'], - - // Strings - [/"([^"\\]|\\.)*$/, 'string.invalid'], - [/"/, 'string', '@string'], - - // Annotations - [/@[a-zA-Z_$][\w$]*/, 'annotation'], - - // Labels (L0, L1, etc.) - [/\bL\d+\b/, 'label'], - - // Directives (MAXSTACK, LINENUMBER, etc.) - [/\b(?:MAXSTACK|MAXLOCALS|LINENUMBER|LOCALVARIABLE|FRAME|TRYCATCHBLOCK|FIELD|METHOD|OUTERCLASS|DEPRECATED|SIGNATURE|SOURCEFILE|SOURCEDEBUGINFOEXTENSION|NESTHOST|NESTMEMBERS|PERMITTEDSUBCLASSES|RECORDCOMPONENT|ANNOTATION|PARAMETER|ATTRIBUTE|INNERCLASS)\b/, 'keyword.directive'], - - // Opcodes - [/\b(?:NOP|ACONST_NULL|ICONST_M1|ICONST_0|ICONST_1|ICONST_2|ICONST_3|ICONST_4|ICONST_5|LCONST_0|LCONST_1|FCONST_0|FCONST_1|FCONST_2|DCONST_0|DCONST_1|BIPUSH|SIPUSH|LDC|LDC_W|LDC2_W|ILOAD|LLOAD|FLOAD|DLOAD|ALOAD|ILOAD_0|ILOAD_1|ILOAD_2|ILOAD_3|LLOAD_0|LLOAD_1|LLOAD_2|LLOAD_3|FLOAD_0|FLOAD_1|FLOAD_2|FLOAD_3|DLOAD_0|DLOAD_1|DLOAD_2|DLOAD_3|ALOAD_0|ALOAD_1|ALOAD_2|ALOAD_3|IALOAD|LALOAD|FALOAD|DALOAD|AALOAD|BALOAD|CALOAD|SALOAD|ISTORE|LSTORE|FSTORE|DSTORE|ASTORE|ISTORE_0|ISTORE_1|ISTORE_2|ISTORE_3|LSTORE_0|LSTORE_1|LSTORE_2|LSTORE_3|FSTORE_0|FSTORE_1|FSTORE_2|FSTORE_3|DSTORE_0|DSTORE_1|DSTORE_2|DSTORE_3|ASTORE_0|ASTORE_1|ASTORE_2|ASTORE_3|IASTORE|LASTORE|FASTORE|DASTORE|AASTORE|BASTORE|CASTORE|SASTORE|POP|POP2|DUP|DUP_X1|DUP_X2|DUP2|DUP2_X1|DUP2_X2|SWAP|IADD|LADD|FADD|DADD|ISUB|LSUB|FSUB|DSUB|IMUL|LMUL|FMUL|DMUL|IDIV|LDIV|FDIV|DDIV|IREM|LREM|FREM|DREM|INEG|LNEG|FNEG|DNEG|ISHL|LSHL|ISHR|LSHR|IUSHR|LUSHR|IAND|LAND|IOR|LOR|IXOR|LXOR|IINC|I2L|I2F|I2D|L2I|L2F|L2D|F2I|F2L|F2D|D2I|D2L|D2F|I2B|I2C|I2S|LCMP|FCMPL|FCMPG|DCMPL|DCMPG|IFEQ|IFNE|IFLT|IFGE|IFGT|IFLE|IF_ICMPEQ|IF_ICMPNE|IF_ICMPLT|IF_ICMPGE|IF_ICMPGT|IF_ICMPLE|IF_ACMPEQ|IF_ACMPNE|GOTO|JSR|RET|TABLESWITCH|LOOKUPSWITCH|IRETURN|LRETURN|FRETURN|DRETURN|ARETURN|RETURN|GETSTATIC|PUTSTATIC|GETFIELD|PUTFIELD|INVOKEVIRTUAL|INVOKESPECIAL|INVOKESTATIC|INVOKEINTERFACE|INVOKEDYNAMIC|NEW|NEWARRAY|ANEWARRAY|ARRAYLENGTH|ATHROW|CHECKCAST|INSTANCEOF|MONITORENTER|MONITOREXIT|WIDE|MULTIANEWARRAY|IFNULL|IFNONNULL)\b/, 'keyword.opcode'], - - // Access flags and modifiers - [/\b(?:public|private|protected|static|final|synchronized|volatile|transient|native|interface|abstract|strictfp|synthetic|annotation|enum|mandated)\b/, 'keyword'], - - // Keywords - [/\b(?:class|extends|implements|version|access|flags|signature|declaration|parameter|handle|kind|arguments|itf)\b/, 'keyword'], - - // Types - [/\b(?:void|boolean|byte|char|short|int|long|float|double)\b/, 'type.primitive'], - - // Numbers (including hex and various formats) - [/\b0[xX][0-9a-fA-F]+\b/, 'number.hex'], - [/\b\d+\b/, 'number'], - - // Type descriptors (like Ljava/lang/String;) - [/L[a-zA-Z0-9_$/]+;/, 'type.descriptor'], - [/\[[ZBCSIJFD]/, 'type.descriptor'], - [/\[L[a-zA-Z0-9_$/]+;/, 'type.descriptor'], - - // Identifiers with slashes (package/class names) - [/[a-zA-Z_$][\w$/]*/, 'identifier'], - - // Operators and punctuation - [/[{}()[\]]/, 'delimiter.bracket'], - [/[<>]/, 'delimiter.angle'], - [/[:,.]/, 'delimiter'], - ], - - string: [ - [/[^\\"]+/, 'string'], - [/\\./, 'string.escape'], - [/"/, 'string', '@pop'] - ], - }, - }); - - return tokensProvider; -} \ No newline at end of file + monaco.languages.register({ id: LANGUAGE_ID }); + + const tokensProvider = monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, { + tokenizer: { + root: [ + // Comments + [/\/\/.*$/, "comment"], + + // Strings + [/"([^"\\]|\\.)*$/, "string.invalid"], + [/"/, "string", "@string"], + + // Annotations + [/@[a-zA-Z_$][\w$]*/, "annotation"], + + // Labels (L0, L1, etc.) + [/\bL\d+\b/, "label"], + + // Directives (MAXSTACK, LINENUMBER, etc.) + [ + /\b(?:MAXSTACK|MAXLOCALS|LINENUMBER|LOCALVARIABLE|FRAME|TRYCATCHBLOCK|FIELD|METHOD|OUTERCLASS|DEPRECATED|SIGNATURE|SOURCEFILE|SOURCEDEBUGINFOEXTENSION|NESTHOST|NESTMEMBERS|PERMITTEDSUBCLASSES|RECORDCOMPONENT|ANNOTATION|PARAMETER|ATTRIBUTE|INNERCLASS)\b/, + "keyword.directive", + ], + + // Opcodes + [ + /\b(?:NOP|ACONST_NULL|ICONST_M1|ICONST_0|ICONST_1|ICONST_2|ICONST_3|ICONST_4|ICONST_5|LCONST_0|LCONST_1|FCONST_0|FCONST_1|FCONST_2|DCONST_0|DCONST_1|BIPUSH|SIPUSH|LDC|LDC_W|LDC2_W|ILOAD|LLOAD|FLOAD|DLOAD|ALOAD|ILOAD_0|ILOAD_1|ILOAD_2|ILOAD_3|LLOAD_0|LLOAD_1|LLOAD_2|LLOAD_3|FLOAD_0|FLOAD_1|FLOAD_2|FLOAD_3|DLOAD_0|DLOAD_1|DLOAD_2|DLOAD_3|ALOAD_0|ALOAD_1|ALOAD_2|ALOAD_3|IALOAD|LALOAD|FALOAD|DALOAD|AALOAD|BALOAD|CALOAD|SALOAD|ISTORE|LSTORE|FSTORE|DSTORE|ASTORE|ISTORE_0|ISTORE_1|ISTORE_2|ISTORE_3|LSTORE_0|LSTORE_1|LSTORE_2|LSTORE_3|FSTORE_0|FSTORE_1|FSTORE_2|FSTORE_3|DSTORE_0|DSTORE_1|DSTORE_2|DSTORE_3|ASTORE_0|ASTORE_1|ASTORE_2|ASTORE_3|IASTORE|LASTORE|FASTORE|DASTORE|AASTORE|BASTORE|CASTORE|SASTORE|POP|POP2|DUP|DUP_X1|DUP_X2|DUP2|DUP2_X1|DUP2_X2|SWAP|IADD|LADD|FADD|DADD|ISUB|LSUB|FSUB|DSUB|IMUL|LMUL|FMUL|DMUL|IDIV|LDIV|FDIV|DDIV|IREM|LREM|FREM|DREM|INEG|LNEG|FNEG|DNEG|ISHL|LSHL|ISHR|LSHR|IUSHR|LUSHR|IAND|LAND|IOR|LOR|IXOR|LXOR|IINC|I2L|I2F|I2D|L2I|L2F|L2D|F2I|F2L|F2D|D2I|D2L|D2F|I2B|I2C|I2S|LCMP|FCMPL|FCMPG|DCMPL|DCMPG|IFEQ|IFNE|IFLT|IFGE|IFGT|IFLE|IF_ICMPEQ|IF_ICMPNE|IF_ICMPLT|IF_ICMPGE|IF_ICMPGT|IF_ICMPLE|IF_ACMPEQ|IF_ACMPNE|GOTO|JSR|RET|TABLESWITCH|LOOKUPSWITCH|IRETURN|LRETURN|FRETURN|DRETURN|ARETURN|RETURN|GETSTATIC|PUTSTATIC|GETFIELD|PUTFIELD|INVOKEVIRTUAL|INVOKESPECIAL|INVOKESTATIC|INVOKEINTERFACE|INVOKEDYNAMIC|NEW|NEWARRAY|ANEWARRAY|ARRAYLENGTH|ATHROW|CHECKCAST|INSTANCEOF|MONITORENTER|MONITOREXIT|WIDE|MULTIANEWARRAY|IFNULL|IFNONNULL)\b/, + "keyword.opcode", + ], + + // Access flags and modifiers + [ + /\b(?:public|private|protected|static|final|synchronized|volatile|transient|native|interface|abstract|strictfp|synthetic|annotation|enum|mandated)\b/, + "keyword", + ], + + // Keywords + [ + /\b(?:class|extends|implements|version|access|flags|signature|declaration|parameter|handle|kind|arguments|itf)\b/, + "keyword", + ], + + // Types + [/\b(?:void|boolean|byte|char|short|int|long|float|double)\b/, "type.primitive"], + + // Numbers (including hex and various formats) + [/\b0[xX][0-9a-fA-F]+\b/, "number.hex"], + [/\b\d+\b/, "number"], + + // Type descriptors (like Ljava/lang/String;) + [/L[a-zA-Z0-9_$/]+;/, "type.descriptor"], + [/\[[ZBCSIJFD]/, "type.descriptor"], + [/\[L[a-zA-Z0-9_$/]+;/, "type.descriptor"], + + // Identifiers with slashes (package/class names) + [/[a-zA-Z_$][\w$/]*/, "identifier"], + + // Operators and punctuation + [/[{}()[\]]/, "delimiter.bracket"], + [/[<>]/, "delimiter.angle"], + [/[:,.]/, "delimiter"], + ], + + string: [ + [/[^\\"]+/, "string"], + [/\\./, "string.escape"], + [/"/, "string", "@pop"], + ], + }, + }); + + return tokensProvider; +} diff --git a/src/utils/UseObservable.ts b/src/utils/UseObservable.ts index 7de6e53..500a242 100644 --- a/src/utils/UseObservable.ts +++ b/src/utils/UseObservable.ts @@ -1,13 +1,13 @@ -import { useState, useEffect } from 'react'; -import { Observable } from 'rxjs'; +import { useState, useEffect } from "react"; +import { Observable } from "rxjs"; export function useObservable(observable: Observable) { - const [state, setState] = useState(); + const [state, setState] = useState(); - useEffect(() => { - const sub = observable.subscribe(setState); - return () => sub.unsubscribe(); - }, [observable]); + useEffect(() => { + const sub = observable.subscribe(setState); + return () => sub.unsubscribe(); + }, [observable]); - return state; -} \ No newline at end of file + return state; +} diff --git a/src/workers/JarIndex.ts b/src/workers/JarIndex.ts index 60c87f7..796b13d 100644 --- a/src/workers/JarIndex.ts +++ b/src/workers/JarIndex.ts @@ -10,26 +10,23 @@ export type Field = `${string}:${string}:${string}`; // oxlint-disable-next-line typescript/no-redundant-type-constituents export type ReferenceKey = Class | Method; -export type ReferenceString = - | `c:${Class}` - | `m:${Method}` - | `f:${Field}`; +export type ReferenceString = `c:${Class}` | `m:${Method}` | `f:${Field}`; export interface ClassData { - className: string; - superName: string; - accessFlags: number; - interfaces: string[]; + className: string; + superName: string; + accessFlags: number; + interfaces: string[]; } export function parseClassData(data: ClassDataString): ClassData { - const [className, superName, accessFlagsStr, interfacesStr] = data.split("|"); - return { - className, - superName, - accessFlags: parseInt(accessFlagsStr, 10), - interfaces: interfacesStr ? interfacesStr.split(",").filter(i => i.length > 0) : [] - }; + const [className, superName, accessFlagsStr, interfacesStr] = data.split("|"); + return { + className, + superName, + accessFlags: parseInt(accessFlagsStr, 10), + interfaces: interfacesStr ? interfacesStr.split(",").filter((i) => i.length > 0) : [], + }; } type JarIndexWorker = typeof import("./JarIndexWorker"); @@ -40,184 +37,187 @@ export const indexProgress = new BehaviorSubject(-1); let currentJarIndex: JarIndex | null = null; export const jarIndex = minecraftJar.pipe( - distinctUntilChanged(), - map(jar => { - // Clean up the previous JarIndex instance - if (currentJarIndex) { - currentJarIndex.destroy(); - } - - const newIndex = new JarIndex(jar); - currentJarIndex = newIndex; - return newIndex; - }), - shareReplay({ bufferSize: 1, refCount: false }) + distinctUntilChanged(), + map((jar) => { + // Clean up the previous JarIndex instance + if (currentJarIndex) { + currentJarIndex.destroy(); + } + + const newIndex = new JarIndex(jar); + currentJarIndex = newIndex; + return newIndex; + }), + shareReplay({ bufferSize: 1, refCount: false }), ); interface JarClassData { - name: string, - classes: ClassData[], + name: string; + classes: ClassData[]; } const db = new Dexie("indexer") as Dexie & { - classData: EntityTable; + classData: EntityTable; }; db.version(1).stores({ - classData: "name" + classData: "name", }); // Number of classes to send to each worker in a single batch const batchSize = 25; export class JarIndex { - readonly minecraftJar: MinecraftJar; - - private _workers: ReturnType[] | undefined; - private get workers() { - if (this._workers) return this._workers; - const threads = navigator.hardwareConcurrency || 4; - this._workers = Array.from({ length: threads }, () => createWrorker()); - console.log(`Created JarIndex with ${threads} workers`); - return this._workers; - } - - private indexPromise: Promise | null = null; - private classDataCache: ClassData[] | null = null; - - constructor(minecraftJar: MinecraftJar) { - this.minecraftJar = minecraftJar; - } - - destroy(): void { - if (this._workers) { - for (const worker of this._workers) { - worker[endpointSymbol].terminate(); - } - delete this._workers; - } - this.classDataCache = null; - this.indexPromise = null; + readonly minecraftJar: MinecraftJar; + + private _workers: ReturnType[] | undefined; + private get workers() { + if (this._workers) return this._workers; + const threads = navigator.hardwareConcurrency || 4; + this._workers = Array.from({ length: threads }, () => createWrorker()); + console.log(`Created JarIndex with ${threads} workers`); + return this._workers; + } + + private indexPromise: Promise | null = null; + private classDataCache: ClassData[] | null = null; + + constructor(minecraftJar: MinecraftJar) { + this.minecraftJar = minecraftJar; + } + + destroy(): void { + if (this._workers) { + for (const worker of this._workers) { + worker[endpointSymbol].terminate(); + } + delete this._workers; } + this.classDataCache = null; + this.indexPromise = null; + } - private async indexJar(): Promise { - if (!this.indexPromise) { - this.indexPromise = this.performIndexing(); - } - return this.indexPromise; + private async indexJar(): Promise { + if (!this.indexPromise) { + this.indexPromise = this.performIndexing(); } + return this.indexPromise; + } - private async performIndexing(): Promise { - try { - const startTime = performance.now(); + private async performIndexing(): Promise { + try { + const startTime = performance.now(); - indexProgress.next(0); - console.log(`Indexing minecraft jar using ${this.workers.length} workers`); + indexProgress.next(0); + console.log(`Indexing minecraft jar using ${this.workers.length} workers`); - // Initialize all workers in parallel - await Promise.all(this.workers.map(worker => worker.setWorkerJar(this.minecraftJar.version, this.minecraftJar.blob))); + // Initialize all workers in parallel + await Promise.all( + this.workers.map((worker) => + worker.setWorkerJar(this.minecraftJar.version, this.minecraftJar.blob), + ), + ); - const jar = this.minecraftJar.jar; - const classNames = Object.keys(jar.entries) - .filter(name => name.endsWith(".class")); + const jar = this.minecraftJar.jar; + const classNames = Object.keys(jar.entries).filter((name) => name.endsWith(".class")); - let promises: Promise[] = []; + let promises: Promise[] = []; - let taskQueue = [...classNames]; - let completed = 0; + let taskQueue = [...classNames]; + let completed = 0; - for (let i = 0; i < this.workers.length; i++) { - const worker = this.workers[i]; + for (let i = 0; i < this.workers.length; i++) { + const worker = this.workers[i]; - promises.push((async () => { - while (true) { - const batch = taskQueue.splice(0, batchSize); + promises.push( + (async () => { + while (true) { + const batch = taskQueue.splice(0, batchSize); - if (batch.length === 0) { - const indexed = await worker.getReferenceSize(); - return indexed; - } + if (batch.length === 0) { + const indexed = await worker.getReferenceSize(); + return indexed; + } - await worker.indexBatch(batch); - completed += batch.length; + await worker.indexBatch(batch); + completed += batch.length; - indexProgress.next(Math.round((completed / classNames.length) * 100)); - } - })()); + indexProgress.next(Math.round((completed / classNames.length) * 100)); } - - const indexedCounts = await Promise.all(promises); - const totalIndexed = indexedCounts.reduce((sum, count) => sum + count, 0); - - const endTime = performance.now(); - const duration = ((endTime - startTime) / 1000).toFixed(2); - console.log(`Indexing completed in ${duration} seconds. Total indexed: ${totalIndexed}`); - indexProgress.next(-1); - } catch (error) { - // Reset promise on error so indexing can be retried - this.indexPromise = null; - throw error; - } finally { - await Promise.all(this.workers.map(worker => worker.setWorkerJar("", null))); - } + })(), + ); + } + + const indexedCounts = await Promise.all(promises); + const totalIndexed = indexedCounts.reduce((sum, count) => sum + count, 0); + + const endTime = performance.now(); + const duration = ((endTime - startTime) / 1000).toFixed(2); + console.log(`Indexing completed in ${duration} seconds. Total indexed: ${totalIndexed}`); + indexProgress.next(-1); + } catch (error) { + // Reset promise on error so indexing can be retried + this.indexPromise = null; + throw error; + } finally { + await Promise.all(this.workers.map((worker) => worker.setWorkerJar("", null))); } + } - async getReference(key: ReferenceKey): Promise { - await this.indexJar(); - - let results: Promise[] = []; + async getReference(key: ReferenceKey): Promise { + await this.indexJar(); - for (const worker of this.workers) { - results.push(worker.getReference(key)); - } + let results: Promise[] = []; - return Promise.all(results).then(arrays => arrays.flat()); + for (const worker of this.workers) { + results.push(worker.getReference(key)); } - async getClassData(): Promise { - if (this.classDataCache) { - return this.classDataCache; - } + return Promise.all(results).then((arrays) => arrays.flat()); + } - const dbResult = await db.classData.get(this.minecraftJar.jar.name); - if (dbResult) { - this.classDataCache = dbResult.classes; - return this.classDataCache; - } + async getClassData(): Promise { + if (this.classDataCache) { + return this.classDataCache; + } - try { - await this.indexJar(); + const dbResult = await db.classData.get(this.minecraftJar.jar.name); + if (dbResult) { + this.classDataCache = dbResult.classes; + return this.classDataCache; + } - let results: Promise[] = []; - for (const worker of this.workers) { - results.push(worker.getClassData()); - } + try { + await this.indexJar(); + + let results: Promise[] = []; + for (const worker of this.workers) { + results.push(worker.getClassData()); + } - const classDataStrings = await Promise.all(results).then(arrays => arrays.flat()); - this.classDataCache = classDataStrings.map(parseClassData); + const classDataStrings = await Promise.all(results).then((arrays) => arrays.flat()); + this.classDataCache = classDataStrings.map(parseClassData); - await db.classData.put({ - name: this.minecraftJar.jar.name, - classes: this.classDataCache, - }); + await db.classData.put({ + name: this.minecraftJar.jar.name, + classes: this.classDataCache, + }); - return this.classDataCache; - } finally { - this.destroy(); - } + return this.classDataCache; + } finally { + this.destroy(); } + } } let bytecodeWorker: JarIndexWorker | null = null; export async function getBytecode(classData: ArrayBufferLike[]): Promise { - if (!bytecodeWorker) { - bytecodeWorker = createWrorker(); - } - return bytecodeWorker.getBytecode(classData); + if (!bytecodeWorker) { + bytecodeWorker = createWrorker(); + } + return bytecodeWorker.getBytecode(classData); } function createWrorker() { - return new ComlinkWorker( - new URL("./JarIndexWorker", import.meta.url), - ); + return new ComlinkWorker(new URL("./JarIndexWorker", import.meta.url)); } diff --git a/src/workers/JarIndexWorker.ts b/src/workers/JarIndexWorker.ts index 7e4454e..4e2d89d 100644 --- a/src/workers/JarIndexWorker.ts +++ b/src/workers/JarIndexWorker.ts @@ -1,5 +1,5 @@ import { load } from "../../java/build/generated/teavm/wasm-gc/java.wasm-runtime.js"; -import indexerWasm from '../../java/build/generated/teavm/wasm-gc/java.wasm?url'; +import indexerWasm from "../../java/build/generated/teavm/wasm-gc/java.wasm?url"; import { openJar, type Jar } from "../utils/Jar.js"; import type { ReferenceKey, ReferenceString } from "./JarIndex.js"; @@ -8,72 +8,76 @@ export type ClassDataString = `${string}|${string}|${number}|${string}`; let indexerFunc: Indexer | null = null; const getIndexer = async (): Promise => { - if (!indexerFunc) { - try { - const teavm = await load(indexerWasm); - indexerFunc = teavm.exports as Indexer; - } catch (e) { - console.warn("Failed to load WASM module (non-compliant browser?), falling back to JS implementation", e); - indexerFunc = await import("../../java/build/generated/teavm/js/java.js") as unknown as Indexer; - } + if (!indexerFunc) { + try { + const teavm = await load(indexerWasm); + indexerFunc = teavm.exports as Indexer; + } catch (e) { + console.warn( + "Failed to load WASM module (non-compliant browser?), falling back to JS implementation", + e, + ); + indexerFunc = + (await import("../../java/build/generated/teavm/js/java.js")) as unknown as Indexer; } - return indexerFunc; + } + return indexerFunc; }; let jar: Jar | null = null; export const setWorkerJar = async (name: string, blob: Blob | null) => { - if (!blob) { - jar = null; - return; - } + if (!blob) { + jar = null; + return; + } - jar = await openJar(name, blob); + jar = await openJar(name, blob); }; export const indexBatch = async (classNames: string[]): Promise => { - if (!jar) { - throw new Error("Jar not set in worker"); - } + if (!jar) { + throw new Error("Jar not set in worker"); + } - const currentJar = jar; // Capture for closure - const arrayBufferPromises = classNames.map(async className => { - const entry = currentJar.entries[className]; - const data = await entry.blob(); - return data.arrayBuffer(); - }); + const currentJar = jar; // Capture for closure + const arrayBufferPromises = classNames.map(async (className) => { + const entry = currentJar.entries[className]; + const data = await entry.blob(); + return data.arrayBuffer(); + }); - const indexer = await getIndexer(); + const indexer = await getIndexer(); - for (const arrayBuffer of arrayBufferPromises) { - indexer.index(await arrayBuffer); - } + for (const arrayBuffer of arrayBufferPromises) { + indexer.index(await arrayBuffer); + } }; export const getReference = async (key: ReferenceKey): Promise<[ReferenceString]> => { - const indexer = await getIndexer(); - return indexer.getReference(key); + const indexer = await getIndexer(); + return indexer.getReference(key); }; export const getReferenceSize = async (): Promise => { - const indexer = await getIndexer(); - return indexer.getReferenceSize(); + const indexer = await getIndexer(); + return indexer.getReferenceSize(); }; export const getBytecode = async (classData: ArrayBufferLike[]): Promise => { - const indexer = await getIndexer(); - return indexer.getBytecode(classData); + const indexer = await getIndexer(); + return indexer.getBytecode(classData); }; export const getClassData = async (): Promise => { - const indexer = await getIndexer(); - return indexer.getClassData(); + const indexer = await getIndexer(); + return indexer.getClassData(); }; interface Indexer { - index(data: ArrayBufferLike): void; - getReference(key: ReferenceKey): [ReferenceString]; - getReferenceSize(): number; - getBytecode(classData: ArrayBufferLike[]): string; - getClassData(): ClassDataString[]; + index(data: ArrayBufferLike): void; + getReference(key: ReferenceKey): [ReferenceString]; + getReferenceSize(): number; + getBytecode(classData: ArrayBufferLike[]): string; + getClassData(): ClassDataString[]; } diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 6796a0e..6075e7e 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -5,10 +5,9 @@ import type { Jar } from "../../utils/Jar"; type DecompileWorker = typeof import("./worker"); function createWrorker() { - return new ComlinkWorker( - new URL("./worker", import.meta.url), - { name: "decompileWorker" } - ); + return new ComlinkWorker(new URL("./worker", import.meta.url), { + name: "decompileWorker", + }); } type WorkerInstance = ReturnType; @@ -17,161 +16,168 @@ let workers: WorkerInstance[] = []; let preferWasmRuntime = true; async function ensureWorkers(count: number) { - count = Math.min(count, MAX_THREADS); - if (workers.length >= count) return; + count = Math.min(count, MAX_THREADS); + if (workers.length >= count) return; - let newWorkers = Array.from( - { length: count - workers.length }, - () => createWrorker()); + let newWorkers = Array.from({ length: count - workers.length }, () => createWrorker()); - await Promise.all(newWorkers.map(w => w.loadVFRuntime(preferWasmRuntime))); - workers.push(...newWorkers); + await Promise.all(newWorkers.map((w) => w.loadVFRuntime(preferWasmRuntime))); + workers.push(...newWorkers); } async function findWorker(): Promise { - let i = 0; - if (workers.length > 0) { - const count = await Promise.all(workers.map(w => w.promiseCount())); - i = workers.reduce((a, _, b) => count[a] < count[b] ? a : b, 0); - if (count[i] === 0) return workers[i]; - } - - if (workers.length < (MAX_THREADS - 1)) { - i = workers.length; - await ensureWorkers(workers.length + 1); - } - - return workers[i]; + let i = 0; + if (workers.length > 0) { + const count = await Promise.all(workers.map((w) => w.promiseCount())); + i = workers.reduce((a, _, b) => (count[a] < count[b] ? a : b), 0); + if (count[i] === 0) return workers[i]; + } + + if (workers.length < MAX_THREADS - 1) { + i = workers.length; + await ensureWorkers(workers.length + 1); + } + + return workers[i]; } export async function setRuntime(preferWasm: boolean) { - preferWasmRuntime = preferWasm; - await Promise.all(workers.map(w => w.scheduleClose())); - workers = []; + preferWasmRuntime = preferWasm; + await Promise.all(workers.map((w) => w.scheduleClose())); + workers = []; } export async function setOptions(options: vf.Options) { - const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); - const state = new Uint32Array(sab); - state[0] = 0; + const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); + const state = new Uint32Array(sab); + state[0] = 0; - await Promise.all(workers.map(w => w.setOptions(options, sab))); + await Promise.all(workers.map((w) => w.setOptions(options, sab))); } export async function deleteCache(): Promise { - const worker = await findWorker(); - return await worker.clear(); + const worker = await findWorker(); + return await worker.clear(); } export type DecompileEntireJarOptions = { - threads?: number, - splits?: number, - logger?: (className: string, current: number, total: number) => void, + threads?: number; + splits?: number; + logger?: (className: string, current: number, total: number) => void; }; export type DecompileEntireJarTask = { - start: () => Promise, - stop: () => void; + start: () => Promise; + stop: () => void; }; -export function decompileEntireJar(jar: Jar, options?: DecompileEntireJarOptions): DecompileEntireJarTask { - const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); - const state = new Uint32Array(sab); - state[0] = 0; - - const dJar = new DecompileJar(jar); - return { - async start() { - try { - const classNames = dJar.classes.filter(n => !n.includes("$")); - options?.logger?.("Decompiling...", 0, classNames.length); - - const optThreads = Math.min(options?.threads ?? MAX_THREADS, MAX_THREADS); - const optSplits = options?.splits ?? 100; - - let current = 0; - const optLogger = options?.logger ? Comlink.proxy((i: number) => { - options.logger!(classNames[i], ++current, classNames.length); - }) : undefined; - - await ensureWorkers(optThreads); - const result = await Promise.all((workers - .slice(0, optThreads)) - .map(w => w.decompileMany(jar.name, jar.blob, classNames, sab, optSplits, optLogger))); - const total = result.reduce((acc, n) => acc + n, 0); - return total; - } finally { - // kill all workers - await setRuntime(preferWasmRuntime); - } - }, - stop() { - Atomics.store(state, 0, dJar.classes.length); - }, - }; +export function decompileEntireJar( + jar: Jar, + options?: DecompileEntireJarOptions, +): DecompileEntireJarTask { + const sab = new SharedArrayBuffer(Uint32Array.BYTES_PER_ELEMENT); + const state = new Uint32Array(sab); + state[0] = 0; + + const dJar = new DecompileJar(jar); + return { + async start() { + try { + const classNames = dJar.classes.filter((n) => !n.includes("$")); + options?.logger?.("Decompiling...", 0, classNames.length); + + const optThreads = Math.min(options?.threads ?? MAX_THREADS, MAX_THREADS); + const optSplits = options?.splits ?? 100; + + let current = 0; + const optLogger = options?.logger + ? Comlink.proxy((i: number) => { + options.logger!(classNames[i], ++current, classNames.length); + }) + : undefined; + + await ensureWorkers(optThreads); + const result = await Promise.all( + workers + .slice(0, optThreads) + .map((w) => w.decompileMany(jar.name, jar.blob, classNames, sab, optSplits, optLogger)), + ); + const total = result.reduce((acc, n) => acc + n, 0); + return total; + } finally { + // kill all workers + await setRuntime(preferWasmRuntime); + } + }, + stop() { + Atomics.store(state, 0, dJar.classes.length); + }, + }; } export async function decompileClass(className: string, jar: Jar): Promise { - className = className.replace(".class", ""); - const entry = jar.entries[`${className}.class`]; - - if (!entry) return { - className, - checksum: 0, - source: `// Class not found: ${className}`, - tokens: [], - language: "java", - }; + className = className.replace(".class", ""); + const entry = jar.entries[`${className}.class`]; - const jarClasses = new DecompileJar(jar).classes; - const classData: DecompileData = {}; - classData[className] = { - checksum: entry.crc32, - data: await entry.bytes(), + if (!entry) + return { + className, + checksum: 0, + source: `// Class not found: ${className}`, + tokens: [], + language: "java", }; - for (const classFile of jarClasses) { - if (!classFile.startsWith(`${className}\$`)) { - continue; - } + const jarClasses = new DecompileJar(jar).classes; + const classData: DecompileData = {}; + classData[className] = { + checksum: entry.crc32, + data: await entry.bytes(), + }; - const entry = jar.entries[`${classFile}.class`]; - classData[classFile] = { - checksum: entry.crc32, - data: await entry.bytes(), - }; + for (const classFile of jarClasses) { + if (!classFile.startsWith(`${className}\$`)) { + continue; } - const worker = await findWorker(); - return await worker.decompile(jarClasses, className, classData); + const entry = jar.entries[`${classFile}.class`]; + classData[classFile] = { + checksum: entry.crc32, + data: await entry.bytes(), + }; + } + + const worker = await findWorker(); + return await worker.decompile(jarClasses, className, classData); } export async function getClassBytecode(className: string, jar: Jar): Promise { - className = className.replace(".class", ""); - const entry = jar.entries[`${className}.class`]; - - if (!entry) return { - className, - checksum: 0, - source: `// Class not found: ${className}`, - tokens: [], - language: "bytecode", - }; + className = className.replace(".class", ""); + const entry = jar.entries[`${className}.class`]; - const classData: ArrayBufferLike[] = []; - const data = await entry.bytes(); - classData.push(data.buffer); + if (!entry) + return { + className, + checksum: 0, + source: `// Class not found: ${className}`, + tokens: [], + language: "bytecode", + }; - const jarClasses = new DecompileJar(jar).classes; - for (const classFile of jarClasses) { - if (!classFile.startsWith(`${className}\$`)) { - continue; - } + const classData: ArrayBufferLike[] = []; + const data = await entry.bytes(); + classData.push(data.buffer); - const data = await jar.entries[`${classFile}.class`].bytes(); - classData.push(data.buffer); + const jarClasses = new DecompileJar(jar).classes; + for (const classFile of jarClasses) { + if (!classFile.startsWith(`${className}\$`)) { + continue; } - const worker = await findWorker(); - return await worker.getClassBytecode(className, entry.crc32, classData); + const data = await jar.entries[`${classFile}.class`].bytes(); + classData.push(data.buffer); + } + + const worker = await findWorker(); + return await worker.getClassBytecode(className, entry.crc32, classData); } diff --git a/src/workers/decompile/types.ts b/src/workers/decompile/types.ts index 967fd7c..efb3861 100644 --- a/src/workers/decompile/types.ts +++ b/src/workers/decompile/types.ts @@ -2,46 +2,52 @@ import type { Token } from "../../logic/Tokens"; import type { Jar } from "../../utils/Jar"; export type DecompileResult = { - className: string; - checksum: number; - source: string; - tokens: Token[]; - language: 'java' | 'bytecode'; + className: string; + checksum: number; + source: string; + tokens: Token[]; + language: "java" | "bytecode"; }; -export type DecompileOption = { key: string, value: string; }; +export type DecompileOption = { key: string; value: string }; export type DecompileData = { - [className: string]: undefined | { + [className: string]: + | undefined + | { checksum: number; data: Uint8Array | Promise; - }; + }; }; export class DecompileJar { - jar: Jar; - proxy: DecompileData; + jar: Jar; + proxy: DecompileData; - constructor(jar: Jar) { - this.jar = jar; - this.proxy = new Proxy({}, { - get(_, className: string): DecompileData[""] { - const entry = jar.entries[className + ".class"]; - if (entry) return { - checksum: entry.crc32, - data: entry.bytes() - }; - } - }); - } + constructor(jar: Jar) { + this.jar = jar; + this.proxy = new Proxy( + {}, + { + get(_, className: string): DecompileData[""] { + const entry = jar.entries[className + ".class"]; + if (entry) + return { + checksum: entry.crc32, + data: entry.bytes(), + }; + }, + }, + ); + } - private _classes: string[] | null = null; - get classes() { - if (this._classes) return this._classes; - this._classes = Object.keys(this.jar.entries) - .filter(f => f.endsWith(".class")) - .map(f => f.replace(".class", "")) - .sort(); - return this._classes; - } + private _classes: string[] | null = null; + get classes() { + if (this._classes) return this._classes; + this._classes = Object.keys(this.jar.entries) + .filter((f) => f.endsWith(".class")) + .map((f) => f.replace(".class", "")) + .sort(); + return this._classes; + } } diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index 195e663..8968d46 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -2,7 +2,12 @@ import * as vf from "../../logic/vf"; import Dexie, { type EntityTable, type Table } from "dexie"; import type { Token } from "../../logic/Tokens"; import { getBytecode } from "../JarIndexWorker"; -import { type DecompileResult, type DecompileOption, type DecompileData, DecompileJar } from "./types"; +import { + type DecompileResult, + type DecompileOption, + type DecompileData, + DecompileJar, +} from "./types"; import { openJar } from "../../utils/Jar"; let lastPromise: Promise | undefined = undefined; @@ -10,40 +15,41 @@ let _promiseCount = 0; export const promiseCount = () => _promiseCount; async function schedule(fn: () => Promise): Promise { - try { - _promiseCount++; - if (lastPromise) await lastPromise; - lastPromise = fn(); - return await lastPromise as Promise; - } finally { - _promiseCount--; - lastPromise = undefined; - } + try { + _promiseCount++; + if (lastPromise) await lastPromise; + lastPromise = fn(); + return (await lastPromise) as Promise; + } finally { + _promiseCount--; + lastPromise = undefined; + } } export const scheduleClose = () => schedule(async () => close()); const db = new Dexie("decompiler") as Dexie & { - options: EntityTable, - results2: Table, + options: EntityTable; + results2: Table; }; db.version(2).stores({ - options: "key", - results2: "[className+checksum+language]", - // clear old data - results: null, + options: "key", + results2: "[className+checksum+language]", + // clear old data + results: null, }); let _options: vf.Options | undefined = undefined; export async function getOptions(): Promise { - if (_options) return _options; + if (_options) return _options; - const dbOptions = await db.options.toArray(); - _options = Object.fromEntries(dbOptions.map((it) => [it.key, it.value])); - return _options; + const dbOptions = await db.options.toArray(); + _options = Object.fromEntries(dbOptions.map((it) => [it.key, it.value])); + return _options; } -export const setOptions = (options: vf.Options, sab: SharedArrayBuffer) => schedule(async () => { +export const setOptions = (options: vf.Options, sab: SharedArrayBuffer) => + schedule(async () => { _options = undefined; // Only set the DB on one worker, should be propagated everywhere else. @@ -55,214 +61,265 @@ export const setOptions = (options: vf.Options, sab: SharedArrayBuffer) => sched let changed = false; const notVisited = new Set(Object.keys(options)); for (const dbOption of dbOptions) { - const option = options[dbOption.key]; - if (option !== dbOption.value) changed = true; - if (option) notVisited.delete(dbOption.key); + const option = options[dbOption.key]; + if (option !== dbOption.value) changed = true; + if (option) notVisited.delete(dbOption.key); } if (changed || notVisited.size > 0) { - await db.results2.clear(); + await db.results2.clear(); } await db.options.clear(); await db.options.bulkAdd(Object.entries(options).map(([k, v]) => ({ key: k, value: v }))); -}); + }); -export const loadVFRuntime = (preferWasm: boolean) => schedule(() => - vf.loadRuntime(preferWasm)); +export const loadVFRuntime = (preferWasm: boolean) => schedule(() => vf.loadRuntime(preferWasm)); -export const clear = (): Promise => schedule(async () => { +export const clear = (): Promise => + schedule(async () => { const count = await db.results2.count(); await db.results2.clear(); return count; -}); + }); export const decompileMany = ( - jarName: string, - jarBlob: Blob, - classNames: string[], - sab: SharedArrayBuffer, - splits: number, - logger?: (index: number) => Promise | void, -): Promise => schedule(async () => { + jarName: string, + jarBlob: Blob, + classNames: string[], + sab: SharedArrayBuffer, + splits: number, + logger?: (index: number) => Promise | void, +): Promise => + schedule(async () => { const state = new Uint32Array(sab); const jar = new DecompileJar(await openJar(jarName, jarBlob)); let logPromises: Promise[] = []; let nameLogger; if (logger) { - const class2index = new Map(classNames.map((v, i) => [v, i] as [string, number])); - nameLogger = (className: string) => { - if (!class2index) return; - const i = class2index.get(className); - if (i) logPromises.push(Promise.resolve(logger!(i))); - }; + const class2index = new Map(classNames.map((v, i) => [v, i] as [string, number])); + nameLogger = (className: string) => { + if (!class2index) return; + const i = class2index.get(className); + if (i) logPromises.push(Promise.resolve(logger!(i))); + }; } let count = 0; while (true) { - const i = Atomics.add(state, 0, splits); - if (i >= classNames.length) break; - - const targetClassNames: string[] = []; - for (let j = 0; j < splits; j++) { - if ((i + j) >= classNames.length) break; - - const className = classNames[i + j]; - const checksum = jar.proxy[className]?.checksum; - if (!checksum) continue; - - const dbCount = await db.results2 - .where("[className+checksum+language]") - .equals([className, checksum, "java"]) - .count(); - - if (dbCount >= 1) { - nameLogger?.(className); - } else { - targetClassNames.push(className); - } + const i = Atomics.add(state, 0, splits); + if (i >= classNames.length) break; + + const targetClassNames: string[] = []; + for (let j = 0; j < splits; j++) { + if (i + j >= classNames.length) break; + + const className = classNames[i + j]; + const checksum = jar.proxy[className]?.checksum; + if (!checksum) continue; + + const dbCount = await db.results2 + .where("[className+checksum+language]") + .equals([className, checksum, "java"]) + .count(); + + if (dbCount >= 1) { + nameLogger?.(className); + } else { + targetClassNames.push(className); } + } - try { - const result = await _decompile(jar.classes, targetClassNames, jar.proxy, nameLogger); - count += result.length; - } catch (e) { - console.error("Error during decompilation:", e); - } + try { + const result = await _decompile(jar.classes, targetClassNames, jar.proxy, nameLogger); + count += result.length; + } catch (e) { + console.error("Error during decompilation:", e); + } - await Promise.all(logPromises); - logPromises = []; + await Promise.all(logPromises); + logPromises = []; } return count; -}); + }); export const decompile = ( - jarClasses: string[], - className: string, - classData: DecompileData -): Promise => schedule(async () => { + jarClasses: string[], + className: string, + classData: DecompileData, +): Promise => + schedule(async () => { try { - const dbResult = await db.results2.get([className, classData[className]?.checksum, "java"]); - if (dbResult) return dbResult; + const dbResult = await db.results2.get([className, classData[className]?.checksum, "java"]); + if (dbResult) return dbResult; - const result = await _decompile(jarClasses, [className], classData); - return result[0]; + const result = await _decompile(jarClasses, [className], classData); + return result[0]; } catch (e) { - console.error(`Error during decompilation of class '${className}':`, e); - return { - className, - checksum: 0, - source: `// Error during decompilation: ${(e as Error).message}`, - tokens: [], - language: "java" - }; + console.error(`Error during decompilation of class '${className}':`, e); + return { + className, + checksum: 0, + source: `// Error during decompilation: ${(e as Error).message}`, + tokens: [], + language: "java", + }; } -}); + }); async function _decompile( - jarClasses: string[], - classNames: string[], - classData: DecompileData, - logger?: (className: string) => void, + jarClasses: string[], + classNames: string[], + classData: DecompileData, + logger?: (className: string) => void, ): Promise { - const allTokens: Record = {}; - let currentContent: string | undefined; - let currentTokens: Token[] | undefined; - let currentClassName: string | undefined; - - const sources = await vf.decompile(classNames, { - source: async (name) => await classData[name]?.data ?? null, - resources: jarClasses, - options: await getOptions(), - logger: { - writeMessage(level, message, error) { - switch (level) { - case "warn": console.warn(message); break; - case "error": console.error(message, error); break; - } - }, - startClass(className) { - currentClassName = className; - }, - endClass() { - if (logger && currentClassName) logger(currentClassName); - currentClassName = undefined; - }, - }, - tokenCollector: { - start(content) { - currentContent = content; - currentTokens = []; - }, - visitClass(start, length, declaration, name) { - currentTokens!.push({ type: "class", start, length, className: name, declaration }); - }, - visitField(start, length, declaration, className, name, descriptor) { - currentTokens!.push({ type: "field", start, length, className, declaration, name, descriptor }); - }, - visitMethod(start, length, declaration, className, name, descriptor) { - currentTokens!.push({ type: "method", start, length, className, declaration, name, descriptor }); - }, - visitParameter(start, length, declaration, className, _methodName, _methodDescriptor, _index, _name) { - currentTokens!.push({ type: "parameter", start, length, className, declaration }); - }, - visitLocal(start, length, declaration, className, _methodName, _methodDescriptor, _index, _name) { - currentTokens!.push({ type: "local", start, length, className, declaration }); - }, - end() { - allTokens[currentContent!] = currentTokens!; - currentContent = undefined; - currentTokens = undefined; - } - }, - }); - - const res: DecompileResult[] = []; - for (const [className, source] of Object.entries(sources)) { - const checksum = classData[className]?.checksum ?? 0; - const tokens = allTokens[source] ?? []; - - 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); - - tokens.push({ - type: "class", - start: match.index + match[0].lastIndexOf(className), - length: importPath.length - importPath.lastIndexOf(className), - className: importPath, - declaration: false - }); + const allTokens: Record = {}; + let currentContent: string | undefined; + let currentTokens: Token[] | undefined; + let currentClassName: string | undefined; + + const sources = await vf.decompile(classNames, { + source: async (name) => (await classData[name]?.data) ?? null, + resources: jarClasses, + options: await getOptions(), + logger: { + writeMessage(level, message, error) { + switch (level) { + case "warn": + console.warn(message); + break; + case "error": + console.error(message, error); + break; } - - tokens.sort((a, b) => a.start - b.start); - res.push({ className, checksum, source, tokens, language: "java" }); + }, + startClass(className) { + currentClassName = className; + }, + endClass() { + if (logger && currentClassName) logger(currentClassName); + currentClassName = undefined; + }, + }, + tokenCollector: { + start(content) { + currentContent = content; + currentTokens = []; + }, + visitClass(start, length, declaration, name) { + currentTokens!.push({ type: "class", start, length, className: name, declaration }); + }, + visitField(start, length, declaration, className, name, descriptor) { + currentTokens!.push({ + type: "field", + start, + length, + className, + declaration, + name, + descriptor, + }); + }, + visitMethod(start, length, declaration, className, name, descriptor) { + currentTokens!.push({ + type: "method", + start, + length, + className, + declaration, + name, + descriptor, + }); + }, + visitParameter( + start, + length, + declaration, + className, + _methodName, + _methodDescriptor, + _index, + _name, + ) { + currentTokens!.push({ type: "parameter", start, length, className, declaration }); + }, + visitLocal( + start, + length, + declaration, + className, + _methodName, + _methodDescriptor, + _index, + _name, + ) { + currentTokens!.push({ type: "local", start, length, className, declaration }); + }, + end() { + allTokens[currentContent!] = currentTokens!; + currentContent = undefined; + currentTokens = undefined; + }, + }, + }); + + const res: DecompileResult[] = []; + for (const [className, source] of Object.entries(sources)) { + const checksum = classData[className]?.checksum ?? 0; + const tokens = allTokens[source] ?? []; + + 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); + + tokens.push({ + type: "class", + start: match.index + match[0].lastIndexOf(className), + length: importPath.length - importPath.lastIndexOf(className), + className: importPath, + declaration: false, + }); } - await db.results2.bulkPut(res); - return res; + tokens.sort((a, b) => a.start - b.start); + res.push({ className, checksum, source, tokens, language: "java" }); + } + + await db.results2.bulkPut(res); + return res; } -export const getClassBytecode = (className: string, checksum: number, classData: ArrayBufferLike[]): Promise => schedule(async () => { +export const getClassBytecode = ( + className: string, + checksum: number, + classData: ArrayBufferLike[], +): Promise => + schedule(async () => { let result = await db.results2.get([className, checksum, "bytecode"]); if (result) return result; try { - const bytecode = await getBytecode(classData); - result = { className, checksum, source: bytecode, tokens: [], language: "bytecode" }; + const bytecode = await getBytecode(classData); + result = { className, checksum, source: bytecode, tokens: [], language: "bytecode" }; } catch (e) { - console.error(`Error during bytecode retrieval of class '${className}':`, e); - result = { className, checksum, source: `// Error during bytecode retrieval: ${(e as Error).message}`, tokens: [], language: "bytecode" }; + console.error(`Error during bytecode retrieval of class '${className}':`, e); + result = { + className, + checksum, + source: `// Error during bytecode retrieval: ${(e as Error).message}`, + tokens: [], + language: "bytecode", + }; } await db.results2.put(result); return result; -}); + }); diff --git a/tests/bytecode.spec.ts b/tests/bytecode.spec.ts index f3f27ed..77d14d6 100644 --- a/tests/bytecode.spec.ts +++ b/tests/bytecode.spec.ts @@ -1,18 +1,18 @@ -import { test, expect } from '@playwright/test'; -import { setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { setupTest } from "./test-utils"; -test.describe('Bytecode Setting', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - await page.addInitScript(() => { - localStorage.setItem('setting_bytecode', 'true'); - }); +test.describe("Bytecode Setting", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + await page.addInitScript(() => { + localStorage.setItem("setting_bytecode", "true"); }); + }); - test('Shows bytecode when enabled', async ({ page }) => { - await page.goto('/'); + test("Shows bytecode when enabled", async ({ page }) => { + await page.goto("/"); - const editor = page.getByRole("code").first(); - await expect(editor).toContainText('// access flags'); - }); + const editor = page.getByRole("code").first(); + await expect(editor).toContainText("// access flags"); + }); }); diff --git a/tests/decompile.spec.ts b/tests/decompile.spec.ts index d0db9d4..67cc59c 100644 --- a/tests/decompile.spec.ts +++ b/tests/decompile.spec.ts @@ -1,34 +1,34 @@ -import { expect, test } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { expect, test } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Decompilation', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Decompilation", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Decompiles default class on initial load', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); - }); + test("Decompiles default class on initial load", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); + }); - test('Decompile many classes', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Decompile many classes", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const modalButton = page.getByTestId('jar-decompiler').first(); - await modalButton.waitFor(); - await modalButton.click(); + const modalButton = page.getByTestId("jar-decompiler").first(); + await modalButton.waitFor(); + await modalButton.click(); - const splitsInput = page.getByTestId('jar-decompiler-splits').first(); - await splitsInput.waitFor(); - await splitsInput.fill('1'); + const splitsInput = page.getByTestId("jar-decompiler-splits").first(); + await splitsInput.waitFor(); + await splitsInput.fill("1"); - const okButton = page.getByTestId('jar-decompiler-ok').first(); - await okButton.waitFor(); - await okButton.click(); + const okButton = page.getByTestId("jar-decompiler-ok").first(); + await okButton.waitFor(); + await okButton.click(); - const result = page.getByTestId('jar-decompiler-result').first(); - await result.waitFor(); - await expect(result).toContainText(/Decompiled [1-9][0-9]* new classes in/); - }); + const result = page.getByTestId("jar-decompiler-result").first(); + await result.waitFor(); + await expect(result).toContainText(/Decompiled [1-9][0-9]* new classes in/); + }); }); diff --git a/tests/diff.spec.ts b/tests/diff.spec.ts index 53fed87..4b0c736 100644 --- a/tests/diff.spec.ts +++ b/tests/diff.spec.ts @@ -1,56 +1,62 @@ -import { test, expect } from '@playwright/test'; -import { setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { setupTest } from "./test-utils"; -test.describe('Diff View', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Diff View", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Opens diff view and selects LevelRenderer', async ({ page }) => { - await page.goto('/'); + test("Opens diff view and selects LevelRenderer", async ({ page }) => { + await page.goto("/"); - const versionSelect = page.locator('.ant-select').first(); - await versionSelect.click(); + const versionSelect = page.locator(".ant-select").first(); + await versionSelect.click(); - const compareOption = page.getByText('Compare', { exact: true }); - await compareOption.click(); + const compareOption = page.getByText("Compare", { exact: true }); + await compareOption.click(); - const diffEditor = page.locator('.monaco-diff-editor'); - await expect(diffEditor).toBeVisible(); + const diffEditor = page.locator(".monaco-diff-editor"); + await expect(diffEditor).toBeVisible(); - const fileListTable = page.locator('.ant-table-tbody'); - await expect(fileListTable.locator('tr').first()).toBeVisible(); + const fileListTable = page.locator(".ant-table-tbody"); + await expect(fileListTable.locator("tr").first()).toBeVisible(); - const leftVersionSelect = page.locator('.ant-select').nth(0); - await leftVersionSelect.click(); + const leftVersionSelect = page.locator(".ant-select").nth(0); + await leftVersionSelect.click(); - await expect(page.locator('.ant-select-dropdown:visible')).toBeVisible(); + await expect(page.locator(".ant-select-dropdown:visible")).toBeVisible(); - const leftOption = page.locator('.ant-select-dropdown:visible .ant-select-item-option').filter({ hasText: '26.1-mock-1' }).first(); - await leftOption.click(); + const leftOption = page + .locator(".ant-select-dropdown:visible .ant-select-item-option") + .filter({ hasText: "26.1-mock-1" }) + .first(); + await leftOption.click(); - const rightVersionSelect = page.locator('.ant-select').nth(1); - await expect(rightVersionSelect).toBeVisible(); - await rightVersionSelect.click(); + const rightVersionSelect = page.locator(".ant-select").nth(1); + await expect(rightVersionSelect).toBeVisible(); + await rightVersionSelect.click(); - await expect(page.locator('.ant-select-dropdown:visible')).toBeVisible(); + await expect(page.locator(".ant-select-dropdown:visible")).toBeVisible(); - const rightOption = page.locator('.ant-select-dropdown:visible .ant-select-item-option').filter({ hasText: '26.1-mock-2' }).first(); - await rightOption.click(); + const rightOption = page + .locator(".ant-select-dropdown:visible .ant-select-item-option") + .filter({ hasText: "26.1-mock-2" }) + .first(); + await rightOption.click(); - const searchInput = page.locator('input[placeholder="Search"]'); - await searchInput.fill('LevelRenderer'); + const searchInput = page.locator('input[placeholder="Search"]'); + await searchInput.fill("LevelRenderer"); - const firstFileRow = fileListTable.locator('tr').first(); - await expect(firstFileRow).toBeVisible(); - await firstFileRow.click(); + const firstFileRow = fileListTable.locator("tr").first(); + await expect(firstFileRow).toBeVisible(); + await firstFileRow.click(); - await expect(firstFileRow).toHaveClass(/ant-table-row-selected/); + await expect(firstFileRow).toHaveClass(/ant-table-row-selected/); - const decompilingMessage = page.getByText('Decompiling...'); - await expect(decompilingMessage).toBeHidden(); + const decompilingMessage = page.getByText("Decompiling..."); + await expect(decompilingMessage).toBeHidden(); - const editor = page.locator('.monaco-diff-editor'); - await expect(editor).toContainText('net.minecraft.client.renderer'); - }); + const editor = page.locator(".monaco-diff-editor"); + await expect(editor).toContainText("net.minecraft.client.renderer"); + }); }); diff --git a/tests/file-list.spec.ts b/tests/file-list.spec.ts index 9ce9f2c..5c14c43 100644 --- a/tests/file-list.spec.ts +++ b/tests/file-list.spec.ts @@ -1,49 +1,51 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('File List Navigation', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); - - test('Navigates to file via search', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); +test.describe("File List Navigation", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('LevelRenderer'); + test("Navigates to file via search", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchResult = page.getByText('net/minecraft/client/renderer/LevelRenderer', { exact: true }); - await expect(searchResult).toBeVisible(); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("LevelRenderer"); - await searchResult.click(); - await waitForDecompiledContent(page, 'class LevelRenderer'); + const searchResult = page.getByText("net/minecraft/client/renderer/LevelRenderer", { + exact: true, }); + await expect(searchResult).toBeVisible(); - test('Shows multiple search results', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + await searchResult.click(); + await waitForDecompiledContent(page, "class LevelRenderer"); + }); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('Renderer'); + test("Shows multiple search results", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchList = page.locator('.ant-list'); - await expect(searchList).toContainText('LevelRenderer'); - }); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("Renderer"); - test('Clears search and shows file tree', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + const searchList = page.locator(".ant-list"); + await expect(searchList).toContainText("LevelRenderer"); + }); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('LevelRenderer'); + test("Clears search and shows file tree", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - await page.waitForTimeout(500); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("LevelRenderer"); - await searchBox.clear(); + await page.waitForTimeout(500); - const fileTree = page.locator('.ant-tree').first(); - const netFolder = fileTree.getByText('net').first(); - await expect(netFolder).toBeVisible(); - }); + await searchBox.clear(); + + const fileTree = page.locator(".ant-tree").first(); + const netFolder = fileTree.getByText("net").first(); + await expect(netFolder).toBeVisible(); + }); }); diff --git a/tests/find-usages.spec.ts b/tests/find-usages.spec.ts index 64c2ea4..b799354 100644 --- a/tests/find-usages.spec.ts +++ b/tests/find-usages.spec.ts @@ -1,21 +1,21 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Find All References', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Find All References", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Triggers find all references action', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Triggers find all references action", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const methodToken = page.locator('.method-token-decoration').first(); - await methodToken.click(); + const methodToken = page.locator(".method-token-decoration").first(); + await methodToken.click(); - await page.keyboard.press('Alt+F12'); + await page.keyboard.press("Alt+F12"); - const editor = page.getByRole("code").first(); - await expect(editor).toBeVisible(); - }); + const editor = page.getByRole("code").first(); + await expect(editor).toBeVisible(); + }); }); diff --git a/tests/goto-definition.spec.ts b/tests/goto-definition.spec.ts index 66a0397..56c07eb 100644 --- a/tests/goto-definition.spec.ts +++ b/tests/goto-definition.spec.ts @@ -1,20 +1,23 @@ -import { test } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Go to Definition', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Go to Definition", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Ctrl+click on fromEnum navigates to StringRepresentable', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Ctrl+click on fromEnum navigates to StringRepresentable", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const methodToken = page.locator('.method-token-decoration-pointer').filter({ hasText: 'fromEnum' }).first(); - await methodToken.click(); + const methodToken = page + .locator(".method-token-decoration-pointer") + .filter({ hasText: "fromEnum" }) + .first(); + await methodToken.click(); - await page.keyboard.press('F12'); + await page.keyboard.press("F12"); - await waitForDecompiledContent(page, 'interface StringRepresentable'); - }); + await waitForDecompiledContent(page, "interface StringRepresentable"); + }); }); diff --git a/tests/inheritance.spec.ts b/tests/inheritance.spec.ts index f2a118a..615801b 100644 --- a/tests/inheritance.spec.ts +++ b/tests/inheritance.spec.ts @@ -1,35 +1,35 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Inheritance', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Inheritance", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Shows inheritance tree and graph views', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Shows inheritance tree and graph views", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const editor = page.locator('.monaco-editor').first(); - await editor.click({ button: 'right', position: { x: 100, y: 100 } }); - await page.waitForTimeout(500); + const editor = page.locator(".monaco-editor").first(); + await editor.click({ button: "right", position: { x: 100, y: 100 } }); + await page.waitForTimeout(500); - const inheritanceOption = page.getByText('View Inheritance Hierarchy'); - await inheritanceOption.click(); + const inheritanceOption = page.getByText("View Inheritance Hierarchy"); + await inheritanceOption.click(); - const treeTab = page.getByRole('tab', { name: 'Tree' }); - await expect(treeTab).toBeVisible(); - await expect(treeTab).toHaveAttribute('aria-selected', 'true'); + const treeTab = page.getByRole("tab", { name: "Tree" }); + await expect(treeTab).toBeVisible(); + await expect(treeTab).toHaveAttribute("aria-selected", "true"); - const modal = page.getByRole('dialog'); - const treeView = modal.locator('.ant-tree'); - await expect(treeView).toBeVisible(); + const modal = page.getByRole("dialog"); + const treeView = modal.locator(".ant-tree"); + await expect(treeView).toBeVisible(); - const graphTab = page.getByRole('tab', { name: 'Graph' }); - await graphTab.click(); - await expect(graphTab).toHaveAttribute('aria-selected', 'true'); + const graphTab = page.getByRole("tab", { name: "Graph" }); + await graphTab.click(); + await expect(graphTab).toHaveAttribute("aria-selected", "true"); - const graphView = modal.locator('.react-flow'); - await expect(graphView).toBeVisible(); - }); + const graphView = modal.locator(".react-flow"); + await expect(graphView).toBeVisible(); + }); }); diff --git a/tests/permalink.spec.ts b/tests/permalink.spec.ts index 61b3695..9e77769 100644 --- a/tests/permalink.spec.ts +++ b/tests/permalink.spec.ts @@ -1,59 +1,61 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Permalinks and Line Highlighting', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Permalinks and Line Highlighting", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Permalink with line range highlights multiple lines (new format)', async ({ page }) => { - await page.goto('/1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); + test("Permalink with line range highlights multiple lines (new format)", async ({ page }) => { + await page.goto("/1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90"); - await waitForDecompiledContent(page, 'class SystemReport'); + await waitForDecompiledContent(page, "class SystemReport"); - const editor = page.locator('.monaco-editor'); - const highlightedLines = editor.locator('.highlighted-line'); - await expect(highlightedLines.first()).toBeVisible(); - }); + const editor = page.locator(".monaco-editor"); + const highlightedLines = editor.locator(".highlighted-line"); + await expect(highlightedLines.first()).toBeVisible(); + }); - test('Permalink with line range highlights multiple lines (old hash format)', async ({ page }) => { - await page.goto('/#1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90'); + test("Permalink with line range highlights multiple lines (old hash format)", async ({ + page, + }) => { + await page.goto("/#1/26.1-snapshot-1/net/minecraft/SystemReport#L87-90"); - await waitForDecompiledContent(page, 'class SystemReport'); + await waitForDecompiledContent(page, "class SystemReport"); - const editor = page.locator('.monaco-editor'); - const highlightedLines = editor.locator('.highlighted-line'); - await expect(highlightedLines.first()).toBeVisible(); - }); + const editor = page.locator(".monaco-editor"); + const highlightedLines = editor.locator(".highlighted-line"); + await expect(highlightedLines.first()).toBeVisible(); + }); - test('Shift-clicking line number creates line range', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Shift-clicking line number creates line range", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const editor = page.locator('.monaco-editor'); - await expect(editor).toBeVisible(); + const editor = page.locator(".monaco-editor"); + await expect(editor).toBeVisible(); - // First click to select starting line - const lineNumbers = editor.locator('.line-numbers'); - await lineNumbers.first().click(); + // First click to select starting line + const lineNumbers = editor.locator(".line-numbers"); + await lineNumbers.first().click(); - // Wait for URL to update - await page.waitForTimeout(10); - const urlAfterFirstClick = page.url(); - expect(urlAfterFirstClick).toMatch(/\/1\/.*#L\d+$/); + // Wait for URL to update + await page.waitForTimeout(10); + const urlAfterFirstClick = page.url(); + expect(urlAfterFirstClick).toMatch(/\/1\/.*#L\d+$/); - // Shift-click on a different line to create range - await lineNumbers.nth(5).click({ modifiers: ['Shift'] }); + // Shift-click on a different line to create range + await lineNumbers.nth(5).click({ modifiers: ["Shift"] }); - // Wait for URL to update - await page.waitForTimeout(10); + // Wait for URL to update + await page.waitForTimeout(10); - // Check that URL now contains a line range (new path-based format) - expect(page.url()).toMatch(/\/1\/.*#L\d+-\d+$/); - expect(page.url()).not.toEqual(urlAfterFirstClick); + // Check that URL now contains a line range (new path-based format) + expect(page.url()).toMatch(/\/1\/.*#L\d+-\d+$/); + expect(page.url()).not.toEqual(urlAfterFirstClick); - // Check that lines are highlighted - const highlightedLine = editor.locator('.highlighted-line'); - await expect(highlightedLine.first()).toBeVisible(); - }); + // Check that lines are highlighted + const highlightedLine = editor.locator(".highlighted-line"); + await expect(highlightedLine.first()).toBeVisible(); + }); }); diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index 58b3a77..d6b48a1 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -1,84 +1,84 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Tabs', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Tabs", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Opens multiple tabs and switches between them', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Opens multiple tabs and switches between them", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('Minecraft'); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("Minecraft"); - const searchResult = page.getByText('net/minecraft/client/Minecraft', { exact: true }); - await searchResult.click(); - await page.waitForTimeout(500); + const searchResult = page.getByText("net/minecraft/client/Minecraft", { exact: true }); + await searchResult.click(); + await page.waitForTimeout(500); - await waitForDecompiledContent(page, 'class Minecraft'); + await waitForDecompiledContent(page, "class Minecraft"); - const tabs = page.locator('.ant-tabs-tab'); - await expect(tabs).toHaveCount(2); + const tabs = page.locator(".ant-tabs-tab"); + await expect(tabs).toHaveCount(2); - const chatFormattingTab = tabs.filter({ hasText: 'ChatFormatting' }); - await chatFormattingTab.click(); + const chatFormattingTab = tabs.filter({ hasText: "ChatFormatting" }); + await chatFormattingTab.click(); - await waitForDecompiledContent(page, 'enum ChatFormatting'); - }); + await waitForDecompiledContent(page, "enum ChatFormatting"); + }); - test('Closes tabs', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Closes tabs", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('Minecraft'); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("Minecraft"); - const searchResult = page.getByText('net/minecraft/client/Minecraft', { exact: true }); - await searchResult.click(); - await page.waitForTimeout(500); + const searchResult = page.getByText("net/minecraft/client/Minecraft", { exact: true }); + await searchResult.click(); + await page.waitForTimeout(500); - await waitForDecompiledContent(page, 'class Minecraft'); + await waitForDecompiledContent(page, "class Minecraft"); - const tabs = page.locator('.ant-tabs-tab'); - await expect(tabs).toHaveCount(2); + const tabs = page.locator(".ant-tabs-tab"); + await expect(tabs).toHaveCount(2); - const closeButton = tabs.filter({ hasText: 'Minecraft' }).locator('.ant-tabs-tab-remove'); - await closeButton.click(); + const closeButton = tabs.filter({ hasText: "Minecraft" }).locator(".ant-tabs-tab-remove"); + await closeButton.click(); - await expect(tabs).toHaveCount(1); - await waitForDecompiledContent(page, 'enum ChatFormatting'); - }); + await expect(tabs).toHaveCount(1); + await waitForDecompiledContent(page, "enum ChatFormatting"); + }); - test('Closes other tabs via context menu', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Closes other tabs via context menu", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); - await searchBox.fill('Minecraft'); - await page.getByText('net/minecraft/client/Minecraft', { exact: true }).click(); - await page.waitForTimeout(500); - await waitForDecompiledContent(page, 'class Minecraft'); + await searchBox.fill("Minecraft"); + await page.getByText("net/minecraft/client/Minecraft", { exact: true }).click(); + await page.waitForTimeout(500); + await waitForDecompiledContent(page, "class Minecraft"); - await searchBox.fill('SystemReport'); - await page.getByText('net/minecraft/SystemReport', { exact: true }).click(); - await page.waitForTimeout(500); - await waitForDecompiledContent(page, 'class SystemReport'); + await searchBox.fill("SystemReport"); + await page.getByText("net/minecraft/SystemReport", { exact: true }).click(); + await page.waitForTimeout(500); + await waitForDecompiledContent(page, "class SystemReport"); - const tabs = page.locator('.ant-tabs-tab'); - await expect(tabs).toHaveCount(3); + const tabs = page.locator(".ant-tabs-tab"); + await expect(tabs).toHaveCount(3); - const minecraftTab = tabs.filter({ hasText: 'Minecraft' }); - await page.waitForTimeout(50); - await minecraftTab.click({ button: 'right' }); + const minecraftTab = tabs.filter({ hasText: "Minecraft" }); + await page.waitForTimeout(50); + await minecraftTab.click({ button: "right" }); - const closeOthersOption = page.getByText('Close Other Tabs'); - await page.waitForTimeout(50); - await closeOthersOption.click(); + const closeOthersOption = page.getByText("Close Other Tabs"); + await page.waitForTimeout(50); + await closeOthersOption.click(); - await expect(tabs).toHaveCount(1); - await waitForDecompiledContent(page, 'class Minecraft'); - }); + await expect(tabs).toHaveCount(1); + await waitForDecompiledContent(page, "class Minecraft"); + }); }); diff --git a/tests/test-utils.ts b/tests/test-utils.ts index eff0cf7..e80e70c 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -1,83 +1,86 @@ -import { expect, Page } from '@playwright/test'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; +import { expect, Page } from "@playwright/test"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export async function waitForDecompiledContent(page: Page, expectedText: string) { - await expect(async () => { - const decompiling = page.getByText('Decompiling...'); - await expect(decompiling).toBeHidden(); - }).toPass(); + await expect(async () => { + const decompiling = page.getByText("Decompiling..."); + await expect(decompiling).toBeHidden(); + }).toPass(); - const editor = page.getByRole("code").nth(0); - await expect(editor).toContainText(expectedText); + const editor = page.getByRole("code").nth(0); + await expect(editor).toContainText(expectedText); } async function setupNetworkMocking(page: Page) { - const testVersions = { - versions: [ - { - id: "26.1-mock-3", - type: "snapshot", - url: "http://localhost:4173/test-data/dummy3-manifest.json", - releaseTime: "2026-02-11T09:31:23+00:00" - }, - { - id: "26.1-mock-2", - type: "snapshot", - url: "http://localhost:4173/test-data/dummy2-manifest.json", - releaseTime: "2026-02-03T12:46:52+00:00" - }, - { - id: "26.1-mock-1", - type: "snapshot", - url: "http://localhost:4173/test-data/dummy1-manifest.json", - releaseTime: "2026-01-13T12:47:34+00:00" - } - ] - }; + const testVersions = { + versions: [ + { + id: "26.1-mock-3", + type: "snapshot", + url: "http://localhost:4173/test-data/dummy3-manifest.json", + releaseTime: "2026-02-11T09:31:23+00:00", + }, + { + id: "26.1-mock-2", + type: "snapshot", + url: "http://localhost:4173/test-data/dummy2-manifest.json", + releaseTime: "2026-02-03T12:46:52+00:00", + }, + { + id: "26.1-mock-1", + type: "snapshot", + url: "http://localhost:4173/test-data/dummy1-manifest.json", + releaseTime: "2026-01-13T12:47:34+00:00", + }, + ], + }; - await page.route('https://piston-meta.mojang.com/mc/game/version_manifest_v2.json', async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify(testVersions) - }); - }); + await page.route( + "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json", + async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(testVersions), + }); + }, + ); - for (let i = 1; i <= 3; i++) { - await page.route(`http://localhost:4173/test-data/dummy${i}-manifest.json`, async (route) => { - await route.fulfill({ - status: 200, - contentType: 'application/json', - body: JSON.stringify({ - id: `26.1-mock-${i}`, - downloads: { - client: { - url: `http://localhost:4173/test-data/dummy${i}.jar` - } - } - }) - }); - }); + for (let i = 1; i <= 3; i++) { + await page.route(`http://localhost:4173/test-data/dummy${i}-manifest.json`, async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + id: `26.1-mock-${i}`, + downloads: { + client: { + url: `http://localhost:4173/test-data/dummy${i}.jar`, + }, + }, + }), + }); + }); - await page.route(`http://localhost:4173/test-data/dummy${i}.jar`, async (route) => { - const jarPath = path.join(__dirname, `../java/build/libs/dummy${i}.jar`); - const jarBuffer = fs.readFileSync(jarPath); - await route.fulfill({ - status: 200, - contentType: 'application/java-archive', - body: jarBuffer - }); - }); - } + await page.route(`http://localhost:4173/test-data/dummy${i}.jar`, async (route) => { + const jarPath = path.join(__dirname, `../java/build/libs/dummy${i}.jar`); + const jarBuffer = fs.readFileSync(jarPath); + await route.fulfill({ + status: 200, + contentType: "application/java-archive", + body: jarBuffer, + }); + }); + } } export async function setupTest(page: Page) { - await setupNetworkMocking(page); - await page.addInitScript(() => { - localStorage.setItem('setting_eula', 'true'); - }); + await setupNetworkMocking(page); + await page.addInitScript(() => { + localStorage.setItem("setting_eula", "true"); + }); } diff --git a/tests/version-switching.spec.ts b/tests/version-switching.spec.ts index 4fcf3cd..ad1bd33 100644 --- a/tests/version-switching.spec.ts +++ b/tests/version-switching.spec.ts @@ -1,53 +1,53 @@ -import { test, expect } from '@playwright/test'; -import { waitForDecompiledContent, setupTest } from './test-utils'; +import { test, expect } from "@playwright/test"; +import { waitForDecompiledContent, setupTest } from "./test-utils"; -test.describe('Version Switching', () => { - test.beforeEach(async ({ page }) => { - await setupTest(page); - }); +test.describe("Version Switching", () => { + test.beforeEach(async ({ page }) => { + await setupTest(page); + }); - test('Switches between Minecraft versions', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Switches between Minecraft versions", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const versionSelect = page.locator('.ant-select').first(); - await versionSelect.click(); - await page.waitForTimeout(500); + const versionSelect = page.locator(".ant-select").first(); + await versionSelect.click(); + await page.waitForTimeout(500); - const versionOptions = page.locator('.ant-select-dropdown:visible .ant-select-item-option'); - const firstVersion = versionOptions.nth(1); - await firstVersion.click(); + const versionOptions = page.locator(".ant-select-dropdown:visible .ant-select-item-option"); + const firstVersion = versionOptions.nth(1); + await firstVersion.click(); - await page.waitForTimeout(2000); + await page.waitForTimeout(2000); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const editor = page.getByRole('code').first(); - await expect(editor).toBeVisible(); - }); + const editor = page.getByRole("code").first(); + await expect(editor).toBeVisible(); + }); - test('Preserves file when switching versions', async ({ page }) => { - await page.goto('/'); - await waitForDecompiledContent(page, 'enum ChatFormatting'); + test("Preserves file when switching versions", async ({ page }) => { + await page.goto("/"); + await waitForDecompiledContent(page, "enum ChatFormatting"); - const searchBox = page.getByRole('searchbox', { name: 'Search classes' }); - await searchBox.fill('Minecraft'); + const searchBox = page.getByRole("searchbox", { name: "Search classes" }); + await searchBox.fill("Minecraft"); - const searchResult = page.getByText('net/minecraft/client/Minecraft', { exact: true }); - await searchResult.click(); + const searchResult = page.getByText("net/minecraft/client/Minecraft", { exact: true }); + await searchResult.click(); - await waitForDecompiledContent(page, 'class Minecraft'); + await waitForDecompiledContent(page, "class Minecraft"); - const versionSelect = page.locator('.ant-select').first(); - await versionSelect.click(); - await page.waitForTimeout(500); + const versionSelect = page.locator(".ant-select").first(); + await versionSelect.click(); + await page.waitForTimeout(500); - const versionOptions = page.locator('.ant-select-dropdown:visible .ant-select-item-option'); - const firstVersion = versionOptions.nth(1); - await firstVersion.click(); + const versionOptions = page.locator(".ant-select-dropdown:visible .ant-select-item-option"); + const firstVersion = versionOptions.nth(1); + await firstVersion.click(); - await page.waitForTimeout(2000); + await page.waitForTimeout(2000); - await waitForDecompiledContent(page, 'class Minecraft'); - }); + await waitForDecompiledContent(page, "class Minecraft"); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..d32ff68 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,4 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index f343fa7..1ef33fb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig } from "vitest/config"; import { comlink } from "vite-plugin-comlink"; -import react from '@vitejs/plugin-react'; -import svgr from 'vite-plugin-svgr'; +import react from "@vitejs/plugin-react"; +import svgr from "vite-plugin-svgr"; // https://vite.dev/config/ export default defineConfig({ @@ -10,13 +10,16 @@ export default defineConfig({ react(), svgr(), { - name: 'suppress-wasm-warnings', + name: "suppress-wasm-warnings", configResolved(config) { // oxlint-disable-next-line typescript/unbound-method const originalWarn = config.logger.warn; config.logger.warn = (msg, options) => { // Suppress WASM runtime externalization warnings - if (msg.includes('externalized for browser compatibility') && msg.includes('wasm-runtime.js')) { + if ( + msg.includes("externalized for browser compatibility") && + msg.includes("wasm-runtime.js") + ) { return; } originalWarn(msg, options); @@ -26,24 +29,24 @@ export default defineConfig({ ], worker: { plugins: () => [comlink()], - format: 'es', + format: "es", }, test: { - exclude: ['**/node_modules/**', '**/dist/**', 'tests/**'], + exclude: ["**/node_modules/**", "**/dist/**", "tests/**"], }, server: { headers: { // E2E tests will fail on WebKit if caching enabled. // Only seem to be a problem in localhost. // https://predr.ag/blog/debugging-safari-if-at-first-you-succeed/ - 'Cache-Control': 'no-store', - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', + "Cache-Control": "no-store", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", }, // For javadoc API during development proxy: { - '/v1': { - target: 'http://localhost:8080', + "/v1": { + target: "http://localhost:8080", changeOrigin: true, secure: false, }, @@ -55,14 +58,17 @@ export default defineConfig({ rollupOptions: { onwarn(warning, warn) { // Suppress "Module externalized for browser compatibility" warnings for WASM runtime files - if (warning.code === 'MODULE_EXTERNALIZED' && warning.message?.includes('wasm-runtime.js')) { + if ( + warning.code === "MODULE_EXTERNALIZED" && + warning.message?.includes("wasm-runtime.js") + ) { return; } warn(warning); }, output: { manualChunks: { - 'inheritance': ['@xyflow/react', 'dagre'], + inheritance: ["@xyflow/react", "dagre"], }, }, }, diff --git a/wrangler.jsonc b/wrangler.jsonc index f63f51a..3eaaf4a 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -4,6 +4,6 @@ "compatibility_date": "2026-02-06", "assets": { "directory": "./dist/", - "not_found_handling": "single-page-application" - } -} \ No newline at end of file + "not_found_handling": "single-page-application", + }, +} From 00b4d78a534132ec4366d9e1b25a9abfcdf9002d Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Sun, 15 Feb 2026 18:46:09 +0000 Subject: [PATCH 2/3] Fix format --- src/ui/intellij-icons/index.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ui/intellij-icons/index.tsx b/src/ui/intellij-icons/index.tsx index 4cff547..eaa27aa 100644 --- a/src/ui/intellij-icons/index.tsx +++ b/src/ui/intellij-icons/index.tsx @@ -68,17 +68,15 @@ export type ClassDataIconProps = IconProps & { data: ClassData }; export const ClassDataIcon: React.FC = (p) => { const { className, accessFlags, superName } = p.data; - // oxlint-disable-next-line no-constant-binary-expression - if (false || /^(.*\/)?package-info$/.test(className) || /^(.*\/)?module-info$/.test(className)) + if (/^(.*\/)?package-info$/.test(className) || /^(.*\/)?module-info$/.test(className)) { return ; + } if ((accessFlags & ACC_ANNOTATION) !== 0) return ; if ((accessFlags & ACC_INTERFACE) !== 0) return ; if ((accessFlags & ACC_ENUM) !== 0) return ; - // oxlint-disable-next-line no-constant-binary-expression if ( - false || superName === "java/lang/Exception" || superName === "java/lang/RuntimeException" || superName === "java/lang/Throwable" || From 3dc3c8e32a87d849755eb3404d4a4cdd989021ea Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Sun, 15 Feb 2026 18:49:11 +0000 Subject: [PATCH 3/3] Sort imports --- .oxfmtrc.json | 4 +++ src/javadoc/Javadoc.ts | 4 ++- src/javadoc/JavadocCmpletionProvider.ts | 1 + src/javadoc/JavadocCodeExtensions.ts | 4 ++- src/javadoc/JavadocMarkdownEditor.tsx | 8 +++-- src/javadoc/JavadocModal.tsx | 14 ++++---- src/javadoc/api/JavadocApi.ts | 1 + src/javadoc/api/LoginModal.tsx | 7 ++-- src/logic/Decompiler.ts | 12 ++++--- src/logic/Diff.ts | 6 ++-- src/logic/FindAllReferences.ts | 8 +++-- src/logic/Inheritance.ts | 1 + src/logic/JarFile.ts | 1 + src/logic/Keybinds.ts | 1 + src/logic/LineChanges.test.ts | 1 + src/logic/MinecraftApi.ts | 3 +- src/logic/Permalink.ts | 1 + src/logic/Search.test.ts | 1 + src/logic/Settings.ts | 1 + src/logic/State.ts | 3 +- src/logic/Tabs.ts | 3 +- src/logic/vf.ts | 3 +- src/main.tsx | 8 ++--- src/ui/AboutModal.tsx | 5 +-- src/ui/App.tsx | 13 +++---- src/ui/Code.tsx | 45 +++++++++++++------------ src/ui/CodeContextActions.ts | 4 ++- src/ui/CodeExtensions.ts | 9 +++-- src/ui/CodeHoverProvider.ts | 4 ++- src/ui/CodeUtils.ts | 1 + src/ui/FileList.tsx | 18 +++++----- src/ui/FilepathHeader.tsx | 5 +-- src/ui/Header.tsx | 5 +-- src/ui/IndexProgressNotification.tsx | 5 +-- src/ui/JarDecompilerModal.tsx | 9 ++--- src/ui/Modals.tsx | 6 ++-- src/ui/ProgressModal.tsx | 1 + src/ui/ReferenceResults.tsx | 10 +++--- src/ui/SearchResults.tsx | 3 +- src/ui/SettingsModal.tsx | 12 ++++--- src/ui/SideBar.tsx | 20 ++++++----- src/ui/StructureModal.tsx | 3 +- src/ui/StructureView.tsx | 10 +++--- src/ui/TabsComponent.tsx | 5 +-- src/ui/diff/DiffCode.tsx | 14 ++++---- src/ui/diff/DiffFileList.tsx | 16 +++++---- src/ui/diff/DiffVersionSelection.tsx | 5 +-- src/ui/diff/DiffView.tsx | 5 +-- src/ui/inheritance/InheritanceGraph.tsx | 7 ++-- src/ui/inheritance/InheritanceModal.tsx | 5 +-- src/ui/inheritance/InheritanceTree.tsx | 5 +-- src/ui/intellij-icons/index.tsx | 10 +++--- src/workers/JarIndex.ts | 6 ++-- src/workers/JarIndexWorker.ts | 5 +-- src/workers/decompile/client.ts | 4 ++- src/workers/decompile/worker.ts | 6 ++-- tests/bytecode.spec.ts | 1 + tests/decompile.spec.ts | 1 + tests/diff.spec.ts | 1 + tests/file-list.spec.ts | 1 + tests/find-usages.spec.ts | 1 + tests/goto-definition.spec.ts | 1 + tests/inheritance.spec.ts | 1 + tests/permalink.spec.ts | 1 + tests/tabs.spec.ts | 1 + tests/version-switching.spec.ts | 1 + vite.config.ts | 4 +-- 67 files changed, 240 insertions(+), 151 deletions(-) create mode 100644 .oxfmtrc.json diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..dff8992 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,4 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "experimentalSortImports": {} +} diff --git a/src/javadoc/Javadoc.ts b/src/javadoc/Javadoc.ts index b44c85a..3b41966 100644 --- a/src/javadoc/Javadoc.ts +++ b/src/javadoc/Javadoc.ts @@ -1,7 +1,9 @@ import { BehaviorSubject, map, Observable } from "rxjs"; + import type { Token } from "../logic/Tokens"; -import { javadocApi } from "./api/JavadocApi"; + import { selectedMinecraftVersion } from "../logic/State"; +import { javadocApi } from "./api/JavadocApi"; export type JavadocString = string; diff --git a/src/javadoc/JavadocCmpletionProvider.ts b/src/javadoc/JavadocCmpletionProvider.ts index a32cff2..96e6a11 100644 --- a/src/javadoc/JavadocCmpletionProvider.ts +++ b/src/javadoc/JavadocCmpletionProvider.ts @@ -1,4 +1,5 @@ import { editor, languages, Position, Token, type CancellationToken } from "monaco-editor"; + import type { MemberToken } from "../logic/Tokens"; import type { DecompileResult } from "../workers/decompile/types"; diff --git a/src/javadoc/JavadocCodeExtensions.ts b/src/javadoc/JavadocCodeExtensions.ts index 51447f3..ab95830 100644 --- a/src/javadoc/JavadocCodeExtensions.ts +++ b/src/javadoc/JavadocCodeExtensions.ts @@ -1,4 +1,7 @@ import { editor, languages, type CancellationToken, type IDisposable } from "monaco-editor"; + +import type { DecompileResult } from "../workers/decompile/types"; + import { getTokenLocation, type Token, type TokenLocation } from "../logic/Tokens"; import { activeJavadocToken, @@ -8,7 +11,6 @@ import { type JavadocData, type JavadocString, } from "./Javadoc"; -import type { DecompileResult } from "../workers/decompile/types"; type monaco = typeof import("monaco-editor"); diff --git a/src/javadoc/JavadocMarkdownEditor.tsx b/src/javadoc/JavadocMarkdownEditor.tsx index 2a15c70..a20d5ff 100644 --- a/src/javadoc/JavadocMarkdownEditor.tsx +++ b/src/javadoc/JavadocMarkdownEditor.tsx @@ -1,8 +1,10 @@ +import type { editor } from "monaco-editor"; + import { Editor, useMonaco } from "@monaco-editor/react"; -import { useObservable } from "../utils/UseObservable"; -import { currentResult } from "../logic/Decompiler"; import { useEffect, useRef } from "react"; -import type { editor } from "monaco-editor"; + +import { currentResult } from "../logic/Decompiler"; +import { useObservable } from "../utils/UseObservable"; import { JavdocCompletionProvider } from "./JavadocCmpletionProvider"; const JavadocMarkdownEditor = ({ diff --git a/src/javadoc/JavadocModal.tsx b/src/javadoc/JavadocModal.tsx index d683994..f6ff5b9 100644 --- a/src/javadoc/JavadocModal.tsx +++ b/src/javadoc/JavadocModal.tsx @@ -1,12 +1,14 @@ import { Modal, Button, message } from "antd"; -import { activeJavadocToken, getJavadocForToken, javadocData, setTokenJavadoc } from "./Javadoc"; -import { useObservable } from "../utils/UseObservable"; -import { IS_JAVADOC_EDITOR } from "../site"; -import type { Token } from "../logic/Tokens"; -import JavadocMarkdownEditor from "./JavadocMarkdownEditor"; import { useMemo, useState } from "react"; -import { javadocApi, type UpdateTarget } from "./api/JavadocApi"; + +import type { Token } from "../logic/Tokens"; + import { selectedMinecraftVersion } from "../logic/State"; +import { IS_JAVADOC_EDITOR } from "../site"; +import { useObservable } from "../utils/UseObservable"; +import { javadocApi, type UpdateTarget } from "./api/JavadocApi"; +import { activeJavadocToken, getJavadocForToken, javadocData, setTokenJavadoc } from "./Javadoc"; +import JavadocMarkdownEditor from "./JavadocMarkdownEditor"; const ModalBody = ({ token, diff --git a/src/javadoc/api/JavadocApi.ts b/src/javadoc/api/JavadocApi.ts index 043b526..df6af1d 100644 --- a/src/javadoc/api/JavadocApi.ts +++ b/src/javadoc/api/JavadocApi.ts @@ -1,4 +1,5 @@ import { BehaviorSubject, map } from "rxjs"; + import { IS_JAVADOC_EDITOR } from "../../site"; class JavadocApi { diff --git a/src/javadoc/api/LoginModal.tsx b/src/javadoc/api/LoginModal.tsx index 8d05b96..ccd79a1 100644 --- a/src/javadoc/api/LoginModal.tsx +++ b/src/javadoc/api/LoginModal.tsx @@ -1,10 +1,11 @@ -import { Modal, Button, Divider, message } from "antd"; import { GithubOutlined } from "@ant-design/icons"; +import { Modal, Button, Divider, message } from "antd"; +import { useState } from "react"; + +import { agreedEula } from "../../logic/Settings"; import { IS_JAVADOC_EDITOR } from "../../site"; import { useObservable } from "../../utils/UseObservable"; import { javadocApi } from "./JavadocApi"; -import { useState } from "react"; -import { agreedEula } from "../../logic/Settings"; const LoginModal = () => { if (!IS_JAVADOC_EDITOR) { diff --git a/src/logic/Decompiler.ts b/src/logic/Decompiler.ts index f79d5dd..0c9543a 100644 --- a/src/logic/Decompiler.ts +++ b/src/logic/Decompiler.ts @@ -12,13 +12,15 @@ import { tap, throttleTime, } from "rxjs"; -import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; -import { selectedFile } from "./State"; -import { bytecode, displayLambdas } from "./Settings"; -import type { Options } from "./vf"; + +import type { Jar } from "../utils/Jar"; import type { DecompileResult } from "../workers/decompile/types"; +import type { Options } from "./vf"; + import * as worker from "../workers/decompile/client"; -import type { Jar } from "../utils/Jar"; +import { minecraftJar, type MinecraftJar } from "./MinecraftApi"; +import { bytecode, displayLambdas } from "./Settings"; +import { selectedFile } from "./State"; const decompilerCounter = new BehaviorSubject(0); diff --git a/src/logic/Diff.ts b/src/logic/Diff.ts index 9613cf2..c5d11e3 100644 --- a/src/logic/Diff.ts +++ b/src/logic/Diff.ts @@ -7,11 +7,13 @@ import { switchMap, shareReplay, } from "rxjs"; -import { minecraftJar, minecraftJarPipeline, type MinecraftJar } from "./MinecraftApi"; + +import type { DecompileResult } from "../workers/decompile/types"; + import { currentResult, decompileResultPipeline } from "./Decompiler"; import { calculatedLineChanges } from "./LineChanges"; +import { minecraftJar, minecraftJarPipeline, type MinecraftJar } from "./MinecraftApi"; import { diffLeftselectedMinecraftVersion, selectedMinecraftVersion } from "./State"; -import type { DecompileResult } from "../workers/decompile/types"; export const hideUnchangedSizes = new BehaviorSubject(false); diff --git a/src/logic/FindAllReferences.ts b/src/logic/FindAllReferences.ts index 243eda6..97bc5a4 100644 --- a/src/logic/FindAllReferences.ts +++ b/src/logic/FindAllReferences.ts @@ -8,11 +8,13 @@ import { switchMap, throttleTime, } from "rxjs"; + +import type { DecompileResult } from "../workers/decompile/types"; +import type { Token } from "./Tokens"; + import { jarIndex, type ReferenceKey, type ReferenceString } from "../workers/JarIndex"; -import { openTab } from "./Tabs"; import { referencesQuery } from "./State"; -import type { Token } from "./Tokens"; -import type { DecompileResult } from "../workers/decompile/types"; +import { openTab } from "./Tabs"; export const referenceResults = referencesQuery.pipe( throttleTime(200), diff --git a/src/logic/Inheritance.ts b/src/logic/Inheritance.ts index 56a9443..8202389 100644 --- a/src/logic/Inheritance.ts +++ b/src/logic/Inheritance.ts @@ -7,6 +7,7 @@ import { shareReplay, switchMap, } from "rxjs"; + import { jarIndex } from "../workers/JarIndex"; import { minecraftJar } from "./MinecraftApi"; diff --git a/src/logic/JarFile.ts b/src/logic/JarFile.ts index 9f36bc8..d665542 100644 --- a/src/logic/JarFile.ts +++ b/src/logic/JarFile.ts @@ -8,6 +8,7 @@ import { switchMap, throttleTime, } from "rxjs"; + import { minecraftJar } from "./MinecraftApi"; import { performSearch } from "./Search"; import { searchQuery } from "./State"; diff --git a/src/logic/Keybinds.ts b/src/logic/Keybinds.ts index a099d93..0a532a2 100644 --- a/src/logic/Keybinds.ts +++ b/src/logic/Keybinds.ts @@ -1,4 +1,5 @@ import { BehaviorSubject, filter, fromEvent, Observable, tap } from "rxjs"; + import { focusSearch, showStructure, type KeybindSetting } from "./Settings"; // Set to true when the user is currently capturing a keybind diff --git a/src/logic/LineChanges.test.ts b/src/logic/LineChanges.test.ts index b25aed9..c124705 100644 --- a/src/logic/LineChanges.test.ts +++ b/src/logic/LineChanges.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; + import { countLineDiff, updateLineChanges, calculatedLineChanges } from "./LineChanges"; describe("LineChanges", () => { diff --git a/src/logic/MinecraftApi.ts b/src/logic/MinecraftApi.ts index 28266ee..a752b99 100644 --- a/src/logic/MinecraftApi.ts +++ b/src/logic/MinecraftApi.ts @@ -11,8 +11,9 @@ import { Observable, withLatestFrom, } from "rxjs"; -import { agreedEula } from "./Settings"; + import { openJar, type Jar } from "../utils/Jar"; +import { agreedEula } from "./Settings"; import { selectedMinecraftVersion } from "./State"; const CACHE_NAME = "mcsrc-v1"; diff --git a/src/logic/Permalink.ts b/src/logic/Permalink.ts index 831b433..7ded4ec 100644 --- a/src/logic/Permalink.ts +++ b/src/logic/Permalink.ts @@ -1,4 +1,5 @@ import { combineLatest } from "rxjs"; + import { resetPermalinkAffectingSettings, supportsPermalinking } from "./Settings"; import { diffView, selectedFile, selectedLines, selectedMinecraftVersion } from "./State"; diff --git a/src/logic/Search.test.ts b/src/logic/Search.test.ts index 1c3d396..3294843 100644 --- a/src/logic/Search.test.ts +++ b/src/logic/Search.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; + import { performSearch, getCamelCaseAcronym, matchesCamelCase } from "./Search"; describe("Search Algorithm", () => { diff --git a/src/logic/Settings.ts b/src/logic/Settings.ts index 97b2422..c33b289 100644 --- a/src/logic/Settings.ts +++ b/src/logic/Settings.ts @@ -7,6 +7,7 @@ import { Observable, switchMap, } from "rxjs"; + import * as decompiler from "../workers/decompile/client"; export type ModifierKey = "Ctrl" | "Alt" | "Shift"; diff --git a/src/logic/State.ts b/src/logic/State.ts index 04a9f8f..92ae8e1 100644 --- a/src/logic/State.ts +++ b/src/logic/State.ts @@ -1,7 +1,8 @@ import { BehaviorSubject } from "rxjs"; import { pairwise } from "rxjs/operators"; -import { Tab } from "./Tabs"; + import { getInitialState } from "./Permalink"; +import { Tab } from "./Tabs"; const initialState = getInitialState(); diff --git a/src/logic/Tabs.ts b/src/logic/Tabs.ts index 377820a..5451d29 100644 --- a/src/logic/Tabs.ts +++ b/src/logic/Tabs.ts @@ -1,5 +1,6 @@ -import { enableTabs } from "./Settings"; import { editor } from "monaco-editor"; + +import { enableTabs } from "./Settings"; import { selectedFile, openTabs, tabHistory } from "./State"; export class Tab { diff --git a/src/logic/vf.ts b/src/logic/vf.ts index 3fd5bd4..5ec4572 100644 --- a/src/logic/vf.ts +++ b/src/logic/vf.ts @@ -1,6 +1,7 @@ +import type * as vf from "@run-slicer/vf"; + import wasmPath from "@run-slicer/vf/vf.wasm?url"; import { load } from "@run-slicer/vf/vf.wasm-runtime.js"; -import type * as vf from "@run-slicer/vf"; export type * from "@run-slicer/vf"; diff --git a/src/main.tsx b/src/main.tsx index 3f6c56e..95753ec 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,11 @@ +import { loader } from "@monaco-editor/react"; +import * as monaco from "monaco-editor"; +import MonacoWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import * as monaco from "monaco-editor"; -import { loader } from "@monaco-editor/react"; -import App from "./ui/App.tsx"; import "./index.css"; -import MonacoWorker from "monaco-editor/esm/vs/editor/editor.worker.js?worker"; +import App from "./ui/App.tsx"; // Dont load monaco from 3rd party CDN. loader.config({ monaco }); diff --git a/src/ui/AboutModal.tsx b/src/ui/AboutModal.tsx index 20a75c4..70b7957 100644 --- a/src/ui/AboutModal.tsx +++ b/src/ui/AboutModal.tsx @@ -1,9 +1,10 @@ +import { InfoCircleOutlined } from "@ant-design/icons"; import { Button, Checkbox, Modal } from "antd"; import { useState } from "react"; +import { BehaviorSubject } from "rxjs"; + import { agreedEula } from "../logic/Settings"; -import { InfoCircleOutlined } from "@ant-design/icons"; import { useObservable } from "../utils/UseObservable"; -import { BehaviorSubject } from "rxjs"; export const aboutModalOpen = new BehaviorSubject(false); diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 4fac1ff..95fbafa 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -1,16 +1,17 @@ +import { MenuFoldOutlined } from "@ant-design/icons"; import { Button, ConfigProvider, Drawer, Flex, Splitter, theme } from "antd"; -import Code from "./Code.tsx"; -import SideBar from "./SideBar.tsx"; import { useState } from "react"; -import { useObservable } from "../utils/UseObservable.ts"; + import { isThin } from "../logic/Browser.ts"; +import { enableTabs } from "../logic/Settings.ts"; import { diffView, mobileDrawerOpen } from "../logic/State"; +import { useObservable } from "../utils/UseObservable.ts"; +import Code from "./Code.tsx"; import DiffView from "./diff/DiffView.tsx"; import { FilepathHeader } from "./FilepathHeader.tsx"; -import { enableTabs } from "../logic/Settings.ts"; -import { MenuFoldOutlined } from "@ant-design/icons"; -import { TabsComponent } from "./TabsComponent.tsx"; import Modals from "./Modals.tsx"; +import SideBar from "./SideBar.tsx"; +import { TabsComponent } from "./TabsComponent.tsx"; const App = () => { const isSmall = useObservable(isThin); diff --git a/src/ui/Code.tsx b/src/ui/Code.tsx index b33f104..0839a82 100644 --- a/src/ui/Code.tsx +++ b/src/ui/Code.tsx @@ -1,22 +1,31 @@ +import { LoadingOutlined } from "@ant-design/icons"; import Editor, { useMonaco } from "@monaco-editor/react"; -import { useObservable } from "../utils/UseObservable"; -import { currentResult, isDecompiling } from "../logic/Decompiler"; -import { useEffect, useRef, useState } from "react"; +import { message, Spin } from "antd"; import { editor, Range } from "monaco-editor"; +import { useEffect, useRef, useState } from "react"; +import { pairwise, startWith } from "rxjs"; + +import { applyJavadocCodeExtensions } from "../javadoc/JavadocCodeExtensions"; import { isThin } from "../logic/Browser"; +import { currentResult, isDecompiling } from "../logic/Decompiler"; +import { getNextJumpToken, nextReferenceNavigation } from "../logic/FindAllReferences"; +import { selectedInheritanceClassName } from "../logic/Inheritance"; import { classesList } from "../logic/JarFile"; +import { bytecode } from "../logic/Settings"; +import { + selectedFile, + diffView, + openTabs, + selectedLines, + tabHistory, + referencesQuery, + mobileDrawerOpen, +} from "../logic/State"; import { getOpenTab } from "../logic/Tabs"; -import { message, Spin } from "antd"; -import { LoadingOutlined } from "@ant-design/icons"; import { getTokenLocation } from "../logic/Tokens"; -import { pairwise, startWith } from "rxjs"; -import { getNextJumpToken, nextReferenceNavigation } from "../logic/FindAllReferences"; -import { setupJavaBytecodeLanguage } from "../utils/JavaBytecode"; import { IS_JAVADOC_EDITOR } from "../site"; -import { applyJavadocCodeExtensions } from "../javadoc/JavadocCodeExtensions"; -import { selectedInheritanceClassName } from "../logic/Inheritance"; -import { createHoverProvider } from "./CodeHoverProvider"; -import { findTokenAtPosition } from "./CodeUtils"; +import { setupJavaBytecodeLanguage } from "../utils/JavaBytecode"; +import { useObservable } from "../utils/UseObservable"; import { IS_DEFINITION_CONTEXT_KEY_NAME, createCopyAwAction, @@ -32,16 +41,8 @@ import { jumpToToken, pendingTokenJump, } from "./CodeExtensions"; -import { bytecode } from "../logic/Settings"; -import { - selectedFile, - diffView, - openTabs, - selectedLines, - tabHistory, - referencesQuery, - mobileDrawerOpen, -} from "../logic/State"; +import { createHoverProvider } from "./CodeHoverProvider"; +import { findTokenAtPosition } from "./CodeUtils"; const Code = () => { const monaco = useMonaco(); diff --git a/src/ui/CodeContextActions.ts b/src/ui/CodeContextActions.ts index c44a486..fb46899 100644 --- a/src/ui/CodeContextActions.ts +++ b/src/ui/CodeContextActions.ts @@ -1,7 +1,9 @@ import type { editor } from "monaco-editor"; -import { findTokenAtPosition } from "./CodeUtils"; + import type { DecompileResult } from "../workers/decompile/types"; +import { findTokenAtPosition } from "./CodeUtils"; + export const IS_DEFINITION_CONTEXT_KEY_NAME = "is_definition"; async function setClipboard(text: string): Promise { diff --git a/src/ui/CodeExtensions.ts b/src/ui/CodeExtensions.ts index 5d6d22a..5c46742 100644 --- a/src/ui/CodeExtensions.ts +++ b/src/ui/CodeExtensions.ts @@ -1,10 +1,13 @@ import type { CancellationToken, IPosition, IRange, languages } from "monaco-editor"; + import { editor, Range, Uri } from "monaco-editor"; +import { BehaviorSubject } from "rxjs"; + +import type { DecompileResult } from "../workers/decompile/types"; + +import { selectedFile } from "../logic/State"; import { openTab } from "../logic/Tabs"; import { getTokenLocation } from "../logic/Tokens"; -import { selectedFile } from "../logic/State"; -import type { DecompileResult } from "../workers/decompile/types"; -import { BehaviorSubject } from "rxjs"; export type TokenJumpTarget = { className: string; diff --git a/src/ui/CodeHoverProvider.ts b/src/ui/CodeHoverProvider.ts index 8724c6c..7400a64 100644 --- a/src/ui/CodeHoverProvider.ts +++ b/src/ui/CodeHoverProvider.ts @@ -1,7 +1,9 @@ import { editor, type IMarkdownString, type IPosition, Range } from "monaco-editor"; + +import type { DecompileResult } from "../workers/decompile/types"; + import { type Token } from "../logic/Tokens"; import { findTokenAtPosition } from "./CodeUtils"; -import type { DecompileResult } from "../workers/decompile/types"; interface IntegerLiteral { value: number; diff --git a/src/ui/CodeUtils.ts b/src/ui/CodeUtils.ts index 5bc7a3f..2861c98 100644 --- a/src/ui/CodeUtils.ts +++ b/src/ui/CodeUtils.ts @@ -1,4 +1,5 @@ import { editor } from "monaco-editor"; + import { type Token } from "../logic/Tokens"; export function findTokenAtPosition( diff --git a/src/ui/FileList.tsx b/src/ui/FileList.tsx index 74e5331..2535d7d 100644 --- a/src/ui/FileList.tsx +++ b/src/ui/FileList.tsx @@ -1,17 +1,19 @@ -// oxlint-disable typescript/no-base-to-string -import { Tree, Dropdown, message } from "antd"; import type { TreeDataNode, TreeProps, MenuProps } from "antd"; +import type { Key } from "antd/es/table/interface"; + import { CaretDownFilled } from "@ant-design/icons"; +// oxlint-disable typescript/no-base-to-string +import { Tree, Dropdown, message } from "antd"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { combineLatest, from, map, Observable, shareReplay, switchMap, startWith } from "rxjs"; + +import { decompileClass } from "../logic/Decompiler"; import { classesList } from "../logic/JarFile"; -import { useObservable } from "../utils/UseObservable"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import type { Key } from "antd/es/table/interface"; -import { openTab } from "../logic/Tabs"; import { minecraftJar, type MinecraftJar } from "../logic/MinecraftApi"; -import { decompileClass } from "../logic/Decompiler"; -import { selectedFile, referencesQuery } from "../logic/State"; import { compactPackages } from "../logic/Settings"; +import { selectedFile, referencesQuery } from "../logic/State"; +import { openTab } from "../logic/Tabs"; +import { useObservable } from "../utils/UseObservable"; import { jarIndex, type ClassData } from "../workers/JarIndex"; import { ClassDataIcon, JavaIcon, PackageIcon } from "./intellij-icons"; diff --git a/src/ui/FilepathHeader.tsx b/src/ui/FilepathHeader.tsx index 16ddcc8..d6dd690 100644 --- a/src/ui/FilepathHeader.tsx +++ b/src/ui/FilepathHeader.tsx @@ -1,8 +1,9 @@ import { theme } from "antd"; -import { useObservable } from "../utils/UseObservable"; -import { getDiffChanges } from "../logic/Diff"; import { combineLatest, map } from "rxjs"; + +import { getDiffChanges } from "../logic/Diff"; import { selectedFile, diffView } from "../logic/State"; +import { useObservable } from "../utils/UseObservable"; const changeInfoObs = combineLatest([selectedFile, getDiffChanges(), diffView]).pipe( map(([file, changes, isDiff]) => { diff --git a/src/ui/Header.tsx b/src/ui/Header.tsx index 0a86cbd..4e9699c 100644 --- a/src/ui/Header.tsx +++ b/src/ui/Header.tsx @@ -1,10 +1,11 @@ import { Divider, Flex, Select, Space } from "antd"; + import { minecraftVersionIds } from "../logic/MinecraftApi"; +import { diffView, selectedMinecraftVersion } from "../logic/State"; import { useObservable } from "../utils/UseObservable"; import { AboutModalButton } from "./AboutModal"; -import { SettingsModalButton } from "./SettingsModal"; -import { diffView, selectedMinecraftVersion } from "../logic/State"; import { JarDecompilerModalButton } from "./JarDecompilerModal"; +import { SettingsModalButton } from "./SettingsModal"; const Header = () => { return ( diff --git a/src/ui/IndexProgressNotification.tsx b/src/ui/IndexProgressNotification.tsx index 94aa0e2..9788410 100644 --- a/src/ui/IndexProgressNotification.tsx +++ b/src/ui/IndexProgressNotification.tsx @@ -1,8 +1,9 @@ import { notification, Progress } from "antd"; -import { useObservable } from "../utils/UseObservable"; +import { useEffect } from "react"; import { distinctUntilChanged, map } from "rxjs"; + +import { useObservable } from "../utils/UseObservable"; import { indexProgress } from "../workers/JarIndex"; -import { useEffect } from "react"; const distinctJarIndexProgress = indexProgress.pipe(map(Math.round), distinctUntilChanged()); diff --git a/src/ui/JarDecompilerModal.tsx b/src/ui/JarDecompilerModal.tsx index 50fbd1d..c336328 100644 --- a/src/ui/JarDecompilerModal.tsx +++ b/src/ui/JarDecompilerModal.tsx @@ -1,20 +1,21 @@ -import { Alert, Button, Flex, Form, message, Modal, Popconfirm, Progress } from "antd"; import { JavaOutlined } from "@ant-design/icons"; +import { Alert, Button, Flex, Form, message, Modal, Popconfirm, Progress } from "antd"; import { BehaviorSubject } from "rxjs"; -import { useObservable } from "../utils/UseObservable"; -import { BooleanOption, NumberOption } from "./SettingsModal"; + +import { minecraftJar } from "../logic/MinecraftApi"; import { decompilerSplits, decompilerThreads, MAX_THREADS, preferWasmDecompiler, } from "../logic/Settings"; +import { useObservable } from "../utils/UseObservable"; import { decompileEntireJar, deleteCache, type DecompileEntireJarTask, } from "../workers/decompile/client"; -import { minecraftJar } from "../logic/MinecraftApi"; +import { BooleanOption, NumberOption } from "./SettingsModal"; const modalOpen = new BehaviorSubject(false); diff --git a/src/ui/Modals.tsx b/src/ui/Modals.tsx index d63a130..73a52d7 100644 --- a/src/ui/Modals.tsx +++ b/src/ui/Modals.tsx @@ -1,12 +1,12 @@ import LoginModal from "../javadoc/api/LoginModal"; import JavadocModal from "../javadoc/JavadocModal"; +import AboutModal from "./AboutModal"; +import IndexProgressNotification from "./IndexProgressNotification"; import InheritanceModal from "./inheritance/InheritanceModal"; +import { JarDecompilerModal, JarDecompilerProgressModal } from "./JarDecompilerModal"; import ProgressModal from "./ProgressModal"; -import AboutModal from "./AboutModal"; import SettingsModal from "./SettingsModal"; import StructureModal from "./StructureModal"; -import { JarDecompilerModal, JarDecompilerProgressModal } from "./JarDecompilerModal"; -import IndexProgressNotification from "./IndexProgressNotification"; const Modals = () => { return ( diff --git a/src/ui/ProgressModal.tsx b/src/ui/ProgressModal.tsx index c98e296..60becc3 100644 --- a/src/ui/ProgressModal.tsx +++ b/src/ui/ProgressModal.tsx @@ -1,4 +1,5 @@ import { Modal, Progress } from "antd"; + import { downloadProgress } from "../logic/MinecraftApi"; import { useObservable } from "../utils/UseObservable"; diff --git a/src/ui/ReferenceResults.tsx b/src/ui/ReferenceResults.tsx index 9e9f5ea..3d996a6 100644 --- a/src/ui/ReferenceResults.tsx +++ b/src/ui/ReferenceResults.tsx @@ -1,9 +1,11 @@ -import { useObservable } from "../utils/UseObservable"; -import { formatReference, goToReference, referenceResults } from "../logic/FindAllReferences"; -import type { ReferenceString } from "../workers/JarIndex"; import { map, Observable } from "rxjs"; -import { openTab } from "../logic/Tabs"; + +import type { ReferenceString } from "../workers/JarIndex"; + +import { formatReference, goToReference, referenceResults } from "../logic/FindAllReferences"; import { referencesQuery } from "../logic/State"; +import { openTab } from "../logic/Tabs"; +import { useObservable } from "../utils/UseObservable"; function getUsageClass(usage: ReferenceString): string { if (usage.startsWith("m:") || usage.startsWith("f:")) { diff --git a/src/ui/SearchResults.tsx b/src/ui/SearchResults.tsx index 36e9435..458edbe 100644 --- a/src/ui/SearchResults.tsx +++ b/src/ui/SearchResults.tsx @@ -1,7 +1,8 @@ import { List } from "antd"; + import { searchResults } from "../logic/JarFile"; -import { useObservable } from "../utils/UseObservable"; import { openTab } from "../logic/Tabs"; +import { useObservable } from "../utils/UseObservable"; const SearchResults = () => { const results = useObservable(searchResults); diff --git a/src/ui/SettingsModal.tsx b/src/ui/SettingsModal.tsx index 53aa9f4..1d24ef4 100644 --- a/src/ui/SettingsModal.tsx +++ b/src/ui/SettingsModal.tsx @@ -1,3 +1,6 @@ +import type React from "react"; + +import { SettingOutlined } from "@ant-design/icons"; import { Button, Modal, @@ -7,9 +10,10 @@ import { InputNumber, type InputNumberProps, } from "antd"; -import { SettingOutlined } from "@ant-design/icons"; import { Checkbox } from "antd"; -import { useObservable } from "../utils/UseObservable"; +import { BehaviorSubject } from "rxjs"; + +import { capturingKeybind, rawKeydownEvent } from "../logic/Keybinds"; import { BooleanSetting, enableTabs, @@ -23,9 +27,7 @@ import { preferWasmDecompiler, compactPackages, } from "../logic/Settings"; -import { capturingKeybind, rawKeydownEvent } from "../logic/Keybinds"; -import { BehaviorSubject } from "rxjs"; -import type React from "react"; +import { useObservable } from "../utils/UseObservable"; export const settingsModalOpen = new BehaviorSubject(false); diff --git a/src/ui/SideBar.tsx b/src/ui/SideBar.tsx index e18adbf..17902f6 100644 --- a/src/ui/SideBar.tsx +++ b/src/ui/SideBar.tsx @@ -1,16 +1,18 @@ -import { Button, Divider, Flex, Input } from "antd"; -import Header from "./Header"; -import FileList from "./FileList"; import type { InputRef, SearchProps } from "antd/es/input"; -import { useObservable } from "../utils/UseObservable"; -import { isSearching } from "../logic/JarFile"; -import SearchResults from "./SearchResults"; -import ReferenceResults from "./ReferenceResults"; -import { formatReferenceQuery, isViewingReferences } from "../logic/FindAllReferences"; + import { ArrowLeftOutlined } from "@ant-design/icons"; -import { focusSearchEvent } from "../logic/Keybinds"; +import { Button, Divider, Flex, Input } from "antd"; import { useEffect, useRef } from "react"; + +import { formatReferenceQuery, isViewingReferences } from "../logic/FindAllReferences"; +import { isSearching } from "../logic/JarFile"; +import { focusSearchEvent } from "../logic/Keybinds"; import { searchQuery, referencesQuery } from "../logic/State"; +import { useObservable } from "../utils/UseObservable"; +import FileList from "./FileList"; +import Header from "./Header"; +import ReferenceResults from "./ReferenceResults"; +import SearchResults from "./SearchResults"; const { Search } = Input; diff --git a/src/ui/StructureModal.tsx b/src/ui/StructureModal.tsx index cca2e28..0eebf2e 100644 --- a/src/ui/StructureModal.tsx +++ b/src/ui/StructureModal.tsx @@ -1,7 +1,8 @@ import { Modal } from "antd"; import { useEffect, useState } from "react"; -import { useObservable } from "../utils/UseObservable"; + import { showStructureEvent } from "../logic/Keybinds"; +import { useObservable } from "../utils/UseObservable"; import StructureView from "./StructureView"; const StructureModal = () => { diff --git a/src/ui/StructureView.tsx b/src/ui/StructureView.tsx index fcaa312..e7e29d1 100644 --- a/src/ui/StructureView.tsx +++ b/src/ui/StructureView.tsx @@ -1,11 +1,13 @@ -import { Empty, Tree } from "antd"; import type { TreeDataNode, TreeProps } from "antd"; + +import { Empty, Tree } from "antd"; import { useMemo } from "react"; -import { useObservable } from "../utils/UseObservable"; + import { currentResult } from "../logic/Decompiler"; -import { parseDescriptor } from "./CodeHoverProvider"; -import { getTokenLocation, type MemberToken, type Token } from "../logic/Tokens"; import { selectedLines } from "../logic/State"; +import { getTokenLocation, type MemberToken, type Token } from "../logic/Tokens"; +import { useObservable } from "../utils/UseObservable"; +import { parseDescriptor } from "./CodeHoverProvider"; type StructureNode = TreeDataNode & { token?: Token }; diff --git a/src/ui/TabsComponent.tsx b/src/ui/TabsComponent.tsx index 48e2413..6d8ce37 100644 --- a/src/ui/TabsComponent.tsx +++ b/src/ui/TabsComponent.tsx @@ -1,8 +1,9 @@ import { Tabs } from "antd"; -import { useObservable } from "../utils/UseObservable"; -import { closeTab, openTab, setTabPosition, closeOtherTabs } from "../logic/Tabs"; import React, { useEffect, useRef, useState } from "react"; + import { selectedFile, openTabs } from "../logic/State"; +import { closeTab, openTab, setTabPosition, closeOtherTabs } from "../logic/Tabs"; +import { useObservable } from "../utils/UseObservable"; export const TabsComponent = () => { // variables - tabs diff --git a/src/ui/diff/DiffCode.tsx b/src/ui/diff/DiffCode.tsx index 06fc571..cfa349d 100644 --- a/src/ui/diff/DiffCode.tsx +++ b/src/ui/diff/DiffCode.tsx @@ -1,14 +1,16 @@ -import { DiffEditor } from "@monaco-editor/react"; -import { useObservable } from "../../utils/UseObservable"; -import { getLeftDiff, getRightDiff } from "../../logic/Diff"; -import { updateLineChanges } from "../../logic/LineChanges"; -import { useEffect, useRef } from "react"; import type { editor } from "monaco-editor"; -import { Spin } from "antd"; + import { LoadingOutlined } from "@ant-design/icons"; +import { DiffEditor } from "@monaco-editor/react"; +import { Spin } from "antd"; +import { useEffect, useRef } from "react"; + import { isDecompiling } from "../../logic/Decompiler.ts"; +import { getLeftDiff, getRightDiff } from "../../logic/Diff"; +import { updateLineChanges } from "../../logic/LineChanges"; import { unifiedDiff } from "../../logic/Settings"; import { selectedFile } from "../../logic/State.ts"; +import { useObservable } from "../../utils/UseObservable"; const DiffCode = () => { const leftResult = useObservable(getLeftDiff().result); diff --git a/src/ui/diff/DiffFileList.tsx b/src/ui/diff/DiffFileList.tsx index a32b1cc..221ef39 100644 --- a/src/ui/diff/DiffFileList.tsx +++ b/src/ui/diff/DiffFileList.tsx @@ -1,4 +1,5 @@ -import { Table, Tag, Input, Button, Flex, theme, Checkbox, Tooltip, Layout, Space } from "antd"; +import type { SearchProps } from "antd/es/input"; + import { SplitCellsOutlined, AlignLeftOutlined, @@ -7,7 +8,11 @@ import { CodeOutlined, FileTextOutlined, } from "@ant-design/icons"; -import DiffVersionSelection from "./DiffVersionSelection"; +import { Table, Tag, Input, Button, Flex, theme, Checkbox, Tooltip, Layout, Space } from "antd"; +import { useEffect, useMemo } from "react"; +import { BehaviorSubject, map, combineLatest } from "rxjs"; + +import { isDecompiling } from "../../logic/Decompiler.ts"; import { getDiffChanges, type ChangeState, @@ -16,13 +21,10 @@ import { getDiffSummary, type DiffSummary, } from "../../logic/Diff"; -import { BehaviorSubject, map, combineLatest } from "rxjs"; -import { useObservable } from "../../utils/UseObservable"; -import type { SearchProps } from "antd/es/input"; -import { isDecompiling } from "../../logic/Decompiler.ts"; -import { useEffect, useMemo } from "react"; import { bytecode, unifiedDiff } from "../../logic/Settings.ts"; import { selectedFile, diffView } from "../../logic/State.ts"; +import { useObservable } from "../../utils/UseObservable"; +import DiffVersionSelection from "./DiffVersionSelection"; const statusColors: Record = { modified: "gold", diff --git a/src/ui/diff/DiffVersionSelection.tsx b/src/ui/diff/DiffVersionSelection.tsx index de07e3c..b3f8e70 100644 --- a/src/ui/diff/DiffVersionSelection.tsx +++ b/src/ui/diff/DiffVersionSelection.tsx @@ -1,7 +1,8 @@ import { Select, Flex } from "antd"; -import { useObservable } from "../../utils/UseObservable"; -import { minecraftVersionIds } from "../../logic/MinecraftApi"; + import { getLeftDiff, getRightDiff } from "../../logic/Diff"; +import { minecraftVersionIds } from "../../logic/MinecraftApi"; +import { useObservable } from "../../utils/UseObservable"; const DiffVersionSelection = () => { const versions = useObservable(minecraftVersionIds); diff --git a/src/ui/diff/DiffView.tsx b/src/ui/diff/DiffView.tsx index eef2350..fb108a7 100644 --- a/src/ui/diff/DiffView.tsx +++ b/src/ui/diff/DiffView.tsx @@ -1,8 +1,9 @@ import { Splitter } from "antd"; import { useState } from "react"; -import DiffFileList from "./DiffFileList"; -import DiffCode from "./DiffCode"; + import { FilepathHeader } from "../FilepathHeader"; +import DiffCode from "./DiffCode"; +import DiffFileList from "./DiffFileList"; const DiffView = () => { const [sizes, setSizes] = useState<(number | string)[]>(["70%", "30%"]); diff --git a/src/ui/inheritance/InheritanceGraph.tsx b/src/ui/inheritance/InheritanceGraph.tsx index 2ebe972..82c2a64 100644 --- a/src/ui/inheritance/InheritanceGraph.tsx +++ b/src/ui/inheritance/InheritanceGraph.tsx @@ -7,11 +7,12 @@ import { ReactFlowProvider, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; -import { ClassNode, selectedInheritanceClassName } from "../../logic/Inheritance"; -import { isInterface, isAbstract } from "../../utils/Classfile"; -import { useMemo, useCallback, useEffect } from "react"; import dagre from "dagre"; +import { useMemo, useCallback, useEffect } from "react"; + +import { ClassNode, selectedInheritanceClassName } from "../../logic/Inheritance"; import { openTab } from "../../logic/Tabs"; +import { isInterface, isAbstract } from "../../utils/Classfile"; function buildGraphData(classNode: ClassNode): { nodes: Node[]; edges: Edge[] } { const nodes: Node[] = []; diff --git a/src/ui/inheritance/InheritanceModal.tsx b/src/ui/inheritance/InheritanceModal.tsx index 1202785..ebbf7cb 100644 --- a/src/ui/inheritance/InheritanceModal.tsx +++ b/src/ui/inheritance/InheritanceModal.tsx @@ -1,11 +1,12 @@ -import { lazy, Suspense } from "react"; import { Modal, Spin, Tabs } from "antd"; -import { useObservable } from "../../utils/UseObservable"; +import { lazy, Suspense } from "react"; + import { ClassNode, selectedInheritanceClassName, selectedInheritanceClassNode, } from "../../logic/Inheritance"; +import { useObservable } from "../../utils/UseObservable"; const InheritanceTree = lazy(() => import("./InheritanceTree")); const InheritanceGraph = lazy(() => import("./InheritanceGraph")); diff --git a/src/ui/inheritance/InheritanceTree.tsx b/src/ui/inheritance/InheritanceTree.tsx index 1c9addb..ee15d9f 100644 --- a/src/ui/inheritance/InheritanceTree.tsx +++ b/src/ui/inheritance/InheritanceTree.tsx @@ -1,9 +1,10 @@ -import { Tree, type TreeDataNode } from "antd"; import { ApiOutlined, CopyrightOutlined, NumberOutlined } from "@ant-design/icons"; +import { Tree, type TreeDataNode } from "antd"; import { useCallback, useMemo, type Key } from "react"; + import { ClassNode, selectedInheritanceClassName } from "../../logic/Inheritance"; -import { isEnum, isInterface } from "../../utils/Classfile"; import { openTab } from "../../logic/Tabs"; +import { isEnum, isInterface } from "../../utils/Classfile"; function getSimpleClassName(fullName: string): string { const i = fullName.lastIndexOf("/"); diff --git a/src/ui/intellij-icons/index.tsx b/src/ui/intellij-icons/index.tsx index eaa27aa..92e1901 100644 --- a/src/ui/intellij-icons/index.tsx +++ b/src/ui/intellij-icons/index.tsx @@ -1,19 +1,21 @@ -import Icon from "@ant-design/icons"; +import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; import type React from "react"; import type { SVGProps } from "react"; + +import Icon from "@ant-design/icons"; + import type { ClassData } from "../../workers/JarIndex"; -import type { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; import AnnotationSvg from "./annotation_dark.svg?react"; -import ClassAbstractSvg from "./classAbstract_dark.svg?react"; import ClassSvg from "./class_dark.svg?react"; +import ClassAbstractSvg from "./classAbstract_dark.svg?react"; import EnumSvg from "./enum_dark.svg?react"; import ExceptionSvg from "./exception_dark.svg?react"; import FinalMarkSvg from "./finalMark_dark.svg?react"; import InterfaceSvg from "./interface_dark.svg?react"; import JavaSvg from "./java_dark.svg?react"; -import RecordSvg from "./record_dark.svg?react"; import PackageSvg from "./package_dark.svg?react"; +import RecordSvg from "./record_dark.svg?react"; type SVGFC = React.FC>; const stack = diff --git a/src/workers/JarIndex.ts b/src/workers/JarIndex.ts index 796b13d..8d6e9f9 100644 --- a/src/workers/JarIndex.ts +++ b/src/workers/JarIndex.ts @@ -1,8 +1,10 @@ +import Dexie, { type EntityTable } from "dexie"; import { BehaviorSubject, distinctUntilChanged, map, shareReplay } from "rxjs"; import { endpointSymbol } from "vite-plugin-comlink/symbol"; -import { minecraftJar, type MinecraftJar } from "../logic/MinecraftApi"; + import type { ClassDataString } from "./JarIndexWorker"; -import Dexie, { type EntityTable } from "dexie"; + +import { minecraftJar, type MinecraftJar } from "../logic/MinecraftApi"; export type Class = string; export type Method = `${string}:${string}:${string}`; diff --git a/src/workers/JarIndexWorker.ts b/src/workers/JarIndexWorker.ts index 4e2d89d..f89575c 100644 --- a/src/workers/JarIndexWorker.ts +++ b/src/workers/JarIndexWorker.ts @@ -1,7 +1,8 @@ -import { load } from "../../java/build/generated/teavm/wasm-gc/java.wasm-runtime.js"; +import type { ReferenceKey, ReferenceString } from "./JarIndex.js"; + import indexerWasm from "../../java/build/generated/teavm/wasm-gc/java.wasm?url"; +import { load } from "../../java/build/generated/teavm/wasm-gc/java.wasm-runtime.js"; import { openJar, type Jar } from "../utils/Jar.js"; -import type { ReferenceKey, ReferenceString } from "./JarIndex.js"; export type ClassDataString = `${string}|${string}|${number}|${string}`; diff --git a/src/workers/decompile/client.ts b/src/workers/decompile/client.ts index 6075e7e..1dfaef4 100644 --- a/src/workers/decompile/client.ts +++ b/src/workers/decompile/client.ts @@ -1,8 +1,10 @@ import * as Comlink from "comlink"; + import type * as vf from "../../logic/vf"; -import { DecompileJar, type DecompileData, type DecompileResult } from "./types"; import type { Jar } from "../../utils/Jar"; +import { DecompileJar, type DecompileData, type DecompileResult } from "./types"; + type DecompileWorker = typeof import("./worker"); function createWrorker() { return new ComlinkWorker(new URL("./worker", import.meta.url), { diff --git a/src/workers/decompile/worker.ts b/src/workers/decompile/worker.ts index 8968d46..44a9002 100644 --- a/src/workers/decompile/worker.ts +++ b/src/workers/decompile/worker.ts @@ -1,6 +1,9 @@ -import * as vf from "../../logic/vf"; import Dexie, { type EntityTable, type Table } from "dexie"; + import type { Token } from "../../logic/Tokens"; + +import * as vf from "../../logic/vf"; +import { openJar } from "../../utils/Jar"; import { getBytecode } from "../JarIndexWorker"; import { type DecompileResult, @@ -8,7 +11,6 @@ import { type DecompileData, DecompileJar, } from "./types"; -import { openJar } from "../../utils/Jar"; let lastPromise: Promise | undefined = undefined; let _promiseCount = 0; diff --git a/tests/bytecode.spec.ts b/tests/bytecode.spec.ts index 77d14d6..cb1824b 100644 --- a/tests/bytecode.spec.ts +++ b/tests/bytecode.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { setupTest } from "./test-utils"; test.describe("Bytecode Setting", () => { diff --git a/tests/decompile.spec.ts b/tests/decompile.spec.ts index 67cc59c..275563a 100644 --- a/tests/decompile.spec.ts +++ b/tests/decompile.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Decompilation", () => { diff --git a/tests/diff.spec.ts b/tests/diff.spec.ts index 4b0c736..1e0824c 100644 --- a/tests/diff.spec.ts +++ b/tests/diff.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { setupTest } from "./test-utils"; test.describe("Diff View", () => { diff --git a/tests/file-list.spec.ts b/tests/file-list.spec.ts index 5c14c43..d8df244 100644 --- a/tests/file-list.spec.ts +++ b/tests/file-list.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("File List Navigation", () => { diff --git a/tests/find-usages.spec.ts b/tests/find-usages.spec.ts index b799354..124905b 100644 --- a/tests/find-usages.spec.ts +++ b/tests/find-usages.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Find All References", () => { diff --git a/tests/goto-definition.spec.ts b/tests/goto-definition.spec.ts index 56c07eb..7c3a4ae 100644 --- a/tests/goto-definition.spec.ts +++ b/tests/goto-definition.spec.ts @@ -1,4 +1,5 @@ import { test } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Go to Definition", () => { diff --git a/tests/inheritance.spec.ts b/tests/inheritance.spec.ts index 615801b..a2f5945 100644 --- a/tests/inheritance.spec.ts +++ b/tests/inheritance.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Inheritance", () => { diff --git a/tests/permalink.spec.ts b/tests/permalink.spec.ts index 9e77769..43e8b9a 100644 --- a/tests/permalink.spec.ts +++ b/tests/permalink.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Permalinks and Line Highlighting", () => { diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index d6b48a1..688b1da 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Tabs", () => { diff --git a/tests/version-switching.spec.ts b/tests/version-switching.spec.ts index ad1bd33..1647b66 100644 --- a/tests/version-switching.spec.ts +++ b/tests/version-switching.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; + import { waitForDecompiledContent, setupTest } from "./test-utils"; test.describe("Version Switching", () => { diff --git a/vite.config.ts b/vite.config.ts index 1ef33fb..3c2f78e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "vitest/config"; -import { comlink } from "vite-plugin-comlink"; import react from "@vitejs/plugin-react"; +import { comlink } from "vite-plugin-comlink"; import svgr from "vite-plugin-svgr"; +import { defineConfig } from "vitest/config"; // https://vite.dev/config/ export default defineConfig({