├── gradle.properties ├── scripts ├── .gitignore ├── aarch64.json └── help-generator-arm.py ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── kotlinc.xml ├── misc.xml └── gradle.xml ├── art ├── app-icon │ ├── icon.ico │ ├── icon.icns │ ├── kotlin-explorer.afphoto │ └── icon.iconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_256x256@2x.png │ │ └── icon_512x512@2x.png └── kotlin-explorer.png ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── src ├── jvmMain │ ├── resources │ │ ├── icons │ │ │ ├── icon.ico │ │ │ ├── done.svg │ │ │ └── error.svg │ │ └── themes │ │ │ ├── kotlin_explorer.xml │ │ │ └── kotlin_explorer_disassembly.xml │ └── kotlin │ │ └── dev │ │ └── romainguy │ │ └── kotlin │ │ └── explorer │ │ ├── code │ │ ├── OpCodeDoc.kt │ │ ├── CodeStyle.kt │ │ ├── CodeContent.kt │ │ ├── SyntaxStyle.kt │ │ ├── Code.kt │ │ ├── Documentation.kt │ │ ├── DataModels.kt │ │ ├── CodeTextArea.kt │ │ └── CodeBuilder.kt │ │ ├── OS.kt │ │ ├── Regex.kt │ │ ├── Swing.kt │ │ ├── Logger.kt │ │ ├── String.kt │ │ ├── DocumentChangeListener.kt │ │ ├── PeekingIterator.kt │ │ ├── Theme.kt │ │ ├── build │ │ ├── ByteCodeDecompiler.kt │ │ ├── DexCompiler.kt │ │ └── KolinCompiler.kt │ │ ├── SourceTextArea.kt │ │ ├── Process.kt │ │ ├── SyntaxTextArea.kt │ │ ├── Menu.kt │ │ ├── ProgressUpdater.kt │ │ ├── Splitter.kt │ │ ├── Paths.kt │ │ ├── DependencyCache.kt │ │ ├── bytecode │ │ └── ByteCodeParser.kt │ │ ├── State.kt │ │ ├── dex │ │ └── DexDumpParser.kt │ │ ├── Settings.kt │ │ └── oat │ │ └── OatDumpParser.kt └── jvmTest │ └── kotlin │ ├── testData │ ├── Issue_45.kt │ ├── TryCatch.kt │ ├── TryCatch-Bytecode.expected │ ├── TryCatch.javap │ ├── Issue_45-Bytecode.expected │ └── Issue_45.javap │ └── dev │ └── romainguy │ └── kotlin │ └── explorer │ ├── bytecode │ └── ByteCodeParserTest.kt │ └── testing │ └── Builder.kt ├── compose-stability.config ├── settings.gradle.kts ├── token-makers ├── build.gradle.kts └── src │ └── main │ └── java │ └── dev │ └── romainguy │ └── kotlin │ └── explorer │ └── code │ ├── DexTokenMaker.flex │ └── OatTokenMaker.flex ├── .github └── workflows │ ├── presubmit.yml │ └── release.yml ├── .gitignore ├── compose-desktop.pro ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | aarch64-docs/ 2 | bs4-env/ 3 | *.kt 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /art/app-icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.ico -------------------------------------------------------------------------------- /art/app-icon/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.icns -------------------------------------------------------------------------------- /art/kotlin-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/kotlin-explorer.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /art/app-icon/kotlin-explorer.afphoto: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/kotlin-explorer.afphoto -------------------------------------------------------------------------------- /src/jvmMain/resources/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/src/jvmMain/resources/icons/icon.ico -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /art/app-icon/icon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romainguy/kotlin-explorer/HEAD/art/app-icon/icon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /compose-stability.config: -------------------------------------------------------------------------------- 1 | androidx.collection.* 2 | 3 | java.nio.file.Path 4 | 5 | kotlin.collections.* 6 | 7 | kotlinx.coroutines.CoroutineScope 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /src/jvmMain/resources/icons/done.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/testData/Issue_45.kt: -------------------------------------------------------------------------------- 1 | package testData 2 | 3 | fun main() { 4 | f1() { 5 | f2() 6 | } 7 | } 8 | 9 | fun f1(f: () -> Unit) { f() } 10 | fun f2() { 11 | println("Hi") 12 | } 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/testData/TryCatch.kt: -------------------------------------------------------------------------------- 1 | package testData 2 | 3 | fun main() { 4 | val a = null 5 | try { 6 | foo() 7 | } 8 | catch (e: Exception) { 9 | println() 10 | } 11 | } 12 | 13 | fun foo() { 14 | println() 15 | } 16 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | } 8 | } 9 | 10 | rootProject.name = "kotlin-explorer" 11 | 12 | include("tests") 13 | include("token-makers") 14 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scripts/aarch64.json: -------------------------------------------------------------------------------- 1 | { 2 | "archive": { 3 | "url": "https://developer.arm.com/-/media/developer/products/architecture/armv9-a-architecture/2023-03/ISA_A64_xml_A_profile-2023-03.tar.gz", 4 | "name": "ISA_A64_xml_A_profile-2023-03.tar.gz", 5 | "subdir": "ISA_A64_xml_A_profile-2023-03" 6 | }, 7 | "documentation": "https://developer.arm.com/documentation/ddi0602/2024-06/Base-Instructions/", 8 | "isa": "aarch64" 9 | } 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /token-makers/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("java") 5 | } 6 | 7 | group = "dev.romainguy.kotlin.explorer.code" 8 | version = "1.0" 9 | 10 | repositories { 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | implementation(libs.rsyntaxtextarea) 16 | implementation(libs.rstaui) 17 | } 18 | 19 | java { 20 | toolchain { 21 | vendor = JvmVendorSpec.JETBRAINS 22 | languageVersion = JavaLanguageVersion.of(21) 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/presubmit.yml: -------------------------------------------------------------------------------- 1 | name: Presubmit 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-java@v4 17 | with: 18 | distribution: 'jetbrains' 19 | java-version: 21 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | - uses: gradle/actions/setup-gradle@v3 23 | - run: ./gradlew build 24 | -------------------------------------------------------------------------------- /src/jvmMain/resources/icons/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/**/build/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/uiDesigner.xml 11 | .idea/libraries/ 12 | .idea/artifacts/ 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | /local.properties 45 | /.idea/AndroidProjectSystem.xml 46 | /.idea/inspectionProfiles/Project_Default.xml 47 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/OpCodeDoc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.code 18 | 19 | data class OpCodeDoc(val name: String, val documentation: String, val url: String) 20 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeStyle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.code 18 | 19 | data class CodeStyle( 20 | val indent: Int = 4, 21 | val showLineNumbers: Boolean = true, 22 | val lineNumberWidth: Int = 4, 23 | ) 24 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/OS.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | val isWindows: Boolean = System.getProperty("os.name").lowercase().startsWith("win") 20 | val isMac: Boolean = System.getProperty("os.name").lowercase().startsWith("mac") 21 | val isLinux = !isWindows && !isMac 22 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Regex.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | const val HexDigit = "[0-9a-fA-F]" 20 | 21 | fun MatchResult.getValue(group: String): String { 22 | return groups[group]?.value ?: throw IllegalStateException("Value of $group not found in $value") 23 | } 24 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/testData/TryCatch-Bytecode.expected: -------------------------------------------------------------------------------- 1 | public final class testData.TryCatchKt 2 | public static final void main() 3 | -- 9 instructions 4 | 4: 0: aconst_null 5 | 1: astore_0 6 | 5: 2: nop 7 | 6: 3: invokestatic #11 // Method foo:()V 8 | 6: goto 16 9 | 8: 9: astore_1 10 | 9: 10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream; 11 | 13: invokevirtual #22 // Method java/io/PrintStream.println:()V 12 | 11: 16: return 13 | 14 | public static final void foo() 15 | -- 3 instructions 16 | 14: 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream; 17 | 3: invokevirtual #22 // Method java/io/PrintStream.println:()V 18 | 15: 6: return 19 | 20 | public static void main(java.lang.String[]) 21 | -- 2 instructions 22 | 0: invokestatic #29 // Method main:()V 23 | 3: return 24 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Swing.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("FunctionName") 18 | 19 | package dev.romainguy.kotlin.explorer 20 | 21 | import java.awt.Point 22 | import javax.swing.JTextArea 23 | import javax.swing.JViewport 24 | 25 | fun JTextArea.centerCaretInView() { 26 | val viewport = parent as? JViewport ?: return 27 | val linePos = modelToView2D(caretPosition).bounds.centerY.toInt() 28 | viewport.viewPosition = Point(0, maxOf(0, linePos - viewport.height / 2)) 29 | } 30 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Logger.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | private const val Debug = 1 20 | private const val Warning = 2 21 | 22 | object Logger { 23 | private val level = System.getenv("KOTLIN_EXPLORER_LOG")?.toIntOrNull() ?: 0 24 | 25 | fun debug(message: String) { 26 | if (level >= Debug) { 27 | println("Debug: $message") 28 | } 29 | } 30 | 31 | fun warn(message: String) { 32 | if (level >= Warning) { 33 | println("Warning: $message") 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/String.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | fun Iterator.consumeUntil(prefix: String): Boolean { 20 | while (hasNext()) { 21 | val line = next() 22 | if (line.trim().startsWith(prefix)) return true 23 | } 24 | return false 25 | } 26 | 27 | fun Iterator.consumeUntil(regex: Regex): MatchResult? { 28 | while (hasNext()) { 29 | val line = next() 30 | val match = regex.matchEntire(line) 31 | if (match != null) { 32 | return match 33 | } 34 | } 35 | return null 36 | } 37 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/CodeContent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.code 18 | 19 | import java.io.ByteArrayOutputStream 20 | import java.io.PrintStream 21 | 22 | sealed class CodeContent { 23 | data class Success(val classes: List) : CodeContent() 24 | data class Error(val errorText: String) : CodeContent() { 25 | constructor(e: Exception) : this(e.toFullString()) 26 | } 27 | data object Empty : CodeContent() 28 | } 29 | 30 | private fun Throwable.toFullString(): String { 31 | return ByteArrayOutputStream().use { 32 | printStackTrace(PrintStream(it)) 33 | it.toString() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/DocumentChangeListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import javax.swing.event.DocumentEvent 20 | import javax.swing.event.DocumentListener 21 | 22 | /** 23 | * A [DocumentListener] that takes a single lambda that's invoked on any change 24 | */ 25 | class DocumentChangeListener(private val block: (DocumentEvent) -> Unit) : DocumentListener { 26 | override fun insertUpdate(event: DocumentEvent) { 27 | block(event) 28 | } 29 | 30 | override fun removeUpdate(event: DocumentEvent) { 31 | block(event) 32 | } 33 | 34 | override fun changedUpdate(event: DocumentEvent) { 35 | block(event) 36 | } 37 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/PeekingIterator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | /** Based on Guava PeekingIterator */ 20 | class PeekingIterator(private val iterator: Iterator) : Iterator { 21 | private var peekedElement: E? = null 22 | 23 | override fun hasNext(): Boolean { 24 | return peekedElement != null || iterator.hasNext() 25 | } 26 | 27 | override fun next(): E { 28 | val element = peekedElement ?: return iterator.next() 29 | peekedElement = null 30 | return element 31 | } 32 | 33 | fun peek(): E { 34 | return peekedElement.takeIf { it != null } ?: iterator.next().also { peekedElement = it } 35 | } 36 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import androidx.compose.ui.graphics.Color 20 | import org.jetbrains.skiko.SystemTheme 21 | 22 | val IconErrorColor = Color(0xffee4056) 23 | val IconValidColor = Color(0xff3369d6) 24 | val ErrorColor = Color(0xffa04646) 25 | val ProgressColor = Color(0xff3369d6) 26 | val ProgressTrackColor = Color(0xffc4c4c4) 27 | 28 | enum class KotlinExplorerTheme { 29 | Dark, Light, System; 30 | 31 | // TODO: Using currentSystemTheme leads to an UnsatisfiedLinkError with the JetBrains Runtime 32 | fun isDark() = (if (this == System) fromSystemTheme(/* currentSystemTheme */ SystemTheme.LIGHT) else this) == Dark 33 | 34 | companion object { 35 | fun fromSystemTheme(systemTheme: SystemTheme) = if (systemTheme == SystemTheme.LIGHT) Light else Dark 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/build/ByteCodeDecompiler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.build 18 | 19 | import dev.romainguy.kotlin.explorer.process 20 | import java.nio.file.Path 21 | import kotlin.io.path.ExperimentalPathApi 22 | import kotlin.io.path.extension 23 | import kotlin.io.path.pathString 24 | import kotlin.io.path.walk 25 | 26 | class ByteCodeDecompiler { 27 | suspend fun decompile(directory: Path) = process(*buildJavapCommand(directory), directory = directory) 28 | 29 | @OptIn(ExperimentalPathApi::class) 30 | private fun buildJavapCommand(directory: Path): Array { 31 | val command = mutableListOf("javap", "-p", "-l", "-c") 32 | val classFiles = directory.walk() 33 | .filter { path -> path.extension == "class" } 34 | .map { path -> directory.relativize(path).pathString } 35 | .sorted() 36 | command.addAll(classFiles) 37 | return command.toTypedArray() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/testData/TryCatch.javap: -------------------------------------------------------------------------------- 1 | Compiled from "TryCatch.kt" 2 | public final class testData.TryCatchKt { 3 | public static final void main(); 4 | Code: 5 | 0: aconst_null 6 | 1: astore_0 7 | 2: nop 8 | 3: invokestatic #11 // Method foo:()V 9 | 6: goto 16 10 | 9: astore_1 11 | 10: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream; 12 | 13: invokevirtual #22 // Method java/io/PrintStream.println:()V 13 | 16: return 14 | Exception table: 15 | from to target type 16 | 2 6 9 Class java/lang/Exception 17 | LineNumberTable: 18 | line 4: 0 19 | line 5: 2 20 | line 6: 3 21 | line 8: 9 22 | line 9: 10 23 | line 11: 16 24 | LocalVariableTable: 25 | Start Length Slot Name Signature 26 | 10 6 1 e Ljava/lang/Exception; 27 | 2 15 0 a Ljava/lang/Void; 28 | 29 | public static final void foo(); 30 | Code: 31 | 0: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream; 32 | 3: invokevirtual #22 // Method java/io/PrintStream.println:()V 33 | 6: return 34 | LineNumberTable: 35 | line 14: 0 36 | line 15: 6 37 | 38 | public static void main(java.lang.String[]); 39 | Code: 40 | 0: invokestatic #29 // Method main:()V 41 | 3: return 42 | LocalVariableTable: 43 | Start Length Slot Name Signature 44 | 0 4 0 args [Ljava/lang/String; 45 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/SyntaxStyle.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.code 18 | 19 | import org.fife.ui.rsyntaxtextarea.AbstractTokenMakerFactory 20 | import org.fife.ui.rsyntaxtextarea.TokenMakerFactory 21 | 22 | class SyntaxStyle private constructor() { 23 | companion object { 24 | val Dex: String get() = "text/dex-bytecode" 25 | val ByteCode: String get() = "text/java-bytecode" 26 | val Kotlin: String get() = "text/kotlin" 27 | val Oat: String get() = "text/oat-assembly" 28 | 29 | init { 30 | val factory = TokenMakerFactory.getDefaultInstance() as AbstractTokenMakerFactory 31 | factory.putMapping(ByteCode, DexTokenMaker::class.java.canonicalName) 32 | factory.putMapping(Dex, DexTokenMaker::class.java.canonicalName) 33 | factory.putMapping(Kotlin, KotlinTokenMaker::class.java.canonicalName) 34 | factory.putMapping(Oat, OatTokenMaker::class.java.canonicalName) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /compose-desktop.pro: -------------------------------------------------------------------------------- 1 | -dontoptimize 2 | 3 | -dontwarn androidx.compose.desktop.DesktopTheme* 4 | -dontwarn kotlinx.datetime.** 5 | 6 | -keep class dev.romainguy.kotlin.explorer.code.*TokenMarker { *; } 7 | -dontnote dev.romainguy.kotlin.explorer.code.*TokenMarker 8 | 9 | -keep class org.fife.** { *; } 10 | -dontnote org.fife.** 11 | 12 | -keep class sun.misc.Unsafe { *; } 13 | -dontnote sun.misc.Unsafe 14 | 15 | -keep class com.jetbrains.JBR* { *; } 16 | -dontnote com.jetbrains.JBR* 17 | 18 | -keep class com.sun.jna** { *; } 19 | -dontnote com.sun.jna** 20 | 21 | -keep class org.jsoup** { *; } 22 | -dontnote org.jsoup** 23 | 24 | -keep class androidx.compose.ui.input.key.KeyEvent_desktopKt { *; } 25 | -dontnote androidx.compose.ui.input.key.KeyEvent_desktopKt 26 | 27 | -keep class androidx.compose.ui.input.key.KeyEvent_skikoKt { *; } 28 | -dontnote androidx.compose.ui.input.key.KeyEvent_skikoKt 29 | -dontwarn androidx.compose.ui.input.key.KeyEvent_skikoKt 30 | 31 | -dontnote org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.** 32 | -dontwarn org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.** 33 | 34 | -keep class org.jetbrains.jewel.ui** { *; } 35 | -dontnote org.jetbrains.jewel.ui** 36 | 37 | -dontnote org.jetbrains.jewel.foundation.lazy.** 38 | -dontwarn org.jetbrains.jewel.foundation.lazy.** 39 | 40 | -dontnote org.jetbrains.jewel.foundation.util.** 41 | -dontwarn org.jetbrains.jewel.foundation.util.** 42 | 43 | -dontnote org.jetbrains.jewel.window.utils.** 44 | -dontwarn org.jetbrains.jewel.window.utils.** 45 | 46 | -dontnote org.jetbrains.jewel.ui.component.SpinnerProgressIconGenerator 47 | -dontwarn org.jetbrains.jewel.ui.component.SpinnerProgressIconGenerator 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | os: [ macos-15-intel, macos-latest, windows-latest ] 13 | include: 14 | - os: macos-15-intel 15 | outputDirName: dmg 16 | - os: macos-latest 17 | outputDirName: dmg 18 | - os: windows-latest 19 | outputDirName: msi 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - run: git config --global core.autocrlf false 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-java@v4 25 | with: 26 | distribution: 'jetbrains' 27 | java-version: 21 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | - uses: gradle/actions/setup-gradle@v3 31 | - run: ./gradlew packageAndRenameReleaseDistributionForCurrentOS 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: distribution-${{ matrix.os }} 35 | if-no-files-found: error 36 | path: | 37 | build/compose/binaries/main-release/${{ matrix.outputDirName }}/* 38 | 39 | release: 40 | runs-on: ubuntu-latest 41 | if: github.repository_owner == 'romainguy' 42 | needs: build 43 | permissions: 44 | contents: write 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/download-artifact@v4 48 | - name: Create release and upload dists 49 | uses: softprops/action-gh-release@v2 50 | with: 51 | files: | 52 | distribution-*/*.dmg 53 | distribution-*/*.msi 54 | draft: true 55 | generate_release_notes: true 56 | token: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SourceTextArea.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import dev.romainguy.kotlin.explorer.code.CodeTextArea 20 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea 21 | import java.awt.event.MouseAdapter 22 | import java.awt.event.MouseEvent 23 | 24 | class SourceTextArea( 25 | var isSyncLinesEnabled: Boolean 26 | ) : SyntaxTextArea() { 27 | private val codeTextAreas = mutableListOf() 28 | 29 | init { 30 | addMouseListener(object : MouseAdapter() { 31 | override fun mouseClicked(event: MouseEvent) { 32 | if (isSyncLinesEnabled) { 33 | codeTextAreas.forEach { 34 | it.gotoSourceLine(getLineOfOffset(viewToModel2D(event.point))) 35 | } 36 | } 37 | } 38 | }) 39 | } 40 | 41 | fun addCodeTextAreas(vararg codeTextAreas: CodeTextArea) { 42 | this.codeTextAreas.addAll(codeTextAreas) 43 | } 44 | 45 | fun gotoLine(src: CodeTextArea, line: Int) { 46 | caretPosition = getLineStartOffset(line.coerceIn(0 until lineCount)) 47 | centerCaretInView() 48 | // Sync other `CodeTextArea` to same line as the `src` sent 49 | codeTextAreas.filter { it !== src }.forEach { it.gotoSourceLine(line) } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/testData/Issue_45-Bytecode.expected: -------------------------------------------------------------------------------- 1 | final class testData.Issue_45Kt$main$1 2 | testData.Issue_45Kt$main$1() 3 | -- 4 instructions 4 | 0: aload_0 5 | 1: iconst_0 6 | 2: invokespecial #12 // Method kotlin/jvm/internal/Lambda."":(I)V 7 | 5: return 8 | 9 | public final void invoke() 10 | -- 2 instructions 11 | 5: 0: invokestatic #20 // Method testData/Issue_45Kt.f2:()V 12 | 6: 3: return 13 | 14 | public java.lang.Object invoke() 15 | -- 4 instructions 16 | 4: 0: aload_0 17 | 1: invokevirtual #23 // Method invoke:()V 18 | 4: getstatic #29 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit; 19 | 7: areturn 20 | 21 | public final class testData.Issue_45Kt 22 | public static final void main() 23 | -- 4 instructions 24 | 4: 0: getstatic #12 // Field testData/Issue_45Kt$main$1.INSTANCE:LtestData/Issue_45Kt$main$1; 25 | 3: checkcast #14 // class kotlin/jvm/functions/Function0 26 | 6: invokestatic #18 // Method f1:(Lkotlin/jvm/functions/Function0;)V 27 | 7: 9: return 28 | 29 | public static final void f1(kotlin.jvm.functions.Function0) 30 | -- 4 instructions 31 | 9: 0: aload_0 32 | 1: invokeinterface #24, 1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()Ljava/lang/Object; 33 | 6: pop 34 | 7: return 35 | 36 | public static final void f2() 37 | -- 5 instructions 38 | 11: 0: ldc #29 // String Hi 39 | 2: getstatic #35 // Field java/lang/System.out:Ljava/io/PrintStream; 40 | 5: swap 41 | 6: invokevirtual #41 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 42 | 12: 9: return 43 | 44 | public static void main(java.lang.String[]) 45 | -- 2 instructions 46 | 0: invokestatic #44 // Method main:()V 47 | 3: return 48 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Process.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import kotlinx.coroutines.* 20 | import kotlinx.coroutines.flow.asFlow 21 | import kotlinx.coroutines.flow.map 22 | import kotlinx.coroutines.flow.toList 23 | import java.nio.file.Path 24 | 25 | class ProcessResult( 26 | val exitCode: Int, 27 | val output: String 28 | ) 29 | 30 | suspend fun process( 31 | vararg command: String, 32 | directory: Path? = null 33 | ): ProcessResult { 34 | return withContext(Dispatchers.IO) { 35 | val process = ProcessBuilder() 36 | .directory(directory?.toFile()) 37 | .command(*command) 38 | .redirectErrorStream(true) 39 | .start() 40 | 41 | val output = async { 42 | process 43 | .inputStream 44 | .bufferedReader() 45 | .lineSequence() 46 | .asFlow() 47 | .map { value -> 48 | yield() 49 | value 50 | } 51 | .toList() 52 | .joinToString(System.lineSeparator()) 53 | } 54 | 55 | try { 56 | val exitCode = runInterruptible { 57 | process.waitFor() 58 | } 59 | val processOutput = output.await() 60 | ProcessResult(exitCode, processOutput) 61 | } catch (e: CancellationException) { 62 | throw e 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/dev/romainguy/kotlin/explorer/bytecode/ByteCodeParserTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.bytecode 18 | 19 | import com.google.common.truth.Truth.assertThat 20 | import dev.romainguy.kotlin.explorer.code.Code 21 | import dev.romainguy.kotlin.explorer.testing.Builder 22 | import dev.romainguy.kotlin.explorer.testing.parseSuccess 23 | import org.junit.Rule 24 | import org.junit.Test 25 | import org.junit.rules.TemporaryFolder 26 | import java.nio.file.Path 27 | import kotlin.io.path.readText 28 | 29 | class ByteCodeParserTest { 30 | @get:Rule 31 | val temporaryFolder = TemporaryFolder() 32 | 33 | private val builder by lazy { Builder.getInstance(temporaryFolder.root.toPath()) } 34 | private val byteCodeParser = ByteCodeParser() 35 | 36 | @Test 37 | fun issue_45() { 38 | val content = byteCodeParser.parseSuccess(builder.generateByteCode("Issue_45.kt")) 39 | 40 | val text = Code.fromClasses(content.classes).text 41 | 42 | assertThat(text).isEqualTo(loadTestDataFile("Issue_45-Bytecode.expected")) 43 | } 44 | 45 | @Test 46 | fun tryCatch() { 47 | val content = byteCodeParser.parseSuccess(builder.generateByteCode("TryCatch.kt")) 48 | 49 | val text = Code.fromClasses(content.classes).text 50 | 51 | assertThat(text).isEqualTo(loadTestDataFile("TryCatch-Bytecode.expected")) 52 | } 53 | } 54 | 55 | fun loadTestDataFile(path: String): String { 56 | val cwd = Path.of(System.getProperty("user.dir")) 57 | val testData = cwd.resolve("src/jvmTest/kotlin/testData") 58 | return testData.resolve(path).readText() 59 | } 60 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/SyntaxTextArea.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea 20 | import javax.swing.JScrollPane 21 | import javax.swing.JViewport 22 | 23 | private const val FontSizeEditingMode = 12.0f 24 | private const val FontSizePresentationMode = 20.0f 25 | 26 | open class SyntaxTextArea : RSyntaxTextArea() { 27 | var presentationMode = false 28 | set(value) { 29 | if (field != value) { 30 | field = value 31 | updateFontSize() 32 | } 33 | } 34 | 35 | private fun updateFontSize() { 36 | val scheme = syntaxScheme 37 | 38 | val increaseRatio = if (presentationMode) { 39 | FontSizePresentationMode / FontSizeEditingMode 40 | } else { 41 | FontSizeEditingMode / FontSizePresentationMode 42 | } 43 | 44 | val count = scheme.styleCount 45 | for (i in 0 until count) { 46 | val ss = scheme.getStyle(i) 47 | if (ss != null) { 48 | val font = ss.font 49 | if (font != null) { 50 | val oldSize: Float = font.size2D 51 | val newSize: Float = oldSize * increaseRatio 52 | ss.font = font.deriveFont(newSize) 53 | } 54 | } 55 | } 56 | 57 | font = font.deriveFont(if (presentationMode) FontSizePresentationMode else FontSizeEditingMode) 58 | 59 | syntaxScheme = scheme 60 | var parent = parent 61 | if (parent is JViewport) { 62 | parent = parent.parent 63 | if (parent is JScrollPane) { 64 | parent.repaint() 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("FunctionName") 18 | 19 | package dev.romainguy.kotlin.explorer 20 | 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.ui.input.key.Key 23 | import androidx.compose.ui.input.key.KeyShortcut 24 | import androidx.compose.ui.window.MenuScope 25 | import kotlin.reflect.KMutableProperty0 26 | 27 | /** Convenience class handles `Ctrl <-> Meta` modifies */ 28 | sealed class Shortcut( 29 | private val key: Key, 30 | private val isShift: Boolean, 31 | private val isCtrl: Boolean, 32 | private val isAlt: Boolean = false, 33 | private val metaOnMac: Boolean = true 34 | ) { 35 | class Ctrl(key: Key) : Shortcut(key, isCtrl = true, isShift = false) 36 | class CtrlAlt(key: Key) : Shortcut(key, isCtrl = true, isShift = false, isAlt = true) 37 | class CtrlOnly(key: Key) : Shortcut(key, isCtrl = true, isShift = false, metaOnMac = false) 38 | class CtrlShift(key: Key) : Shortcut(key, isCtrl = true, isShift = true) 39 | 40 | fun asKeyShortcut() = 41 | KeyShortcut( 42 | key = key, 43 | ctrl = isCtrl && (!isMac || !metaOnMac), 44 | shift = isShift, 45 | meta = isCtrl && isMac && metaOnMac, 46 | alt = isAlt 47 | ) 48 | } 49 | 50 | @Composable 51 | fun MenuScope.MenuCheckboxItem( 52 | text: String, 53 | shortcut: Shortcut?, 54 | property: KMutableProperty0, 55 | onCheckedChanged: (Boolean) -> Unit = {} 56 | ) { 57 | CheckboxItem(text, property.get(), shortcut = shortcut?.asKeyShortcut(), onCheckedChange = { 58 | property.set(it) 59 | onCheckedChanged(it) 60 | }) 61 | } 62 | 63 | @Composable 64 | fun MenuScope.MenuItem(text: String, shortcut: Shortcut, onClick: () -> Unit, enabled: Boolean = true) { 65 | Item(text, enabled = enabled, shortcut = shortcut.asKeyShortcut(), onClick = onClick) 66 | } 67 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | collection = "1.5.0" 3 | compose = "1.9.3" 4 | material3 = "1.9.0" 5 | jewel = "0.32.0-253.28294.205" 6 | jna = "5.17.0" 7 | jsoup = "1.21.2" 8 | junit4 = "4.13.2" 9 | kotlin = "2.2.21" 10 | lifecycle = "2.8.4" 11 | rstaui = "3.3.1" 12 | rsyntaxtextarea="3.4.0" 13 | truth = "1.4.2" 14 | 15 | [libraries] 16 | collection = { group = "androidx.collection", name = "collection", version.ref = "collection" } 17 | compose-material3 = { group = "org.jetbrains.compose.material3", name = "material3-desktop", version.ref = "material3" } 18 | compose-splitpane = { group = "org.jetbrains.compose.components", name = "components-splitpane-desktop", version.ref = "material3" } 19 | compose-backhandler = { group = "org.jetbrains.compose.ui", name = "ui-backhandler", version.ref = "compose" } 20 | jewel = { group = "org.jetbrains.jewel", name = "jewel-int-ui-standalone", version.ref = "jewel" } 21 | jewel-decorated = { group = "org.jetbrains.jewel", name = "jewel-int-ui-decorated-window", version.ref = "jewel" } 22 | jewel-markdown-core = { group = "org.jetbrains.jewel", name = "jewel-markdown-core", version.ref = "jewel" } 23 | jewel-markdown-intUiStandaloneStyling = { group = "org.jetbrains.jewel", name = "jewel-markdown-int-ui-standalone-styling", version.ref = "jewel" } 24 | jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } 25 | jsoup = { group = "org.jsoup", name = "jsoup", version.ref = "jsoup" } 26 | junit4 = { group = "junit", name = "junit", version.ref = "junit4" } 27 | kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } 28 | lifecycle = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime", version.ref = "lifecycle" } 29 | lifecycle-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } 30 | lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "lifecycle" } 31 | lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } 32 | rstaui = { group = "com.fifesoft", name = "rstaui", version.ref = "rstaui" } 33 | rsyntaxtextarea = { group = "com.fifesoft", name = "rsyntaxtextarea", version.ref = "rsyntaxtextarea" } 34 | truth = { group = "com.google.truth", name = "truth", version.ref = "truth" } 35 | 36 | [plugins] 37 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } 38 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 39 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 40 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/ProgressUpdater.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer 18 | 19 | import kotlinx.coroutines.Job 20 | import kotlinx.coroutines.joinAll 21 | import java.util.concurrent.atomic.AtomicInteger 22 | import java.util.concurrent.locks.ReentrantReadWriteLock 23 | import kotlin.concurrent.read 24 | import kotlin.concurrent.write 25 | import kotlin.math.min 26 | 27 | /** Helps manage updates to a status bar */ 28 | class ProgressUpdater( 29 | val steps: Int, 30 | private val onUpdate: (String, Float) -> Unit, 31 | ) { 32 | private val stepCounter = AtomicInteger(0) 33 | private val jobs = mutableListOf() 34 | 35 | /** Update without advancing progress */ 36 | fun update(message: String) { 37 | sendUpdate(message, stepCounter.get()) 38 | } 39 | 40 | /** 41 | * Update and advance progress 42 | * 43 | * We can advance by more than one step, for example, if a step fails and that prevents the next 3 steps from being 44 | * able to run, we would advance by 4. 45 | */ 46 | fun advance(message: String, steps: Int = 1) { 47 | sendUpdate(message, stepCounter.addAndGet(steps)) 48 | } 49 | 50 | suspend fun waitForJobs() { 51 | jobs.joinAll() 52 | } 53 | 54 | fun skipToEnd(message: String) { 55 | stepCounter.set(steps) 56 | sendUpdate(message, steps) 57 | } 58 | 59 | /** 60 | * Joins all threads and sends the last update 61 | */ 62 | suspend fun finish() { 63 | jobs.joinAll() 64 | val step = stepCounter.get() 65 | if (step < steps) { 66 | Logger.warn("finish() called but progress is not yet finished: step=$step") 67 | } 68 | } 69 | 70 | /** Add a job that needs to be joined before finishing */ 71 | fun addJob(job: Job) { 72 | jobs.add(job) 73 | } 74 | 75 | private fun sendUpdate(message: String, step: Int) { 76 | if (step > steps) { 77 | Logger.warn("Progress already completed while sending: '$message'") 78 | } 79 | Logger.debug("Sending $step/$steps: '$message'") 80 | onUpdate(message, min(steps, step).toFloat() / steps) 81 | } 82 | } -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Code.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.romainguy.kotlin.explorer.code 18 | 19 | import androidx.collection.IntIntMap 20 | import androidx.collection.IntObjectMap 21 | import androidx.collection.mutableIntObjectMapOf 22 | 23 | /** 24 | * A data model representing disassembled code 25 | * 26 | * Given a list [Class]'s constructs a mode that provides: 27 | * - Disassembled text with optional line number annotations 28 | * - Jump information for branch instructions 29 | */ 30 | class Code( 31 | val isa: ISA, 32 | val text: String, 33 | val instructions: IntObjectMap, 34 | private val jumps: IntIntMap, 35 | private val sourceToCodeLine: IntIntMap, 36 | private val codeToSourceToLine: IntIntMap, 37 | ) { 38 | fun getJumpTargetOfLine(line: Int) = jumps.getOrDefault(line, -1) 39 | 40 | fun getCodeLine(sourceLine: Int) = sourceToCodeLine.getOrDefault(sourceLine, -1) 41 | 42 | fun getSourceLine(codeLine: Int) = codeToSourceToLine.getOrDefault(codeLine, -1) 43 | 44 | companion object { 45 | fun fromClasses(classes: List, codeStyle: CodeStyle = CodeStyle()): Code { 46 | return buildCode(codeStyle) { 47 | val indexedMethods = buildIndexedMethods(classes) 48 | classes.forEachIndexed { classIndex, clazz -> 49 | if (clazz.builtIn) return@forEachIndexed 50 | startClass(clazz) 51 | val notLastClass = classIndex < classes.size - 1 52 | clazz.methods.forEachIndexed { methodIndex, method -> 53 | writeMethod(method, indexedMethods) 54 | if (methodIndex < clazz.methods.size - 1 || notLastClass) writeLine("") 55 | } 56 | } 57 | }.build() 58 | } 59 | 60 | private fun buildIndexedMethods(classes: List): IntObjectMap { 61 | val map = mutableIntObjectMapOf() 62 | classes.forEach { clazz -> 63 | clazz.methods.forEach { method -> 64 | if (method.index != -1) { 65 | map[method.index] = method 66 | } 67 | } 68 | } 69 | return map 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/code/Documentation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 Romain Guy 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:OptIn(ExperimentalJewelApi::class) 18 | @file:Suppress("UnstableApiUsage") 19 | 20 | package dev.romainguy.kotlin.explorer.code 21 | 22 | import androidx.collection.ScatterMap 23 | import androidx.collection.mutableScatterMapOf 24 | import org.jetbrains.jewel.foundation.ExperimentalJewelApi 25 | import org.jetbrains.jewel.markdown.InlineMarkdown 26 | import org.jetbrains.jewel.markdown.MarkdownBlock 27 | import org.jetbrains.jewel.markdown.processing.MarkdownProcessor 28 | 29 | private val Conditions = mutableScatterMapOf( 30 | "eq" to "equal.", 31 | "ne" to "not equal.", 32 | "cs" to "carry set.", 33 | "cc" to "carry clear.", 34 | "mi" to "negative.", 35 | "pl" to "positive or zero.", 36 | "vs" to "overflow.", 37 | "vc" to "no overflow.", 38 | "hi" to "unsigned higher.", 39 | "ls" to "unsigned lower or same.", 40 | "ge" to "signed greater than or equal.", 41 | "lt" to "signed less than.", 42 | "gt" to "signed greater than.", 43 | "le" to "signed less than or equal." 44 | ) as ScatterMap 45 | 46 | fun MarkdownProcessor.generateInlineDocumentation(code: Code, line: Int): List { 47 | if (code.isa == ISA.Aarch64) { 48 | val fullOp = code.instructions[line]?.op ?: "" 49 | val op = fullOp.substringBefore('.') 50 | val opDocumentation = Aarch64Docs[op] 51 | if (opDocumentation != null) { 52 | val blocks = ArrayList() 53 | blocks += MarkdownBlock.Heading(2, InlineMarkdown.Text(opDocumentation.name)) 54 | 55 | val condition = fullOp.substringAfter('.', "") 56 | if (condition.isNotEmpty()) { 57 | val conditionDocumentation = Conditions[condition] 58 | if (conditionDocumentation != null) { 59 | blocks += MarkdownBlock.Paragraph( 60 | InlineMarkdown.StrongEmphasis("**", InlineMarkdown.Text("Condition: ")), 61 | InlineMarkdown.Text(conditionDocumentation) 62 | ) 63 | } 64 | } 65 | 66 | blocks += processMarkdownDocument(opDocumentation.documentation) 67 | 68 | blocks += MarkdownBlock.Paragraph( 69 | InlineMarkdown.Link( 70 | opDocumentation.url, 71 | "See full documentation", 72 | InlineMarkdown.Text("See full documentation") 73 | ) 74 | ) 75 | 76 | return blocks 77 | } 78 | } 79 | return emptyList() 80 | } 81 | -------------------------------------------------------------------------------- /src/jvmMain/resources/themes/kotlin_explorer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |