diff --git a/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties index 61c45b252f72..4fb5eee34829 100644 --- a/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties +++ b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties @@ -1 +1,3 @@ -action.BytecodeViewer.text=Show Bytecode \ No newline at end of file +action.BytecodeViewer.text=Show Bytecode +action.show.debug.action.name=Show Debug Info +deserialization.error=Could not read class file \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.kt index a9f36c0d70a5..7d5bd09f9e9e 100644 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.kt +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeToolWindowPanel.kt @@ -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(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() { diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt new file mode 100644 index 000000000000..5d55e77f9649 --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt @@ -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 { + 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() + } +} \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt index c169a5374e50..b570fb02565e 100644 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt @@ -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) + } } diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/bytecodeViewerUtil.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/bytecodeViewerUtil.kt deleted file mode 100644 index c1c0bf04b79c..000000000000 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/bytecodeViewerUtil.kt +++ /dev/null @@ -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 -} \ No newline at end of file