diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromEditorAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromEditorAction.kt deleted file mode 100644 index c3b672ee5d..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromEditorAction.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.utbot.intellij.plugin.ui.actions - -import org.utbot.intellij.plugin.ui.UtTestsDialogProcessor -import org.utbot.intellij.plugin.ui.utils.PsiElementHandler -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.editor.Editor -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiMethod -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.refactoring.util.classMembers.MemberInfo -import com.intellij.testIntegration.TestIntegrationUtils - -class GenerateFromEditorAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val project = e.project ?: return - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return - val editor = e.getData(CommonDataKeys.EDITOR) ?: return - - val element = findPsiElement(file, editor) ?: return - - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - val srcClass = psiElementHandler.containingClass(element) ?: error("Containing class not found for element $element") - val srcMethods = TestIntegrationUtils.extractClassMethods(srcClass, false) - val focusedMethod = focusedMethodOrNull(element, srcMethods, psiElementHandler) - - UtTestsDialogProcessor.createDialogAndGenerateTests(project, setOf(srcClass), focusedMethod) - } - } - - private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { - val offset = editor.caretModel.offset - var element = file.findElementAt(offset) - if (element == null && offset == file.textLength) { - element = file.findElementAt(offset - 1) - } - - return element - } - - private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { - // getParentOfType might return element which does not correspond to the standard Psi hierarchy. - // Thus, make transition to the Psi if it is required. - val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) - ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } - - return methods.singleOrNull { it.member == currentMethod } - } -} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromProjectTreeAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromProjectTreeAction.kt deleted file mode 100644 index 31f7318ec2..0000000000 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromProjectTreeAction.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.utbot.intellij.plugin.ui.actions - -import org.utbot.intellij.plugin.ui.UtTestsDialogProcessor -import org.utbot.intellij.plugin.ui.utils.KotlinPsiElementHandler -import org.utbot.intellij.plugin.ui.utils.PsiElementHandler -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.util.PsiTreeUtil -import org.jetbrains.kotlin.idea.core.getPackage -import org.jetbrains.kotlin.idea.core.util.toPsiDirectory -import org.jetbrains.kotlin.idea.core.util.toPsiFile -import org.jetbrains.kotlin.psi.KtClass - -class GenerateFromProjectTreeAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val project = e.project ?: error("No project found for action event $e") - val selectedElement = e.getData(CommonDataKeys.PSI_ELEMENT) - val selectedFiles = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) - - val srcClasses = mutableSetOf() - if (selectedElement != null) srcClasses += getAllClasses(selectedElement) - if (selectedFiles != null) srcClasses += getAllClasses(project, selectedFiles) - - if (srcClasses.isEmpty()) { - error("Tests generation can be performed only on a class, package or a set of classes from one package") - } - - UtTestsDialogProcessor.createDialogAndGenerateTests(project, srcClasses, focusedMethod = null) - } - - override fun update(e: AnActionEvent) { - val project = e.project ?: error("No project found for action event $e") - val psiElement = e.getData(CommonDataKeys.PSI_ELEMENT) - val virtualFiles = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) - - val isPsiClass = psiElement is PsiClass || psiElement is KtClass - val isPsiPackage = psiElement is PsiDirectory && (psiElement.getPackage()?.qualifiedName?.isNotEmpty() ?: false) - val isPsiClassArray = virtualFiles != null && getAllClasses(project, virtualFiles).isNotEmpty() - - e.presentation.isEnabled = isPsiClass || isPsiPackage || isPsiClassArray - } - - private fun getAllClasses(psiElement: PsiElement): Set { - return when (psiElement) { - is KtClass -> setOf(KotlinPsiElementHandler().toPsi(psiElement, PsiClass::class.java)) - is PsiClass -> setOf(psiElement) - is PsiDirectory -> getAllClasses(psiElement) - else -> emptySet() - } - } - - private fun getAllClasses(directory: PsiDirectory): Set { - val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() - for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) - return allClasses - } - - private fun getAllClasses(project: Project, virtualFiles: Array): Set { - val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } - val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } - val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } - - if (!dirsArePackages) { - return emptySet() - } - val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() - for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) - - return allClasses - } - - private fun getClassesFromFile(psiFile: PsiFile): List { - val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) - return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) - .map { psiElementHandler.toPsi(it, PsiClass::class.java) } - } -} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt new file mode 100644 index 0000000000..7df71aa2f5 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt @@ -0,0 +1,138 @@ +package org.utbot.intellij.plugin.ui.actions + +import org.utbot.intellij.plugin.ui.UtTestsDialogProcessor +import org.utbot.intellij.plugin.ui.utils.KotlinPsiElementHandler +import org.utbot.intellij.plugin.ui.utils.PsiElementHandler +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.util.classMembers.MemberInfo +import com.intellij.testIntegration.TestIntegrationUtils +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import org.jetbrains.kotlin.psi.KtClass +import java.util.* + +class GenerateTestsAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val psiTargets = getPsiTargets(e) ?: return + UtTestsDialogProcessor.createDialogAndGenerateTests(project, psiTargets.first, psiTargets.second) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Pair, MemberInfo?>? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + if (editor != null) { + //The action is being called from editor + val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + val element = findPsiElement(file, editor) ?: return null + + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + val srcClass = psiElementHandler.containingClass(element) ?: return null + val srcMethods = TestIntegrationUtils.extractClassMethods(srcClass, false) + val focusedMethod = focusedMethodOrNull(element, srcMethods, psiElementHandler) + return Pair(setOf(srcClass), focusedMethod) + } + } else { + // The action is being called from 'Project' tool window + val srcClasses = mutableSetOf() + e.getData(CommonDataKeys.PSI_ELEMENT)?.let { + srcClasses += getAllClasses(it) + } + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + srcClasses += getAllClasses(project, it) + } + var commonSourceRoot = null as VirtualFile? + for (srcClass in srcClasses) { + if (commonSourceRoot == null) { + commonSourceRoot = srcClass.getSourceRoot()?: return null + } else if (commonSourceRoot != srcClass.getSourceRoot()) return null + } + if (commonSourceRoot == null) return null + val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null + + if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) + .flatMap { entry -> Arrays.stream(entry.sourceFolders) } + .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} + .findAny().isPresent ) return null + + return Pair(srcClasses, null) + } + return null + } + + private fun PsiElement?.getSourceRoot() : VirtualFile? { + val project = this?.project?: return null + val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null + return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } + + private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { + // getParentOfType might return element which does not correspond to the standard Psi hierarchy. + // Thus, make transition to the Psi if it is required. + val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) + ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } + + return methods.singleOrNull { it.member == currentMethod } + } + + private fun getAllClasses(psiElement: PsiElement): Set { + return when (psiElement) { + is KtClass -> setOf(KotlinPsiElementHandler().toPsi(psiElement, PsiClass::class.java)) + is PsiClass -> setOf(psiElement) + is PsiDirectory -> getAllClasses(psiElement) + else -> emptySet() + } + } + + private fun getAllClasses(directory: PsiDirectory): Set { + val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() + for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) + return allClasses + } + private fun getAllClasses(project: Project, virtualFiles: Array): Set { + val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } + val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } + val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } + + if (!dirsArePackages) { + return emptySet() + } + val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() + for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) + + return allClasses + } + + private fun getClassesFromFile(psiFile: PsiFile): List { + val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) + .map { psiElementHandler.toPsi(it, PsiClass::class.java) } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt index 414e653f69..6455cbbf55 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt @@ -8,8 +8,7 @@ import org.jetbrains.kotlin.psi.KtFile /** * Interface to abstract some checks and hierarchy actions from working with Java or Kotlin. * - * Used in [org.utbot.intellij.plugin.ui.actions.GenerateFromEditorAction] - * and [org.utbot.intellij.plugin.ui.actions.GenerateFromProjectTreeAction]. + * Used in [org.utbot.intellij.plugin.ui.actions.GenerateTestsAction]. */ interface PsiElementHandler { companion object { diff --git a/utbot-intellij/src/main/resources/META-INF/plugin.xml b/utbot-intellij/src/main/resources/META-INF/plugin.xml index 4f47640eb8..d1eacd55a5 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -15,21 +15,14 @@ org.jetbrains.android - - - - - - - + - + + +