[ByteCodeViewer] IDEA-373600 Gracefully handle missing bytecode

Now, if 'Show Bytecode' action is triggered for a Java file for which we can't find corresponding
bytecode (as done by ByteCodeViewerManager#findClassFile), we show a message telling the user to
build the project.

GitOrigin-RevId: a39c0de5cdd4cb8158261f5d8ef13f08c56a6cd6
This commit is contained in:
Bartek Pacia
2025-06-23 16:05:18 +00:00
committed by intellij-monorepo-bot
parent 7a7c9b3f2d
commit 982c040f08
3 changed files with 82 additions and 21 deletions
@@ -1,3 +1,7 @@
action.BytecodeViewer.text=Show Bytecode
action.show.debug.action.name=Show Debug Info
deserialization.error=Could not read class file
deserialization.error=Could not read class file
bytecode.not.found.title=Bytecode not found
bytecode.not.found=Bytecode not found.
bytecode.not.found.for.class=Bytecode not found for class ''{0}''.
please.build.project=Please build the project and try again.
@@ -13,42 +13,83 @@ import com.intellij.openapi.editor.event.SelectionListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.VerticalFlowLayout
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiClass
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBPanel
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
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.awt.GridLayout
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import javax.swing.JPanel
import javax.swing.SwingConstants
import kotlin.math.min
/**
* Displays JVM bytecode in a tool window.
*
* It has 2 states:
* - there is bytecode: display a non-editable editor with source<->bytecode line matching
* - there is no bytecode: display a label instructing the user to build the project first.
*/
internal class BytecodeToolWindowPanel(
private val project: Project,
private val psiClass: PsiClass,
private val classFile: VirtualFile,
) : JPanel(BorderLayout()), Disposable {
private val classFile: VirtualFile?,
) : JBPanel<Nothing>(BorderLayout()), Disposable {
private val bytecodeEditor: Editor = EditorFactory.getInstance()
.createEditor(EditorFactory.getInstance().createDocument(""), project, JavaClassFileType.INSTANCE, true)
init {
add(bytecodeEditor.getComponent())
setBorder(null)
if (classFile == null) {
val labelsPanel = JPanel(GridLayout(0, 1))
labelsPanel.setBorder(JBUI.Borders.empty())
setEditorText()
EditorFactory.getInstance().getEventMulticaster().addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
val sourceEditor = selectedMatchingEditor()
if (e.editor != sourceEditor) return
updateBytecodeSelection(sourceEditor)
val className = psiClass.name
val message = if (className != null) BytecodeViewerBundle.message("bytecode.not.found.for.class", className)
else BytecodeViewerBundle.message("bytecode.not.found")
JBLabel(wrapWithHtml(message), SwingConstants.CENTER).apply {
foreground = UIUtil.getLabelDisabledForeground()
setBorder(JBUI.Borders.empty(2, 0))
labelsPanel.add(this)
}
}, this@BytecodeToolWindowPanel)
JBLabel(wrapWithHtml(BytecodeViewerBundle.message("please.build.project")), SwingConstants.CENTER).apply {
foreground = UIUtil.getLabelDisabledForeground()
setBorder(JBUI.Borders.empty(2, 0))
labelsPanel.add(this)
}
val centerPanel = JPanel(VerticalFlowLayout(VerticalFlowLayout.MIDDLE))
centerPanel.add(labelsPanel)
add(centerPanel, BorderLayout.CENTER)
}
else {
add(bytecodeEditor.getComponent())
setEditorText()
EditorFactory.getInstance().getEventMulticaster().addSelectionListener(object : SelectionListener {
override fun selectionChanged(e: SelectionEvent) {
val sourceEditor = selectedMatchingEditor()
if (e.editor != sourceEditor) return
updateBytecodeSelection(sourceEditor)
}
}, this@BytecodeToolWindowPanel)
}
}
fun setEditorText() {
if (classFile == null) throw IllegalStateException("Class file must not be null at this point")
val byteCodeText = deserializeBytecode()
bytecodeEditor.document.putUserData(BYTECODE_WITH_DEBUG_INFO, byteCodeText) // include debug info for selection matching
runWriteAction {
@@ -115,6 +156,7 @@ internal class BytecodeToolWindowPanel(
}
private fun deserializeBytecode(): String {
if (classFile == null) throw IllegalStateException("Class file must not be null at this point")
try {
val bytes = classFile.contentsToByteArray(false)
val stringWriter = StringWriter()
@@ -139,5 +181,13 @@ internal class BytecodeToolWindowPanel(
private val LOG = Logger.getInstance(BytecodeToolWindowPanel::class.java)
private val BYTECODE_WITH_DEBUG_INFO = Key.create<String>("BYTECODE_WITH_DEBUG_INFO")
/**
* Wrapping with HTML tags ensures the text wraps and is not cut off when the tool window becomes too narrow.
*/
@NlsContexts.Label
private fun wrapWithHtml(@NlsContexts.Label text: String): String {
return "<html>$text</html>"
}
}
}
@@ -3,10 +3,8 @@ package com.intellij.byteCodeViewer
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.wm.ToolWindowAnchor
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.psi.PsiFile
import com.intellij.ui.content.ContentFactory
internal class ShowBytecodeAction : AnAction() {
@@ -27,16 +25,25 @@ internal class ShowBytecodeAction : AnAction() {
hideOnEmptyContent = true
canCloseContent = true
}
val editor = event.getData<Editor>(CommonDataKeys.EDITOR) ?: return
val psiFile = event.getData<PsiFile>(CommonDataKeys.PSI_FILE) ?: return
val editor = event.getData(CommonDataKeys.EDITOR) ?: return
val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return
val psiElement = psiFile.findElementAt(editor.caretModel.offset) ?: return
val psiClass = ByteCodeViewerManager.getContainingClass(psiElement) ?: return
val clsFile = ByteCodeViewerManager.findClassFile(psiClass) ?: return
val clsFile = ByteCodeViewerManager.findClassFile(psiClass)
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
}
val content = if (clsFile != null) {
toolWindow.contentManager.contents.firstOrNull { it.description == clsFile.presentableUrl }
?: ContentFactory.getInstance().createContent(panel, clsFile.presentableName, false).apply {
description = clsFile.presentableUrl // appears on tab hover
}
}
else {
toolWindow.contentManager.contents.firstOrNull { it.description == null }
?: ContentFactory.getInstance().createContent(panel, BytecodeViewerBundle.message("bytecode.not.found.title"), false)
}
toolWindow.contentManager.addContent(content)
content.setDisposer(panel)
toolWindow.contentManager.setSelectedContent(content)