[ByteCodeViewer] IDEA-375222 Update tab name when tabs are closed

Current problems:
- does not respect the application-wide "Show directory for non-unique file names" setting

GitOrigin-RevId: 8b68558206017503fad07a55f0e3505ba3f73da9
This commit is contained in:
Bartek Pacia
2025-06-30 19:59:55 +00:00
committed by intellij-monorepo-bot
parent 959522053c
commit 0b01dc7106
2 changed files with 97 additions and 31 deletions
@@ -0,0 +1,91 @@
// 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.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.wm.ToolWindow
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentManagerEvent
import com.intellij.ui.content.ContentManagerListener
import java.util.concurrent.ConcurrentHashMap
/**
* Ensures that the [ContentManagerListener] is registered only once
* and properly unregistered when the project is disposed.
*/
@Service(Service.Level.PROJECT)
class BytecodeToolWindowService(private val project: Project) {
private val registeredListeners = ConcurrentHashMap<ToolWindow, ContentManagerListener>()
/**
* Registers a ContentManagerListener for the BytecodeViewer tool window if not already registered.
* The listener is responsible for updating tab titles when tabs are added or removed.
*
* If the listener for a specific instance of [ToolWindow] is already registered, this is a no-op.
*/
fun ensureContentManagerListenerRegistered(toolWindow: ToolWindow) {
val contentManager = toolWindow.contentManager
// Check if the listener is already registered
if (registeredListeners.containsKey(toolWindow)) {
return
}
val listener = object : ContentManagerListener {
override fun contentAdded(event: ContentManagerEvent) {
deduplicateTabNames(toolWindow)
}
override fun contentRemoved(event: ContentManagerEvent) {
deduplicateTabNames(toolWindow)
}
}
contentManager.addContentManagerListener(listener)
registeredListeners[toolWindow] = listener
Disposer.register(toolWindow.disposable) {
contentManager.removeContentManagerListener(listener)
registeredListeners.remove(toolWindow)
}
}
/**
* Updates tab titles to avoid duplicate names by adding path information when necessary.
*/
fun deduplicateTabNames(toolWindow: ToolWindow) {
val titlesToContents = mutableMapOf<String, MutableList<Content>>()
for (content in toolWindow.contentManager.contents) {
val classFileName = content.getUserData(JAVA_CLASS_FILE)?.name
?: throw IllegalStateException("Content entry has no JAVA_CLASS_FILE or it is null. Entry: $content")
titlesToContents.getOrPut(classFileName) { mutableListOf() }.add(content)
}
for ((classFileName, contents) in titlesToContents) {
if (contents.size == 1) {
contents[0].displayName = classFileName
}
else if (contents.size > 1) {
val paths = contents.map {
it.getUserData(JAVA_CLASS_FILE) ?: throw IllegalStateException("No class file path for content entry $it")
}
val commonAncestor = VfsUtil.getCommonAncestor(paths) ?: continue
for (i in contents.indices) {
val content = contents[i]
content.displayName = VfsUtil.getRelativePath(
content.getUserData(JAVA_CLASS_FILE) ?: throw IllegalStateException("No class file path for content entry $content"),
commonAncestor,
)
}
}
}
}
companion object {
fun getInstance(project: Project): BytecodeToolWindowService {
return project.getService(BytecodeToolWindowService::class.java)
}
}
}
@@ -6,16 +6,12 @@ import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowAnchor
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentFactory
private val JAVA_SOURCE_FILE = Key.create<VirtualFile>("JAVA_SOURCE_FILE")
private val JAVA_CLASS_FILE = Key.create<VirtualFile>("JAVA_CLASS_FILE")
internal val JAVA_CLASS_FILE = Key.create<VirtualFile>("JAVA_CLASS_FILE")
internal class ShowBytecodeAction : AnAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
@@ -35,6 +31,10 @@ internal class ShowBytecodeAction : AnAction() {
hideOnEmptyContent = true
canCloseContent = true
}
val service = BytecodeToolWindowService.getInstance(project)
service.ensureContentManagerListenerRegistered(toolWindow)
val editor = event.getData(CommonDataKeys.EDITOR) ?: return
val psiFile = event.getData(CommonDataKeys.PSI_FILE) ?: return
val psiElement = psiFile.findElementAt(editor.caretModel.offset) ?: return
@@ -77,38 +77,13 @@ internal class ShowBytecodeAction : AnAction() {
toolWindow.contentManager.addContent(content)
deduplicateTabNames(toolWindow)
service.deduplicateTabNames(toolWindow)
content.setDisposer(panel)
toolWindow.contentManager.setSelectedContent(content)
toolWindow.setAdditionalGearActions(createActionGroup())
toolWindow.activate(null)
}
private fun deduplicateTabNames(toolWindow: ToolWindow) {
val titlesToContents = mutableMapOf<String, MutableList<Content>>()
for (contentEntry in toolWindow.contentManager.contents) {
val tabName = contentEntry.getUserData(JAVA_CLASS_FILE)?.name ?: throw IllegalStateException("No title for content entry $contentEntry")
titlesToContents.getOrPut(tabName) { mutableListOf() }.add(contentEntry)
}
for (contents in titlesToContents.values) {
if (contents.size > 1) {
val paths = contents.map {
it.getUserData(JAVA_CLASS_FILE) ?: throw IllegalStateException("No class file path for content entry $it")
}
val commonAncestor = VfsUtil.getCommonAncestor(paths) ?: continue
for (i in contents.indices) {
val content = contents[i]
content.displayName = VfsUtil.getRelativePath(
content.getUserData(JAVA_CLASS_FILE) ?: throw IllegalStateException("No class file path for content entry $content"),
commonAncestor,
)
}
}
}
}
private fun createActionGroup(): ActionGroup {
val action = object : ToggleAction(BytecodeViewerBundle.messagePointer("action.show.debug.action.name")) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT