[ByteCodeViewer] IDEA-375353 Synchronize the class file being shown with the source file open in the main editor

This is optional, the user can toggle/untoggle this behavior.

GitOrigin-RevId: 6e7b8420db67b6b2fd6b36426e6f2a1478fee5bd
This commit is contained in:
Bartek Pacia
2025-07-02 22:38:29 +00:00
committed by intellij-monorepo-bot
parent c98c8991d4
commit 6304c351f6
4 changed files with 83 additions and 3 deletions
@@ -1,5 +1,6 @@
action.BytecodeViewer.text=Show Bytecode
action.show.debug.action.name=Show Debug Info
action.sync.with.editor.name=Sync With Editor
deserialization.error=Could not read class file
bytecode.not.found.title=Could not find bytecode
please.build.project=Please build the project and try again.
@@ -0,0 +1,56 @@
// 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
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiManager
import com.intellij.ui.content.Content
/**
* Synchronizes the bytecode viewer with the currently active editor.
* When the user switches between files in the editor, this service will
* automatically switch to the corresponding bytecode in the bytecode viewer.
*/
@Service(Service.Level.PROJECT)
class BytecodeEditorSynchronizer(private val project: Project) : FileEditorManagerListener {
/**
* Called when the selected editor changes.
* If sync with editor is enabled, this will switch to the corresponding bytecode.
*/
override fun selectionChanged(event: FileEditorManagerEvent) {
if (!BytecodeViewerSettings.getInstance().state.syncWithEditor) return
val virtualFile = event.newFile ?: return
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(BytecodeToolWindowPanel.TOOL_WINDOW_ID) ?: return
if (!toolWindow.isVisible) return
val content = findContentForSourceFile(toolWindow.contentManager.contents, virtualFile) ?: return
toolWindow.contentManager.setSelectedContent(content)
}
/**
* Finds the content pane (tab) in the bytecode viewer tool window that corresponds to the given `sourceFile`.
*/
private fun findContentForSourceFile(contents: Array<Content>, sourceFile: VirtualFile): Content? {
val psiFile = PsiManager.getInstance(project).findFile(sourceFile) ?: return null
if (psiFile !is PsiJavaFile) return null
for (psiClass in psiFile.classes) {
val classFile = ByteCodeViewerManager.findClassFile(psiClass) ?: continue
val content = contents.firstOrNull { it.getUserData(JAVA_CLASS_FILE) == classFile } ?: continue
return content
}
return null
}
companion object {
fun getInstance(project: Project): BytecodeEditorSynchronizer {
return project.getService(BytecodeEditorSynchronizer::class.java)
}
}
}
@@ -6,7 +6,10 @@ 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)
data class State(
var showDebugInfo: Boolean = true,
var syncWithEditor: Boolean = true,
)
private var myState = State()
@@ -5,6 +5,7 @@ import com.intellij.icons.AllIcons
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.IdeFocusManager
@@ -34,6 +35,12 @@ internal class ShowBytecodeAction : AnAction() {
}
BytecodeToolWindowService.getInstance(project).ensureContentManagerListenerRegistered(toolWindow)
// Register the editor synchronizer if not already registered
project.messageBus.connect(toolWindow.disposable).subscribe(
topic = FileEditorManagerListener.FILE_EDITOR_MANAGER,
handler = BytecodeEditorSynchronizer.getInstance(project),
)
val editor = event.getData(CommonDataKeys.EDITOR) ?: return
val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return
val psiElement = psiFile.findElementAt(editor.caretModel.offset) ?: return
@@ -85,7 +92,7 @@ internal class ShowBytecodeAction : AnAction() {
}
private fun createActionGroup(): ActionGroup {
val action = object : ToggleAction(BytecodeViewerBundle.messagePointer("action.show.debug.action.name")) {
val showDebugAction = object : ToggleAction(BytecodeViewerBundle.messagePointer("action.show.debug.action.name")) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun isSelected(e: AnActionEvent): Boolean {
@@ -102,6 +109,19 @@ internal class ShowBytecodeAction : AnAction() {
}
}
}
return DefaultActionGroup(action)
val syncWithEditorAction = object : ToggleAction(BytecodeViewerBundle.messagePointer("action.sync.with.editor.name")) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT
override fun isSelected(e: AnActionEvent): Boolean {
return BytecodeViewerSettings.getInstance().state.syncWithEditor
}
override fun setSelected(e: AnActionEvent, state: Boolean) {
BytecodeViewerSettings.getInstance().state.syncWithEditor = state
}
}
return DefaultActionGroup(showDebugAction, syncWithEditorAction)
}
}