[java] Add option to toggle debug information in bytecode viewer

#IDEA-365971 Fixed

GitOrigin-RevId: 266638a7bd524f04e14ed12a9b3964030d0677fa
This commit is contained in:
Bart van Helvert
2025-01-20 19:43:45 +00:00
committed by intellij-monorepo-bot
parent 73feb7c8ce
commit d47f244cf7
5 changed files with 104 additions and 114 deletions
@@ -1 +1,3 @@
action.BytecodeViewer.text=Show Bytecode
action.BytecodeViewer.text=Show Bytecode
action.show.debug.action.name=Show Debug Info
deserialization.error=Could not read class file
@@ -2,9 +2,7 @@
package com.intellij.byteCodeViewer
import com.intellij.ide.highlighter.JavaClassFileType
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor
@@ -15,60 +13,61 @@ import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiFile
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiClass
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.util.Textifier
import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor
import java.awt.BorderLayout
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import javax.swing.JPanel
import kotlin.math.min
internal class BytecodeToolWindowPanel(private val project: Project, private val file: PsiFile) : JPanel(BorderLayout()), Disposable {
internal class BytecodeToolWindowPanel(
private val project: Project,
private val psiClass: PsiClass,
private val classFile: VirtualFile
) : JPanel(BorderLayout()), Disposable {
private val bytecodeEditor: Editor = EditorFactory.getInstance()
.createEditor(EditorFactory.getInstance().createDocument(""), project, JavaClassFileType.INSTANCE, true)
.also { bytecodeEditor ->
setBorder(null)
val byteCode = generateBytecodeText() ?: return@also
runWriteAction {
bytecodeEditor.document.putUserData<String?>(BYTECODE_WITH_DEBUG_INFO, byteCode.withDebugInfo)
bytecodeEditor.document.setText(StringUtil.convertLineSeparators(byteCode.withoutDebugInfo))
}
val sourceEditor = FileEditorManager.getInstance(project).getSelectedTextEditor()?.takeIf {
it.virtualFile == file.virtualFile
} ?: return@also
updateBytecodeSelection(sourceEditor, bytecodeEditor)
EditorFactory.getInstance().getEventMulticaster().addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
if (e.editor != sourceEditor) return
updateBytecodeSelection(sourceEditor, bytecodeEditor)
}
}, this@BytecodeToolWindowPanel)
}
init {
add(bytecodeEditor.getComponent())
setBorder(null)
setEditorText()
EditorFactory.getInstance().getEventMulticaster().addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
val sourceEditor = selectedMatchingEditor()
if (e.editor != sourceEditor) return
updateBytecodeSelection(sourceEditor)
}
}, this@BytecodeToolWindowPanel)
}
/** Update only text selection ranges. Do not read bytecode again.
*
* @param sourceEditor an editor that displays Java code (either real source Java or decompiled Java). If not, this method does nothing.
*/
@RequiresEdt
private fun updateBytecodeSelection(sourceEditor: Editor, bytecodeEditor: Editor) {
if (sourceEditor.getCaretModel().getCaretCount() != 1) return
val virtualFile = sourceEditor.virtualFile ?: return
if (virtualFile.fileType !== JavaFileType.INSTANCE) return
val selectedPsiElement = getPsiElement(project, sourceEditor)
if (selectedPsiElement == null) return
val containingClass = ByteCodeViewerManager.getContainingClass(selectedPsiElement)
if (containingClass == null) {
bytecodeEditor.getSelectionModel().removeSelection()
return
fun setEditorText() {
val byteCodeText = deserializeBytecode()
bytecodeEditor.document.putUserData(BYTECODE_WITH_DEBUG_INFO, byteCodeText) // include debug info for selection matching
runWriteAction {
val byteCodeToShow = if (BytecodeViewerSettings.getInstance().state.showDebugInfo) byteCodeText else removeDebugInfo(byteCodeText)
bytecodeEditor.document.setText(byteCodeToShow)
}
val sourceEditor = selectedMatchingEditor() ?: return
updateBytecodeSelection(sourceEditor)
}
private fun selectedMatchingEditor(): Editor? {
return FileEditorManager.getInstance(project).getSelectedTextEditor()?.takeIf {
it.virtualFile == psiClass.containingFile.virtualFile
}
}
private fun updateBytecodeSelection(sourceEditor: Editor) {
if (sourceEditor.getCaretModel().getCaretCount() != 1) return
val sourceStartOffset = sourceEditor.getCaretModel().getCurrentCaret().getSelectionStart()
val sourceEndOffset = sourceEditor.getCaretModel().getCurrentCaret().getSelectionEnd()
val sourceDocument = sourceEditor.getDocument()
@@ -112,10 +111,19 @@ internal class BytecodeToolWindowPanel(private val project: Project, private val
bytecodeEditor.getSelectionModel().setSelection(startOffset, endOffset)
}
private fun generateBytecodeText(): Bytecode? = runReadAction {
val selectedEditor = FileEditorManager.getInstance(project).getSelectedTextEditor() ?: return@runReadAction null
val psiElement = getPsiElement(project, selectedEditor) ?: return@runReadAction null
getByteCodeVariants(psiElement)
private fun deserializeBytecode(): String {
try {
val bytes = classFile.contentsToByteArray(false)
val stringWriter = StringWriter()
PrintWriter(stringWriter).use { printWriter ->
ClassReader(bytes).accept(TraceClassVisitor(null, Textifier(), printWriter), ClassReader.SKIP_FRAMES)
}
return stringWriter.toString()
}
catch (e: IOException) {
LOG.warn(e)
return BytecodeViewerBundle.message("deserialization.error")
}
}
override fun dispose() {
@@ -0,0 +1,22 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.byteCodeViewer
import com.intellij.openapi.components.*
@Service(Service.Level.APP)
@State(name = "BytecodeViewerSettings", storages = [Storage(StoragePathMacros.NON_ROAMABLE_FILE)])
internal class BytecodeViewerSettings : PersistentStateComponent<BytecodeViewerSettings.State> {
data class State(var showDebugInfo: Boolean = true)
private var myState = State()
override fun getState(): State = myState
override fun loadState(state: State) {
myState = state
}
companion object {
fun getInstance(): BytecodeViewerSettings = service<BytecodeViewerSettings>()
}
}
@@ -2,10 +2,7 @@
package com.intellij.byteCodeViewer
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.ToolWindowAnchor
import com.intellij.openapi.wm.ToolWindowManager
@@ -35,13 +32,35 @@ internal class ShowBytecodeAction : AnAction() {
val psiElement = psiFile.findElementAt(editor.caretModel.offset) ?: return
val psiClass = ByteCodeViewerManager.getContainingClass(psiElement) ?: return
val clsFile = ByteCodeViewerManager.findClassFile(psiClass) ?: return
val panel = BytecodeToolWindowPanel(project, psiFile)
val panel = BytecodeToolWindowPanel(project, psiClass, clsFile)
val content = toolWindow.contentManager.contents.firstOrNull { it.description == clsFile.presentableUrl }
?: ContentFactory.getInstance().createContent(panel, clsFile.presentableName, false).apply {
description = clsFile.presentableUrl
}
toolWindow.contentManager.addContent(content)
toolWindow.contentManager.setSelectedContent(content)
toolWindow.setAdditionalGearActions(createActionGroup())
toolWindow.activate(null)
}
private fun createActionGroup(): ActionGroup {
val action = object : ToggleAction(BytecodeViewerBundle.messagePointer("action.show.debug.action.name")) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun isSelected(e: AnActionEvent): Boolean {
return BytecodeViewerSettings.getInstance().state.showDebugInfo
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
BytecodeViewerSettings.getInstance().state.showDebugInfo = state
val project = e.project ?: return
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(BytecodeToolWindowPanel.TOOL_WINDOW_ID) ?: return
toolWindow.contentManager.contents.forEach {
val panel = it.component as? BytecodeToolWindowPanel ?: return@forEach
panel.setEditorText()
}
}
}
return DefaultActionGroup(action)
}
}
@@ -1,61 +0,0 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.byteCodeViewer
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
import com.intellij.psi.util.PsiUtilBase
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.ClassVisitor
import org.jetbrains.org.objectweb.asm.util.Textifier
import org.jetbrains.org.objectweb.asm.util.TraceClassVisitor
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
private val LOG = fileLogger()
internal fun getPsiElement(project: Project, editor: Editor): PsiElement? {
fun findElementInFile(psiFile: PsiFile?, editor: Editor): PsiElement? {
return psiFile?.findElementAt(editor.getCaretModel().offset)
}
val file = PsiUtilBase.getPsiFileInEditor(editor, project)
val injectedEditor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file)
var psiElement = findElementInFile(PsiUtilBase.getPsiFileInEditor(injectedEditor, project), injectedEditor)
if (file != null && psiElement == null) {
psiElement = findElementInFile(file, editor)
}
return psiElement
}
internal data class Bytecode(val withDebugInfo: String, val withoutDebugInfo: String)
/**
* Retrieves the bytecode representation of the class containing the provided PSI element.
*
* @return a Pair where the first value is a bytecode with debug info included, and the second value is without any debug info
*/
internal fun getByteCodeVariants(psiElement: PsiElement): Bytecode? {
val containingClass = ByteCodeViewerManager.getContainingClass(psiElement) ?: return null
try {
val bytes = ByteCodeViewerManager.loadClassFileBytes(containingClass) ?: return null
val withDebugInfoWriter = StringWriter()
PrintWriter(withDebugInfoWriter).use { printWriter ->
val textifier = Textifier()
val classVisitor: ClassVisitor = TraceClassVisitor(null, textifier, printWriter)
ClassReader(bytes).accept(classVisitor, ClassReader.SKIP_FRAMES)
}
val noDebugInfo = removeDebugInfo(withDebugInfoWriter.toString())
return Bytecode(withDebugInfoWriter.toString(), noDebugInfo)
}
catch (e: IOException) {
LOG.error(e)
}
return null
}