Skip to content

Commit b239b2d

Browse files
UI: make our shortcut working from all appropriate places (#21)
1 parent b60c923 commit b239b2d

File tree

5 files changed

+146
-153
lines changed

5 files changed

+146
-153
lines changed

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromEditorAction.kt

Lines changed: 0 additions & 53 deletions
This file was deleted.

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateFromProjectTreeAction.kt

Lines changed: 0 additions & 84 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package org.utbot.intellij.plugin.ui.actions
2+
3+
import org.utbot.intellij.plugin.ui.UtTestsDialogProcessor
4+
import org.utbot.intellij.plugin.ui.utils.KotlinPsiElementHandler
5+
import org.utbot.intellij.plugin.ui.utils.PsiElementHandler
6+
import com.intellij.openapi.actionSystem.AnAction
7+
import com.intellij.openapi.actionSystem.AnActionEvent
8+
import com.intellij.openapi.actionSystem.CommonDataKeys
9+
import com.intellij.openapi.editor.Editor
10+
import com.intellij.openapi.module.ModuleUtil
11+
import com.intellij.openapi.project.Project
12+
import com.intellij.openapi.roots.ModuleRootManager
13+
import com.intellij.openapi.roots.ProjectFileIndex
14+
import com.intellij.openapi.vfs.VirtualFile
15+
import com.intellij.psi.*
16+
import com.intellij.psi.util.PsiTreeUtil
17+
import com.intellij.refactoring.util.classMembers.MemberInfo
18+
import com.intellij.testIntegration.TestIntegrationUtils
19+
import org.jetbrains.kotlin.idea.core.getPackage
20+
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
21+
import org.jetbrains.kotlin.idea.core.util.toPsiFile
22+
import org.jetbrains.kotlin.psi.KtClass
23+
import java.util.*
24+
25+
class GenerateTestsAction : AnAction() {
26+
override fun actionPerformed(e: AnActionEvent) {
27+
val project = e.project ?: return
28+
val psiTargets = getPsiTargets(e) ?: return
29+
UtTestsDialogProcessor.createDialogAndGenerateTests(project, psiTargets.first, psiTargets.second)
30+
}
31+
32+
override fun update(e: AnActionEvent) {
33+
e.presentation.isEnabled = getPsiTargets(e) != null
34+
}
35+
36+
private fun getPsiTargets(e: AnActionEvent): Pair<Set<PsiClass>, MemberInfo?>? {
37+
val project = e.project ?: return null
38+
val editor = e.getData(CommonDataKeys.EDITOR)
39+
if (editor != null) {
40+
//The action is being called from editor
41+
val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null
42+
val element = findPsiElement(file, editor) ?: return null
43+
44+
val psiElementHandler = PsiElementHandler.makePsiElementHandler(file)
45+
46+
if (psiElementHandler.isCreateTestActionAvailable(element)) {
47+
val srcClass = psiElementHandler.containingClass(element) ?: return null
48+
val srcMethods = TestIntegrationUtils.extractClassMethods(srcClass, false)
49+
val focusedMethod = focusedMethodOrNull(element, srcMethods, psiElementHandler)
50+
return Pair(setOf(srcClass), focusedMethod)
51+
}
52+
} else {
53+
// The action is being called from 'Project' tool window
54+
val srcClasses = mutableSetOf<PsiClass>()
55+
e.getData(CommonDataKeys.PSI_ELEMENT)?.let {
56+
srcClasses += getAllClasses(it)
57+
}
58+
e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let {
59+
srcClasses += getAllClasses(project, it)
60+
}
61+
var commonSourceRoot = null as VirtualFile?
62+
for (srcClass in srcClasses) {
63+
if (commonSourceRoot == null) {
64+
commonSourceRoot = srcClass.getSourceRoot()?: return null
65+
} else if (commonSourceRoot != srcClass.getSourceRoot()) return null
66+
}
67+
if (commonSourceRoot == null) return null
68+
val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null
69+
70+
if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries)
71+
.flatMap { entry -> Arrays.stream(entry.sourceFolders) }
72+
.filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot}
73+
.findAny().isPresent ) return null
74+
75+
return Pair(srcClasses, null)
76+
}
77+
return null
78+
}
79+
80+
private fun PsiElement?.getSourceRoot() : VirtualFile? {
81+
val project = this?.project?: return null
82+
val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null
83+
return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile)
84+
}
85+
86+
private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? {
87+
val offset = editor.caretModel.offset
88+
var element = file.findElementAt(offset)
89+
if (element == null && offset == file.textLength) {
90+
element = file.findElementAt(offset - 1)
91+
}
92+
93+
return element
94+
}
95+
96+
private fun focusedMethodOrNull(element: PsiElement, methods: List<MemberInfo>, psiElementHandler: PsiElementHandler): MemberInfo? {
97+
// getParentOfType might return element which does not correspond to the standard Psi hierarchy.
98+
// Thus, make transition to the Psi if it is required.
99+
val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass)
100+
?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) }
101+
102+
return methods.singleOrNull { it.member == currentMethod }
103+
}
104+
105+
private fun getAllClasses(psiElement: PsiElement): Set<PsiClass> {
106+
return when (psiElement) {
107+
is KtClass -> setOf(KotlinPsiElementHandler().toPsi(psiElement, PsiClass::class.java))
108+
is PsiClass -> setOf(psiElement)
109+
is PsiDirectory -> getAllClasses(psiElement)
110+
else -> emptySet()
111+
}
112+
}
113+
114+
private fun getAllClasses(directory: PsiDirectory): Set<PsiClass> {
115+
val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet()
116+
for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir)
117+
return allClasses
118+
}
119+
private fun getAllClasses(project: Project, virtualFiles: Array<VirtualFile>): Set<PsiClass> {
120+
val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) }
121+
val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) }
122+
val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true }
123+
124+
if (!dirsArePackages) {
125+
return emptySet()
126+
}
127+
val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet()
128+
for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir)
129+
130+
return allClasses
131+
}
132+
133+
private fun getClassesFromFile(psiFile: PsiFile): List<PsiClass> {
134+
val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile)
135+
return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass)
136+
.map { psiElementHandler.toPsi(it, PsiClass::class.java) }
137+
}
138+
}

utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/PsiElementHandler.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import org.jetbrains.kotlin.psi.KtFile
88
/**
99
* Interface to abstract some checks and hierarchy actions from working with Java or Kotlin.
1010
*
11-
* Used in [org.utbot.intellij.plugin.ui.actions.GenerateFromEditorAction]
12-
* and [org.utbot.intellij.plugin.ui.actions.GenerateFromProjectTreeAction].
11+
* Used in [org.utbot.intellij.plugin.ui.actions.GenerateTestsAction].
1312
*/
1413
interface PsiElementHandler {
1514
companion object {

utbot-intellij/src/main/resources/META-INF/plugin.xml

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,14 @@
1515
<depends optional="true" config-file="withAndroid.xml">org.jetbrains.android</depends>
1616

1717
<actions>
18-
<!--suppress PluginXmlCapitalization -->
19-
<action id="org.utbot.intellij.plugin.ui.actions.GenerateFromEditorAction"
20-
class="org.utbot.intellij.plugin.ui.actions.GenerateFromEditorAction"
21-
text="Create tests with UtBot..."
22-
description="Action to generate tests from editor">
23-
<add-to-group group-id="GenerateGroup" anchor="after" relative-to-action="JavaGenerateGroup1"/>
24-
<keyboard-shortcut first-keystroke="control alt U" keymap="$default"/>
25-
</action>
26-
<!--suppress PluginXmlCapitalization -->
27-
<action id="org.utbot.intellij.plugin.ui.actions.GenerateFromProjectTreeAction"
28-
class="org.utbot.intellij.plugin.ui.actions.GenerateFromProjectTreeAction"
29-
text="Create tests with UtBot..."
30-
description="Action to generate tests from project tree">
18+
<action id="org.utbot.intellij.plugin.ui.actions.GenerateTestsAction"
19+
class="org.utbot.intellij.plugin.ui.actions.GenerateTestsAction"
20+
text="Create Tests with UTBot..."
21+
description="Cover code with auto-generated tests">
3122
<add-to-group group-id="ProjectViewPopupMenu"/>
32-
<keyboard-shortcut first-keystroke="control alt U" keymap="$default"/>
23+
<add-to-group group-id="GenerateGroup" anchor="after" relative-to-action="JavaGenerateGroup1"/>
24+
<keyboard-shortcut keymap="$default" first-keystroke="control alt U"/>
25+
<keyboard-shortcut keymap="$default" first-keystroke="alt U" second-keystroke="alt T"/>
3326
</action>
3427
</actions>
3528

0 commit comments

Comments
 (0)