mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-08-29 18:17:08 +07:00
GitOrigin-RevId: f1de1c81f50f743457a7a89c8f03598849279f6a
334 lines
13 KiB
Kotlin
334 lines
13 KiB
Kotlin
// 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.devkit.modules.toolwindow
|
|
|
|
import com.intellij.ide.FileSelectInContext
|
|
import com.intellij.ide.SelectInManager
|
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
|
import com.intellij.openapi.actionSystem.CustomShortcutSet
|
|
import com.intellij.openapi.application.readAction
|
|
import com.intellij.openapi.fileEditor.FileEditorManager
|
|
import com.intellij.openapi.project.DumbAwareAction
|
|
import com.intellij.openapi.project.Project
|
|
import com.intellij.openapi.vfs.VirtualFile
|
|
import com.intellij.openapi.vfs.VirtualFileManager
|
|
import com.intellij.psi.PsiElement
|
|
import com.intellij.psi.PsiManager
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.launch
|
|
import java.awt.event.KeyEvent
|
|
import java.io.File
|
|
import javax.swing.JTree
|
|
import javax.swing.KeyStroke
|
|
import javax.swing.tree.DefaultMutableTreeNode
|
|
import javax.swing.tree.TreePath
|
|
|
|
/**
|
|
* Handles navigation in the modules tree.
|
|
*/
|
|
class ModulesTreeNavigator(
|
|
private val project: Project,
|
|
private val tree: JTree,
|
|
private val resetFilterIfNeeded: suspend (desiredPrefix: String) -> Unit
|
|
) {
|
|
/**
|
|
* Navigates to the selected node in the tree.
|
|
* Returns true if navigation was successful, false otherwise.
|
|
*/
|
|
suspend fun navigateToSelectedNode(): Boolean {
|
|
val path = tree.selectionPath ?: return false
|
|
val node = path.lastPathComponent as? DefaultMutableTreeNode ?: return false
|
|
return navigateToNode(node)
|
|
}
|
|
|
|
/**
|
|
* Navigates to the specified node.
|
|
* Returns true if navigation was successful, false otherwise.
|
|
*/
|
|
suspend fun navigateToNode(node: DefaultMutableTreeNode): Boolean {
|
|
val userObject = node.userObject
|
|
|
|
return when (userObject) {
|
|
is ModulesTreeModelService.SourceRootNode, is ModulesTreeModelService.ResourceRootNode,
|
|
is ModulesTreeModelService.TestRootNode, is ModulesTreeModelService.TestResourceRootNode -> {
|
|
navigateToSourceRoot(userObject)
|
|
}
|
|
is ModulesTreeModelService.ModuleDependencyNode -> {
|
|
navigateToModuleDependency(userObject)
|
|
}
|
|
is ModulesTreeModelService.LibraryDependencyNode -> {
|
|
// Navigate to the library in the project view if possible
|
|
// For now, we just return true to indicate the event was handled
|
|
true
|
|
}
|
|
is ModulesTreeModelService.PluginXmlFileWithInfo, is ModulesTreeModelService.ModuleXmlFileWithInfo,
|
|
is JpsModulesService.PluginXmlFile, is JpsModulesService.ModuleXmlFile -> {
|
|
navigateToXmlFile(userObject)
|
|
}
|
|
is ModulesTreeModelService.ContentModuleNode -> {
|
|
navigateToContentModule(userObject)
|
|
}
|
|
is ModulesTreeModelService.DependencyPluginNode -> {
|
|
navigateToDependencyPlugin(userObject)
|
|
}
|
|
is ModulesTreeModelService.OldFashionDependencyNode -> {
|
|
navigateToOldFashionDependency(userObject)
|
|
}
|
|
is ModulesTreeModelService.ModuleValueNode -> {
|
|
// Navigate to the module value if possible
|
|
// For now, we just return true to indicate the event was handled
|
|
true
|
|
}
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
private suspend fun navigateToSourceRoot(userObject: Any): Boolean {
|
|
// Get the source root URL
|
|
val sourceRoot = when (userObject) {
|
|
is ModulesTreeModelService.SourceRootNode -> userObject.sourceRoot
|
|
is ModulesTreeModelService.ResourceRootNode -> userObject.sourceRoot
|
|
is ModulesTreeModelService.TestRootNode -> userObject.sourceRoot
|
|
is ModulesTreeModelService.TestResourceRootNode -> userObject.sourceRoot
|
|
else -> null
|
|
}
|
|
|
|
// Convert URL to file path and navigate to it in project view
|
|
if (sourceRoot != null) {
|
|
val url = sourceRoot.url
|
|
// URL format is typically "file://path/to/directory"
|
|
val filePath = url.removePrefix("file://")
|
|
val virtualFile = getVirtualFile(filePath)
|
|
if (virtualFile != null) {
|
|
// Get the PSI directory
|
|
val psiDirectory = readAction {
|
|
PsiManager.getInstance(project).findDirectory(virtualFile)
|
|
}
|
|
if (psiDirectory != null) {
|
|
return selectInProjectView(psiDirectory)
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private suspend fun navigateToModuleDependency(userObject: ModulesTreeModelService.ModuleDependencyNode): Boolean {
|
|
// First try to find the XML file for the module dependency
|
|
val dependencyModule = userObject.dependency.module
|
|
if (dependencyModule != null) {
|
|
// Try to find module.xml file in the module's content roots
|
|
val moduleXmlFile = findModuleXmlFile(dependencyModule)
|
|
if (moduleXmlFile != null) {
|
|
val virtualFile = getVirtualFile(moduleXmlFile.absolutePath)
|
|
if (virtualFile != null) {
|
|
FileEditorManager.getInstance(project).openFile(virtualFile, true)
|
|
return true
|
|
}
|
|
}
|
|
|
|
// If no XML file found, navigate to the module in the tree as before
|
|
val moduleNode = findModuleNodeByName(dependencyModule.name)
|
|
if (moduleNode != null) {
|
|
return selectAndScrollToNode(moduleNode)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Finds the module.xml file for the given module.
|
|
*/
|
|
private fun findModuleXmlFile(module: org.jetbrains.jps.model.module.JpsModule): File? {
|
|
// Get the module directory
|
|
val moduleDir = module.contentRootsList.urls.firstOrNull()?.removePrefix("file://")?.let { File(it) }
|
|
if (moduleDir != null && moduleDir.exists()) {
|
|
// Look for module.xml in META-INF directory
|
|
val metaInfDir = File(moduleDir, "META-INF")
|
|
if (metaInfDir.exists()) {
|
|
val moduleXml = File(metaInfDir, "module.xml")
|
|
if (moduleXml.exists()) {
|
|
return moduleXml
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
private suspend fun navigateToXmlFile(userObject: Any): Boolean {
|
|
// Get the file and navigate to it in the project view
|
|
val file = when (userObject) {
|
|
is ModulesTreeModelService.PluginXmlFileWithInfo -> userObject.file
|
|
is ModulesTreeModelService.ModuleXmlFileWithInfo -> userObject.file
|
|
is JpsModulesService.PluginXmlFile -> userObject.file
|
|
is JpsModulesService.ModuleXmlFile -> userObject.file
|
|
else -> null
|
|
}
|
|
|
|
if (file != null) {
|
|
val virtualFile = getVirtualFile(file.absolutePath)
|
|
if (virtualFile != null) {
|
|
FileEditorManager.getInstance(project).openFile(virtualFile, true)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private suspend fun navigateToOldFashionDependency(userObject: ModulesTreeModelService.OldFashionDependencyNode): Boolean {
|
|
// Navigate to the old fashion dependency if possible
|
|
// Check if there's a config file to navigate to
|
|
val psiFile = readAction {
|
|
val configFile = userObject.oldFashionDependency.configFile
|
|
if (configFile != null) {
|
|
val file = File(configFile)
|
|
val virtualFile = getVirtualFile(file.absolutePath)
|
|
if (virtualFile != null) {
|
|
// Get the PSI file
|
|
PsiManager.getInstance(project).findFile(virtualFile)
|
|
} else null
|
|
} else null
|
|
}
|
|
if (psiFile != null) {
|
|
return selectInProjectView(psiFile)
|
|
}
|
|
return false
|
|
}
|
|
|
|
private suspend fun navigateToContentModule(userObject: ModulesTreeModelService.ContentModuleNode): Boolean {
|
|
// Navigate to the module in the modules tree
|
|
val contentModuleName = userObject.contentModule.name
|
|
if (contentModuleName.isNotEmpty()) {
|
|
val moduleNode = findModuleNodeByName(contentModuleName)
|
|
// Select the node if found
|
|
if (moduleNode != null) {
|
|
return selectAndScrollToNode(moduleNode)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Navigates to the plugin.xml file for the dependency plugin.
|
|
*/
|
|
private suspend fun navigateToDependencyPlugin(userObject: ModulesTreeModelService.DependencyPluginNode): Boolean {
|
|
// Find the plugin.xml file for the dependency plugin
|
|
// First, we need to find the parent node which should be the XML file node
|
|
val path = tree.selectionPath ?: return false
|
|
val node = path.lastPathComponent as? DefaultMutableTreeNode ?: return false
|
|
val parentNode = node.parent as? DefaultMutableTreeNode ?: return false
|
|
val grandParentNode = parentNode.parent as? DefaultMutableTreeNode ?: return false
|
|
|
|
// The grand parent node should be the XML file node
|
|
val xmlFileNode = grandParentNode
|
|
val xmlFileObject = xmlFileNode.userObject
|
|
|
|
// Get the XML file and navigate to it
|
|
val file = when (xmlFileObject) {
|
|
is ModulesTreeModelService.PluginXmlFileWithInfo -> xmlFileObject.file
|
|
is ModulesTreeModelService.ModuleXmlFileWithInfo -> xmlFileObject.file
|
|
is JpsModulesService.PluginXmlFile -> xmlFileObject.file
|
|
is JpsModulesService.ModuleXmlFile -> xmlFileObject.file
|
|
else -> null
|
|
}
|
|
|
|
if (file != null) {
|
|
val virtualFile = getVirtualFile(file.absolutePath)
|
|
if (virtualFile != null) {
|
|
FileEditorManager.getInstance(project).openFile(virtualFile, true)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Registers F4 key shortcut for navigating to the selected node.
|
|
*/
|
|
fun registerF4KeyShortcut(coroutineScope: CoroutineScope) {
|
|
val navigateAction = object : DumbAwareAction() {
|
|
override fun actionPerformed(e: AnActionEvent) {
|
|
coroutineScope.launch {
|
|
navigateToSelectedNode()
|
|
}
|
|
}
|
|
}
|
|
val keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0)
|
|
navigateAction.registerCustomShortcutSet(CustomShortcutSet(keyStroke), tree)
|
|
}
|
|
|
|
/**
|
|
* Finds a module node in the tree by name.
|
|
* Returns a pair of (found node, was module already visible)
|
|
*/
|
|
private suspend fun findModuleNodeByName(moduleName: String): DefaultMutableTreeNode? {
|
|
val rootNode = tree.model.root as DefaultMutableTreeNode
|
|
var moduleNode: DefaultMutableTreeNode? = null
|
|
|
|
// First try to find the module in the current tree
|
|
moduleNode = findModuleNodeInTree(rootNode, moduleName)
|
|
|
|
// If not found, reset filter and try again
|
|
if (moduleNode == null) {
|
|
resetFilterIfNeeded(moduleName)
|
|
delay(300)
|
|
// After resetting, find the node again
|
|
moduleNode = findModuleNodeInTree(rootNode, moduleName)
|
|
}
|
|
|
|
return moduleNode
|
|
}
|
|
|
|
/**
|
|
* Finds a module node in the tree by name.
|
|
* Returns the found node or null if not found.
|
|
*/
|
|
private fun findModuleNodeInTree(rootNode: DefaultMutableTreeNode, moduleName: String): DefaultMutableTreeNode? {
|
|
for (i in 0 until rootNode.childCount) {
|
|
val node = rootNode.getChildAt(i) as DefaultMutableTreeNode
|
|
val currentModuleName = (node.userObject as? org.jetbrains.jps.model.module.JpsModule)?.name
|
|
if (currentModuleName == moduleName) {
|
|
return node
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Selects and scrolls to the specified node in the tree.
|
|
*/
|
|
private suspend fun selectAndScrollToNode(node: DefaultMutableTreeNode): Boolean {
|
|
val path = TreePath(node.path)
|
|
tree.selectionPath = path
|
|
tree.scrollPathToVisible(path)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Converts a file path to a VirtualFile.
|
|
*/
|
|
private fun getVirtualFile(filePath: String): VirtualFile? {
|
|
return VirtualFileManager.getInstance().findFileByUrl("file://$filePath")
|
|
}
|
|
|
|
/**
|
|
* Selects an item in the project view.
|
|
*/
|
|
private suspend fun selectInProjectView(element: PsiElement): Boolean {
|
|
val selectInManager = SelectInManager.getInstance(project)
|
|
val context = when (element) {
|
|
is com.intellij.psi.PsiFile -> FileSelectInContext(project, element.virtualFile)
|
|
is com.intellij.psi.PsiDirectory -> FileSelectInContext(element)
|
|
else -> return false
|
|
}
|
|
|
|
for (target in selectInManager.targetList) {
|
|
if (readAction { target.canSelect(context) }) {
|
|
target.selectIn(context, true)
|
|
break
|
|
}
|
|
true
|
|
}
|
|
return false
|
|
}
|
|
}
|