mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[java] Add option to toggle debug information in bytecode viewer
#IDEA-365971 Fixed GitOrigin-RevId: 266638a7bd524f04e14ed12a9b3964030d0677fa
This commit is contained in:
committed by
intellij-monorepo-bot
parent
73feb7c8ce
commit
d47f244cf7
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user