diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt index c164f27a..96b6824f 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Disassembly.kt @@ -30,19 +30,75 @@ import java.nio.file.Path import java.util.stream.Collectors import kotlin.io.path.extension -private const val TotalSteps = 6 +private const val TotalDisassemblySteps = 6 +private const val TotalRunSteps = 2 private const val Done = 1f private val byteCodeParser = ByteCodeParser() private val dexDumpParser = DexDumpParser() private val oatDumpParser = OatDumpParser() -suspend fun disassemble( +suspend fun buildAndRun( + toolPaths: ToolPaths, + source: String, + onLogs: (String) -> Unit, + onStatusUpdate: (String, Float) -> Unit +) = coroutineScope { + val ui = currentCoroutineContext() + + launch(Dispatchers.IO) { + var step = 0f + + launch(ui) { onStatusUpdate("Compiling Kotlin…", step++ / TotalRunSteps) } + + val directory = toolPaths.tempDirectory + cleanupClasses(directory) + + val path = directory.resolve("KotlinExplorer.kt") + Files.writeString(path, source) + + val kotlinc = process( + *buildKotlincCommand(toolPaths, path), + directory = directory + ) + + if (kotlinc.exitCode != 0) { + launch(ui) { + onLogs(kotlinc.output.replace(path.parent.toString() + "/", "")) + onStatusUpdate("Ready", Done) + } + return@launch + } + + launch(ui) { onStatusUpdate("Running…", step++ / TotalRunSteps) } + + val java = process( + *buildJavaCommand(toolPaths), + directory = directory + ) + + if (java.exitCode != 0) { + launch(ui) { + onLogs(java.output) + onStatusUpdate("Ready", Done) + } + return@launch + } + + launch(ui) { + onLogs(java.output) + onStatusUpdate("Ready", Done) + } + } +} + +suspend fun buildAndDisassemble( toolPaths: ToolPaths, source: String, onByteCode: (CodeContent) -> Unit, onDex: (CodeContent) -> Unit, onOat: (CodeContent) -> Unit, + onLogs: (String) -> Unit, onStatusUpdate: (String, Float) -> Unit, optimize: Boolean ) = coroutineScope { @@ -51,7 +107,7 @@ suspend fun disassemble( launch(Dispatchers.IO) { var step = 0f - launch(ui) { onStatusUpdate("Compiling Kotlin…", step++ / TotalSteps) } + launch(ui) { onStatusUpdate("Compiling Kotlin…", step++ / TotalDisassemblySteps) } val directory = toolPaths.tempDirectory cleanupClasses(directory) @@ -66,13 +122,13 @@ suspend fun disassemble( if (kotlinc.exitCode != 0) { launch(ui) { - onDex(Error(kotlinc.output.replace(path.parent.toString() + "/", ""))) + onLogs(kotlinc.output.replace(path.parent.toString() + "/", "")) onStatusUpdate("Ready", Done) } return@launch } - launch(ui) { onStatusUpdate("Disassembling ByteCode…", step++ / TotalSteps) } + launch(ui) { onStatusUpdate("Disassembling ByteCode…", step++ / TotalDisassemblySteps) } val javap = process( *buildJavapCommand(directory), @@ -83,6 +139,7 @@ suspend fun disassemble( if (javap.exitCode != 0) { launch(ui) { + onLogs(javap.output) onStatusUpdate("Ready", Done) } return@launch @@ -90,7 +147,7 @@ suspend fun disassemble( launch(ui) { val status = if (optimize) "Optimizing with R8…" else "Compiling with D8…" - onStatusUpdate(status, step++ / TotalSteps) + onStatusUpdate(status, step++ / TotalDisassemblySteps) } writeR8Rules(directory) @@ -102,13 +159,13 @@ suspend fun disassemble( if (r8.exitCode != 0) { launch(ui) { - onDex(Error(r8.output)) + onLogs(r8.output) onStatusUpdate("Ready", Done) } return@launch } - launch(ui) { onStatusUpdate("Disassembling DEX…", step++ / TotalSteps) } + launch(ui) { onStatusUpdate("Disassembling DEX…", step++ / TotalDisassemblySteps) } val dexdump = process( toolPaths.dexdump.toString(), @@ -119,7 +176,7 @@ suspend fun disassemble( if (dexdump.exitCode != 0) { launch(ui) { - onDex(Error(dexdump.output)) + onLogs(dexdump.output) onStatusUpdate("Ready", Done) } return@launch @@ -127,7 +184,7 @@ suspend fun disassemble( launch(ui) { onDex(dexDumpParser.parse(dexdump.output)) - onStatusUpdate("AOT compilation…", step++ / TotalSteps) + onStatusUpdate("AOT compilation…", step++ / TotalDisassemblySteps) } val push = process( @@ -163,7 +220,7 @@ suspend fun disassemble( return@launch } - launch(ui) { onStatusUpdate("Disassembling OAT…", step++ / TotalSteps) } + launch(ui) { onStatusUpdate("Disassembling OAT…", step++ / TotalDisassemblySteps) } val oatdump = process( toolPaths.adb.toString(), @@ -184,6 +241,15 @@ suspend fun disassemble( } } +private fun buildJavaCommand(toolPaths: ToolPaths): Array { + val command = mutableListOf( + "java", + "-classpath", + toolPaths.kotlinLibs.joinToString(":") { jar -> jar.toString() } + ":${toolPaths.platform}" + ":.", + "KotlinExplorerKt") + return command.toTypedArray() +} + private fun buildJavapCommand(directory: Path): Array { val command = mutableListOf("javap", "-p", "-l", "-c") val classFiles = Files diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt index ced0f553..edee411d 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/KotlinExplorer.kt @@ -19,7 +19,10 @@ package dev.romainguy.kotlin.explorer import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.LinearProgressIndicator import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -34,13 +37,14 @@ import androidx.compose.ui.input.key.Key.Companion.G import androidx.compose.ui.input.key.Key.Companion.L import androidx.compose.ui.input.key.Key.Companion.O import androidx.compose.ui.input.key.Key.Companion.P +import androidx.compose.ui.input.key.Key.Companion.R +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign.Companion.Center import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import androidx.compose.ui.window.WindowPosition.Aligned -import dev.romainguy.kotlin.explorer.Shortcut.Ctrl -import dev.romainguy.kotlin.explorer.Shortcut.CtrlShift +import dev.romainguy.kotlin.explorer.Shortcut.* import dev.romainguy.kotlin.explorer.code.CodeContent import dev.romainguy.kotlin.explorer.code.CodeStyle import dev.romainguy.kotlin.explorer.code.CodeTextArea @@ -82,6 +86,7 @@ private fun FrameWindowScope.KotlinExplorer( var activeTextArea by remember { mutableStateOf(null) } var status by remember { mutableStateOf("Ready") } var progress by remember { mutableStateOf(1f) } + var logs by remember { mutableStateOf("") } val searchListener = remember { object : SearchListener { override fun searchEvent(e: SearchEvent?) { @@ -143,11 +148,16 @@ private fun FrameWindowScope.KotlinExplorer( area.codeStyle = area.codeStyle.withShowLineNumbers(it) } } - val onProgressUpdate: (String, Float) -> Unit = { newStatus: String, newProgress: Float -> status = newStatus progress = newProgress } + val onLogsUpdate: (String) -> Unit = { text -> + logs = text + if (text.isNotEmpty()) { + explorerState.showLogs = true + } + } MainMenu( explorerState, @@ -155,6 +165,7 @@ private fun FrameWindowScope.KotlinExplorer( byteCodeTextArea::setContent, dexTextArea::setContent, oatTextArea::setContent, + onLogsUpdate, onProgressUpdate, { findDialog.isVisible = true }, { SearchEngine.find(activeTextArea, findDialog.searchContext) }, @@ -174,15 +185,39 @@ private fun FrameWindowScope.KotlinExplorer( } Column(modifier = Modifier.background(JewelTheme.globalColors.paneBackground)) { - MultiSplitter(modifier = Modifier.weight(1.0f), panels) + VerticalOptionalPanel( + modifier = Modifier.weight(1.0f), + showOptionalPanel = explorerState.showLogs, + optionalPanel = { LogsPanel(logs) } + ) { + MultiSplitter(modifier = Modifier.weight(1.0f), panels = panels) + } StatusBar(status, progress) } } +@Composable +private fun LogsPanel(logs: String) { + Column { + Title("Logs") + Text( + text = logs, + fontFamily = FontFamily.Monospace, + modifier = Modifier + .weight(1.0f) + .fillMaxSize() + .background(Color.White) + .verticalScroll(rememberScrollState()) + .border(1.dp, JewelTheme.globalColors.borders.normal) + .padding(8.dp) + ) + } +} + @Composable private fun StatusBar(status: String, progress: Float) { Row(verticalAlignment = CenterVertically) { - val width = 160.dp + val width = 190.dp Text( modifier = Modifier .widthIn(min = width, max = width) @@ -299,6 +334,7 @@ private fun FrameWindowScope.MainMenu( onByteCodeUpdate: (CodeContent) -> Unit, onDexUpdate: (CodeContent) -> Unit, onOatUpdate: (CodeContent) -> Unit, + onLogsUpdate: (String) -> Unit, onStatusUpdate: (String, Float) -> Unit, onFindClicked: () -> Unit, onFindNextClicked: () -> Unit, @@ -311,17 +347,29 @@ private fun FrameWindowScope.MainMenu( val compileAndDisassemble: () -> Unit = { scope.launch { - disassemble( + buildAndDisassemble( explorerState.toolPaths, sourceTextArea.text, onByteCodeUpdate, onDexUpdate, onOatUpdate, + onLogsUpdate, onStatusUpdate, explorerState.optimize ) } } + val compileAndRun: () -> Unit = { + scope.launch { + buildAndRun( + explorerState.toolPaths, + sourceTextArea.text, + onLogsUpdate, + onStatusUpdate + ) + } + } + MenuBar { Menu("File") { Item("Settings…", onClick = onOpenSettings) @@ -339,14 +387,23 @@ private fun FrameWindowScope.MainMenu( onShowLineNumberChanged(it) } Separator() + MenuCheckboxItem("Show Logs", Ctrl(L), explorerState::showLogs) + Separator() MenuCheckboxItem("Presentation Mode", CtrlShift(P), explorerState::presentationMode) { onPresentationModeChanged(it) } } - Menu("Compilation") { + Menu("Build") { + MenuItem( + "Run", + CtrlOnly(R), + onClick = compileAndRun, + enabled = explorerState.toolPaths.isValid + ) + Separator() MenuCheckboxItem("Optimize with R8", CtrlShift(O), explorerState::optimize) MenuItem( - "Compile & Disassemble", + "Build & Disassemble", CtrlShift(D), onClick = compileAndDisassemble, enabled = explorerState.toolPaths.isValid diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt index 5f351a4b..997ac3ae 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Menu.kt @@ -25,11 +25,23 @@ import androidx.compose.ui.window.MenuScope import kotlin.reflect.KMutableProperty0 /** Convenience class handles `Ctrl <-> Meta` modifies */ -sealed class Shortcut(private val key: Key, private val isShift: Boolean, private val isCtrl: Boolean) { +sealed class Shortcut( + private val key: Key, + private val isShift: Boolean, + private val isCtrl: Boolean, + private val metaOnMac: Boolean = true +) { class Ctrl(key: Key) : Shortcut(key, isCtrl = true, isShift = false) + class CtrlOnly(key: Key) : Shortcut(key, isCtrl = true, isShift = false, metaOnMac = false) class CtrlShift(key: Key) : Shortcut(key, isCtrl = true, isShift = true) - fun asKeyShortcut() = KeyShortcut(key = key, ctrl = isCtrl && !isMac, shift = isShift, meta = isCtrl && isMac) + fun asKeyShortcut() = + KeyShortcut( + key = key, + ctrl = isCtrl && (!isMac || !metaOnMac), + shift = isShift, + meta = isCtrl && isMac && metaOnMac + ) } @Composable diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Splitter.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Splitter.kt index 629ac383..8f16108c 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Splitter.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/Splitter.kt @@ -19,23 +19,21 @@ package dev.romainguy.kotlin.explorer -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.unit.dp -import org.jetbrains.compose.splitpane.ExperimentalSplitPaneApi -import org.jetbrains.compose.splitpane.HorizontalSplitPane -import org.jetbrains.compose.splitpane.SplitterScope -import org.jetbrains.compose.splitpane.rememberSplitPaneState +import org.jetbrains.compose.splitpane.* import java.awt.Cursor private fun Modifier.cursorForHorizontalResize(): Modifier = pointerHoverIcon(PointerIcon(Cursor(Cursor.E_RESIZE_CURSOR))) +private fun Modifier.cursorForVerticalResize(): Modifier = + pointerHoverIcon(PointerIcon(Cursor(Cursor.N_RESIZE_CURSOR))) + fun SplitterScope.HorizontalSplitter() { visiblePart { Box( @@ -55,6 +53,25 @@ fun SplitterScope.HorizontalSplitter() { } } +fun SplitterScope.VerticalSplitter() { + visiblePart { + Box( + Modifier + .height(5.dp) + .fillMaxWidth() + ) + } + handle { + Box( + Modifier + .markAsHandle() + .cursorForVerticalResize() + .height(5.dp) + .fillMaxWidth() + ) + } +} + @Composable fun MultiSplitter(modifier: Modifier = Modifier, panels: List<@Composable () -> Unit>) { val size = panels.size @@ -69,7 +86,26 @@ fun MultiSplitter(modifier: Modifier = Modifier, panels: List<@Composable () -> second { MultiSplitter(modifier = modifier, panels.drop(1)) } splitter { HorizontalSplitter() } } - } } +@Composable +fun VerticalOptionalPanel( + modifier: Modifier = Modifier, + showOptionalPanel: Boolean = false, + optionalPanel: @Composable () -> Unit, + content: @Composable () -> Unit +) { + if (showOptionalPanel) { + VerticalSplitPane( + modifier = modifier, + splitPaneState = rememberSplitPaneState(initialPositionPercentage = 4f / 5f) + ) { + first { content() } + second { optionalPanel() } + splitter { VerticalSplitter() } + } + } else { + content() + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt index 53d14bce..be1a5f79 100644 --- a/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt +++ b/src/jvmMain/kotlin/dev/romainguy/kotlin/explorer/State.kt @@ -33,6 +33,7 @@ private const val ShowLineNumbers = "SHOW_LINE_NUMBERS" private const val ShowByteCode = "SHOW_BYTE_CODE" private const val ShowDex = "SHOW_DEX" private const val ShowOat = "SHOW_OAT" +private const val ShowLogs = "SHOW_LOGS" private const val Indent = "Indent" private const val LineNumberWidth = "LINE_NUMBER_WIDTH" private const val WindowPosX = "WINDOW_X" @@ -56,6 +57,7 @@ class ExplorerState { var showByteCode by BooleanState(ShowByteCode, false) var showDex by BooleanState(ShowDex, true) var showOat by BooleanState(ShowOat, true) + var showLogs by BooleanState(ShowLogs, false) var lineNumberWidth by IntState(LineNumberWidth, 4) var indent by IntState(Indent, 4) var sourceCode: String = readSourceCode(toolPaths)