From 6304c351f69cdc17d99e3a632c11b8c1b08b28ae Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 2 Jul 2025 14:51:45 +0200 Subject: [PATCH] [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 --- .../messages/BytecodeViewerBundle.properties | 1 + .../BytecodeEditorSynchronizer.kt | 56 +++++++++++++++++++ .../byteCodeViewer/BytecodeViewerSettings.kt | 5 +- .../byteCodeViewer/ShowBytecodeAction.kt | 24 +++++++- 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeEditorSynchronizer.kt diff --git a/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties index b7267c2712e6..9e8f3b2160cf 100644 --- a/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties +++ b/plugins/ByteCodeViewer/resources/messages/BytecodeViewerBundle.properties @@ -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. diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeEditorSynchronizer.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeEditorSynchronizer.kt new file mode 100644 index 000000000000..c4badb5419af --- /dev/null +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeEditorSynchronizer.kt @@ -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, 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) + } + } +} \ No newline at end of file diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt index 5d55e77f9649..88826c016005 100644 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/BytecodeViewerSettings.kt @@ -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 { - data class State(var showDebugInfo: Boolean = true) + data class State( + var showDebugInfo: Boolean = true, + var syncWithEditor: Boolean = true, + ) private var myState = State() diff --git a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt index e8b8167d4814..78cc7a0c2d17 100644 --- a/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt +++ b/plugins/ByteCodeViewer/src/com/intellij/byteCodeViewer/ShowBytecodeAction.kt @@ -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) } }