From edb1de993e205bbf2fdd7fdd5799bb0f8232b272 Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Thu, 10 Apr 2025 18:49:13 +0300 Subject: [PATCH] IJPL-183527 Create an internal action to move source code files between modules GitOrigin-RevId: da071147e0fa5ddf0a9c7b85e1f81a383ae44459 --- .../actions/MoveFileToAnotherModuleAction.kt | 220 ++++++++++++++++++ .../src/messages/ActionsBundle.properties | 1 + .../src/idea/PlatformActions.xml | 2 + 3 files changed, 223 insertions(+) create mode 100644 platform/platform-impl/src/com/intellij/ide/actions/MoveFileToAnotherModuleAction.kt diff --git a/platform/platform-impl/src/com/intellij/ide/actions/MoveFileToAnotherModuleAction.kt b/platform/platform-impl/src/com/intellij/ide/actions/MoveFileToAnotherModuleAction.kt new file mode 100644 index 000000000000..3bd9b9926581 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ide/actions/MoveFileToAnotherModuleAction.kt @@ -0,0 +1,220 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:Suppress("HardCodedStringLiteral") + +package com.intellij.ide.actions + +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.command.WriteCommandAction.writeCommandAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleUtilCore +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.rootManager +import com.intellij.openapi.roots.SourceFolder +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.isFile +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer +import org.jetbrains.jps.model.java.JavaResourceRootProperties +import org.jetbrains.jps.model.java.JavaResourceRootType +import org.jetbrains.jps.model.java.JavaSourceRootProperties +import org.jetbrains.jps.model.java.JavaSourceRootType +import org.jetbrains.jps.model.module.JpsModuleSourceRootType +import java.io.IOException +import javax.swing.JComponent + +private class MoveFileToAnotherModuleAction : DumbAwareAction() { + private val sourceRootTypes: List> = listOf( + JavaSourceRootType.SOURCE, + JavaSourceRootType.TEST_SOURCE, + JavaResourceRootType.RESOURCE, + JavaResourceRootType.TEST_RESOURCE + ) + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + val project = e.getData(CommonDataKeys.PROJECT) + val file = e.getData(CommonDataKeys.VIRTUAL_FILE) + e.presentation.isEnabled = project != null && file != null && file.isFile + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.getData(CommonDataKeys.PROJECT) ?: return + val file = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return + val module = ModuleUtilCore.findModuleForFile(file, project) + if (module == null) { + bail("Didn't find module for file ${file.name}") + return + } + + val sourceLocation = findSourceLocation(module, file) + if (sourceLocation == null) { + bail("Didn't detect source root for file ${file.name}") + return + } + + val chooserDialog = ModuleChooserDialog(project, file.name) + if (!chooserDialog.showAndGet()) { + return + } + val targetModule = chooserDialog.selectedModule ?: return + + val targetLocation = findTargetLocation(targetModule, sourceLocation) + if (targetLocation == null) { + bail("Couldn't detect target source root in module ${targetModule.name}") + return + } + + try { + moveFile(project, file, targetLocation) + } + catch (e: IOException) { + bail(e.message ?: "Unexpected error while moving the file") + } + } + + private fun findSourceLocation(sourceModule: Module, file: VirtualFile): SourceLocation? { + val rootManager = sourceModule.rootManager + var sourceRootType: JpsModuleSourceRootType<*>? = null + var generatedSourceRoot = false + var pathFromSourceRoot: List? = null + var pathFromContentRoot: List? = null + rootManager.contentEntries.forEach outer@{ contentEntry -> + sourceRootTypes.forEach { rootType -> + contentEntry.getSourceFolders(rootType).forEach { sourceFolder -> + val root = sourceFolder.file + if (root != null) { + val path = collectPath(file, root) + if (path != null) { + sourceRootType = rootType + pathFromSourceRoot = path + generatedSourceRoot = isGenerated(sourceFolder) + return@outer + } + } + } + } + } + rootManager.contentRoots.forEach { root -> + val path = collectPath(file, root) + if (path != null) { + pathFromContentRoot = path + return@forEach + } + } + return if (pathFromSourceRoot == null && pathFromContentRoot == null) { + null + } else { + SourceLocation(sourceRootType, generatedSourceRoot, pathFromSourceRoot, pathFromContentRoot) + } + } + + private fun collectPath(file: VirtualFile, root: VirtualFile): List? { + val pathToRoot = mutableListOf() + var f = file + while (true) { + val parent = f.parent + if (parent == null) { + return null + } + if (parent == root) { + return pathToRoot.reversed() + } + pathToRoot.add(parent.name) + f = parent + } + } + + private fun isGenerated(sourceFolder: SourceFolder) = when (val props = sourceFolder.jpsElement.properties) { + is JavaSourceRootProperties -> props.isForGeneratedSources + is JavaResourceRootProperties -> props.isForGeneratedSources + else -> false + } + + private fun findTargetLocation(targetModule: Module, sourceLocation: SourceLocation): TargetLocation? { + val rootManager = targetModule.rootManager + if (sourceLocation.sourceRootType != null && sourceLocation.pathFromSourceRoot != null) { + rootManager.contentEntries.forEach { contentEntry -> + contentEntry.getSourceFolders(sourceLocation.sourceRootType).forEach { sourceFolder -> + val root = sourceFolder.file + if (root != null && sourceLocation.generatedSourceRoot == isGenerated(sourceFolder)) { + return TargetLocation(root, sourceLocation.pathFromSourceRoot) + } + } + } + } + if (sourceLocation.pathFromContentRoot != null) { + rootManager.contentRoots.firstOrNull()?.let { + return TargetLocation(it, sourceLocation.pathFromContentRoot) + } + } + return null + } + + private fun moveFile(project: Project, file: VirtualFile, targetLocation: TargetLocation) { + writeCommandAction(project).withName("Move File").run { + val requestor = this@MoveFileToAnotherModuleAction + var target = targetLocation.root + targetLocation.pathFromRoot.forEach { + val child = target.findChild(it) + target = child ?: target.createChildDirectory(requestor, it) + if (target.isFile) { + throw IOException("Existing file in target module prevents moving: ${target.path}") + } + } + file.move(requestor, target) + } + } + + private fun bail(message: String) { + Messages.showErrorDialog(message, "Error") + } + + private data class SourceLocation( + val sourceRootType: JpsModuleSourceRootType<*>?, + val generatedSourceRoot: Boolean, + val pathFromSourceRoot: List?, + val pathFromContentRoot: List? + ) + + private data class TargetLocation( + val root: VirtualFile, + val pathFromRoot: List + ) +} + +private class ModuleChooserDialog(private val project: Project, fileName: String) : DialogWrapper(project, false) { + companion object { + private const val LAST_CHOSEN_MODULE_KEY = "MoveFileToAnotherModuleAction.lastChosen" + } + val modules = ModuleManager.getInstance(project).modules.sortedBy { it.name } + var selectedModule = modules.find { it.name == PropertiesComponent.getInstance(project).getValue(LAST_CHOSEN_MODULE_KEY) } + + init { + title = "Move $fileName to" + init() + } + + override fun createCenterPanel(): JComponent { + return panel { + row("Module:") { + comboBox(modules, textListCellRenderer { it?.name ?: "" }).applyToComponent { + isSwingPopup = false // enable speed search + }.focused().bindItem(::selectedModule) + } + } + } + + override fun doOKAction() { + super.doOKAction() + PropertiesComponent.getInstance(project).setValue(LAST_CHOSEN_MODULE_KEY, selectedModule?.name) + } +} \ No newline at end of file diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index 4b124a92a985..cd2a1d1a8d3a 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -2291,6 +2291,7 @@ action.KeymapToCsv.text=Export Keymap to CSV\u2026 action.CompareKeymaps.text=Compare Keymaps\u2026 action.BrowseWeb.text=Browse Web\u2026 action.ShowSearchEverywhereItemInfo.text=Show Search Everywhere Item Info +action.MoveFileToAnotherModule.text=Move File to Another Module action.PerformGC.text=Perform GC action.LogFocusRequests.text=Log Focus Requests action.UiInspector.text=UI &Inspector diff --git a/platform/platform-resources/src/idea/PlatformActions.xml b/platform/platform-resources/src/idea/PlatformActions.xml index 76c75dc4121c..a9ab323982f6 100644 --- a/platform/platform-resources/src/idea/PlatformActions.xml +++ b/platform/platform-resources/src/idea/PlatformActions.xml @@ -1259,6 +1259,8 @@ + +