Skip to content

Commit 446f4de

Browse files
authored
Fix IndexNotReadyException in KotestStructureViewExtension (#345)
1 parent 111e9c3 commit 446f4de

File tree

11 files changed

+121
-136
lines changed

11 files changed

+121
-136
lines changed

Diff for: build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ val descriptors = listOf(
7979
),
8080
)
8181

82-
val productName = System.getenv("PRODUCT_NAME") ?: "IC-242"
82+
val productName = System.getenv("PRODUCT_NAME") ?: "IC-243"
8383
val jvmTargetVersion = System.getenv("JVM_TARGET") ?: "17"
8484
val descriptor = descriptors.first { it.sourceFolder == productName }
8585

Diff for: src/IC-242/kotlin/io/kotest/plugin/intellij/psi/superClasses.kt

+6-12
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,15 @@ import org.jetbrains.kotlin.psi.KtClassOrObject
1111
/**
1212
* Recursively returns the list of classes and interfaces extended or implemented by the class.
1313
*/
14-
@OptIn(KaAllowAnalysisOnEdt::class)
1514
fun KtClassOrObject.getAllSuperClasses(): List<FqName> {
1615
return superTypeListEntries.mapNotNull { it.typeReference }
1716
.flatMap { ref ->
18-
// SurroundSelectionWithFunctionIntention.isAvailable is called in EDT before the intention is applied
19-
// unfortunately API to avoid this was introduced in 23.2 only
20-
// this we need to move intentions to the facade or accept EDT here until 23.2- are still supported
21-
allowAnalysisOnEdt {
22-
analyze(this) {
23-
val kaType = ref.type
24-
val superTypes = (kaType.allSupertypes(false) + kaType).toList()
25-
superTypes.mapNotNull {
26-
val classId = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
27-
classId?.asSingleFqName()
28-
}
17+
analyze(this) {
18+
val kaType = ref.type
19+
val superTypes = (kaType.allSupertypes(false) + kaType).toList()
20+
superTypes.mapNotNull {
21+
val classId = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
22+
classId?.asSingleFqName()
2923
}
3024
}
3125
}

Diff for: src/IC-243/kotlin/io/kotest/plugin/intellij/psi/superClasses.kt

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
11
package io.kotest.plugin.intellij.psi
22

33
import org.jetbrains.kotlin.analysis.api.analyze
4-
import org.jetbrains.kotlin.analysis.api.permissions.KaAllowAnalysisOnEdt
5-
import org.jetbrains.kotlin.analysis.api.permissions.allowAnalysisOnEdt
4+
import org.jetbrains.kotlin.analysis.api.types.KaType
65
import org.jetbrains.kotlin.analysis.api.types.symbol
6+
import org.jetbrains.kotlin.name.ClassId
77
import org.jetbrains.kotlin.name.FqName
88
import org.jetbrains.kotlin.name.StandardClassIds
99
import org.jetbrains.kotlin.psi.KtClassOrObject
1010

1111
/**
1212
* Recursively returns the list of classes and interfaces extended or implemented by the class.
1313
*/
14-
@OptIn(KaAllowAnalysisOnEdt::class)
1514
fun KtClassOrObject.getAllSuperClasses(): List<FqName> {
1615
return superTypeListEntries.mapNotNull { it.typeReference }
1716
.flatMap { ref ->
18-
// SurroundSelectionWithFunctionIntention.isAvailable is called in EDT before the intention is applied
19-
// unfortunately API to avoid this was introduced in 23.2 only
20-
// this we need to move intentions to the facade or accept EDT here until 23.2- are still supported
21-
allowAnalysisOnEdt {
22-
analyze(this) {
23-
val kaType = ref.type
24-
val superTypes = (kaType.allSupertypes(false) + kaType).toList()
25-
superTypes.mapNotNull {
26-
val classId = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
27-
classId?.asSingleFqName()
28-
}
17+
analyze(this) {
18+
val kaType: KaType = ref.type
19+
val superTypes: List<KaType> = (kaType.allSupertypes(shouldApproximate = false) + kaType).toList()
20+
superTypes.mapNotNull {
21+
// don't include the Any supertype that is the root of all types
22+
val classId: ClassId? = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
23+
classId?.asSingleFqName()
2924
}
3025
}
3126
}

Diff for: src/IC-251/kotlin/io/kotest/plugin/intellij/psi/superClasses.kt

+6-12
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,15 @@ import org.jetbrains.kotlin.psi.KtClassOrObject
1111
/**
1212
* Recursively returns the list of classes and interfaces extended or implemented by the class.
1313
*/
14-
@OptIn(KaAllowAnalysisOnEdt::class)
1514
fun KtClassOrObject.getAllSuperClasses(): List<FqName> {
1615
return superTypeListEntries.mapNotNull { it.typeReference }
1716
.flatMap { ref ->
18-
// SurroundSelectionWithFunctionIntention.isAvailable is called in EDT before the intention is applied
19-
// unfortunately API to avoid this was introduced in 23.2 only
20-
// this we need to move intentions to the facade or accept EDT here until 23.2- are still supported
21-
allowAnalysisOnEdt {
22-
analyze(this) {
23-
val kaType = ref.type
24-
val superTypes = (kaType.allSupertypes(false) + kaType).toList()
25-
superTypes.mapNotNull {
26-
val classId = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
27-
classId?.asSingleFqName()
28-
}
17+
analyze(this) {
18+
val kaType = ref.type
19+
val superTypes = (kaType.allSupertypes(false) + kaType).toList()
20+
superTypes.mapNotNull {
21+
val classId = it.symbol?.classId?.takeIf { id -> id != StandardClassIds.Any }
22+
classId?.asSingleFqName()
2923
}
3024
}
3125
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package io.kotest.plugin.intellij.intentions
22

3-
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
43
import com.intellij.openapi.editor.Editor
54
import com.intellij.openapi.project.Project
65
import com.intellij.psi.PsiElement
@@ -9,20 +8,23 @@ import org.jetbrains.kotlin.lexer.KtToken
98
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
109
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
1110

12-
class BangIntention : PsiElementBaseIntentionAction() {
11+
class BangIntention : TestSourceOnlyIntentionAction() {
1312

14-
override fun getText(): String = "Add/Remove bang to test name"
13+
override fun getText(): String = "Add/Remove bang to test name"
1514

16-
override fun getFamilyName(): String = text
15+
override fun getFamilyName(): String = text
1716

18-
override fun isAvailable(project: Project,
19-
editor: Editor?,
20-
element: PsiElement): Boolean {
21-
return element is LeafPsiElement && element.elementType is KtToken && element.parent is KtLiteralStringTemplateEntry
22-
}
17+
override fun isAvailable(
18+
project: Project,
19+
editor: Editor?,
20+
element: PsiElement
21+
): Boolean {
22+
if (!isTestSource(element)) return false
23+
return element is LeafPsiElement && element.elementType is KtToken && element.parent is KtLiteralStringTemplateEntry
24+
}
2325

24-
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
25-
val text = if (element.text.startsWith("!")) element.text.drop(1) else "!" + element.text
26-
(element.parent.parent as KtStringTemplateExpression).updateText(text)
27-
}
26+
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
27+
val text = if (element.text.startsWith("!")) element.text.drop(1) else "!" + element.text
28+
(element.parent.parent as KtStringTemplateExpression).updateText(text)
29+
}
2830
}
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,84 @@
11
package io.kotest.plugin.intellij.intentions
22

3-
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
43
import com.intellij.openapi.application.ApplicationManager
54
import com.intellij.openapi.editor.Editor
5+
import com.intellij.openapi.project.DumbAware
66
import com.intellij.openapi.project.Project
77
import com.intellij.openapi.util.TextRange
88
import com.intellij.psi.PsiDocumentManager
99
import com.intellij.psi.PsiElement
10-
import io.kotest.plugin.intellij.psi.isContainedInSpec
1110
import org.jetbrains.kotlin.name.FqName
1211
import org.jetbrains.kotlin.psi.KtFile
1312
import org.jetbrains.kotlin.psi.KtPsiFactory
1413
import org.jetbrains.kotlin.resolve.ImportPath
1514

16-
internal var testMode = false
17-
18-
abstract class SurroundSelectionWithFunctionIntention : PsiElementBaseIntentionAction() {
15+
abstract class SurroundSelectionWithFunctionIntention : TestSourceOnlyIntentionAction(), DumbAware {
1916

2017
override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
21-
if (ApplicationManager.getApplication().isDispatchThread && !testMode) {
22-
return false
23-
}
24-
return try {
25-
editor?.selectionModel?.hasSelection() == true && element.isContainedInSpec()
26-
} catch (e: Exception) {
27-
e.printStackTrace()
28-
false
29-
}
18+
return isTestSource(element)
3019
}
3120

3221
abstract val importFQN: FqName
3322
abstract val function: String
3423

3524
override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
25+
if (editor == null) return
26+
addPsi(project, editor, element)
27+
}
3628

29+
private fun addPsi(project: Project, editor: Editor, element: PsiElement) {
3730
val docManager = PsiDocumentManager.getInstance(project)
3831
val ktfactory = KtPsiFactory(project)
3932

4033
try {
4134

42-
val selection = editor?.selectionModel
43-
if (selection?.hasSelection() == true) {
35+
val selection = editor.selectionModel
36+
if (selection.hasSelection() == true) {
4437

45-
val file = element.containingFile
46-
if (file is KtFile) {
38+
val file = element.containingFile
39+
if (file is KtFile) {
4740

48-
val line1 = editor.document.getLineNumber(selection.selectionStart)
49-
val linen = editor.document.getLineNumber(selection.selectionEnd)
41+
val line1 = editor.document.getLineNumber(selection.selectionStart)
42+
val linen = editor.document.getLineNumber(selection.selectionEnd)
5043

51-
// if our end position is column 0, then we've selected a full line - intellij wraps this onto the next line for some reason
52-
// val lineN0 = if (selection.selectionEndPosition?.column == 0) linen - 1 else linen
44+
// if our end position is column 0, then we've selected a full line - intellij wraps this onto the next line for some reason
45+
// val lineN0 = if (selection.selectionEndPosition?.column == 0) linen - 1 else linen
5346

54-
// expand the text range to include the full lines of the selection
55-
val lineStart = editor.document.getLineStartOffset(line1)
56-
val lineEnd = editor.document.getLineEndOffset(linen)
57-
val lineRange = TextRange(lineStart, lineEnd)
58-
val text = editor.document.getText(lineRange)
47+
// expand the text range to include the full lines of the selection
48+
val lineStart = editor.document.getLineStartOffset(line1)
49+
val lineEnd = editor.document.getLineEndOffset(linen)
50+
val lineRange = TextRange(lineStart, lineEnd)
51+
val text = editor.document.getText(lineRange)
5952

60-
// we need to work out how indented the first line was, so we can ident the function name the same amount
61-
val whitespacePrefix = text.takeWhile { it.isWhitespace() }
53+
// we need to work out how indented the first line was, so we can ident the function name the same amount
54+
val whitespacePrefix = text.takeWhile { it.isWhitespace() }
6255

63-
// pad each of the original lines to include some extra padding as it will be further indented
64-
// 4 spaces seems to be what most kotlin files use but I like 2 :)
65-
val paddedStatements = text.split('\n').joinToString("\n") { " $it" }
56+
// pad each of the original lines to include some extra padding as it will be further indented
57+
// 4 spaces seems to be what most kotlin files use but I like 2 :)
58+
val paddedStatements = text.split('\n').joinToString("\n") { " $it" }
6659

67-
// create a new string containing the wrapping function and the now-padded original statements
68-
val wrapped = "$whitespacePrefix$function {\n$paddedStatements\n$whitespacePrefix}"
60+
// create a new string containing the wrapping function and the now-padded original statements
61+
val wrapped = "$whitespacePrefix$function {\n$paddedStatements\n$whitespacePrefix}"
6962

70-
// place the new block at the position of the original lines
71-
editor.document.replaceString(lineStart, lineEnd, wrapped)
72-
docManager.commitDocument(editor.document)
63+
// place the new block at the position of the original lines
64+
editor.document.replaceString(lineStart, lineEnd, wrapped)
65+
docManager.commitDocument(editor.document)
7366

74-
// best add the import if needed for the function
75-
val importPath = ImportPath(importFQN, false)
76-
val list = file.importList
77-
if (list != null) {
78-
if (list.imports.none { it.importPath == importPath }) {
79-
val imp = ktfactory.createImportDirective(importPath)
80-
list.add(imp)
81-
}
82-
}
67+
// best add the import if needed for the function
68+
val importPath = ImportPath(importFQN, false)
69+
val list = file.importList
70+
if (list != null) {
71+
if (list.imports.none { it.importPath == importPath }) {
72+
val imp = ktfactory.createImportDirective(importPath)
73+
list.add(imp)
74+
}
75+
}
8376

84-
docManager.doPostponedOperationsAndUnblockDocument(editor.document)
85-
}
77+
docManager.doPostponedOperationsAndUnblockDocument(editor.document)
78+
}
79+
}
80+
} catch (e: Exception) {
81+
e.printStackTrace()
8682
}
87-
} catch (e: Exception) {
88-
e.printStackTrace()
89-
}
90-
}
83+
}
9184
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.kotest.plugin.intellij.intentions
2+
3+
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
4+
import com.intellij.openapi.roots.TestSourcesFilter
5+
import com.intellij.openapi.vfs.VirtualFile
6+
import com.intellij.psi.PsiElement
7+
8+
internal var testMode = false
9+
10+
abstract class TestSourceOnlyIntentionAction : PsiElementBaseIntentionAction() {
11+
12+
protected fun isTestSource(element: PsiElement): Boolean {
13+
val virtualFile: VirtualFile = element.containingFile?.virtualFile ?: return false
14+
return TestSourcesFilter.isTestSources(virtualFile, element.project) || testMode
15+
}
16+
}

Diff for: src/main/kotlin/io/kotest/plugin/intellij/structure/KotestStructureViewExtension.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import com.intellij.ide.structureView.StructureViewExtension
55
import com.intellij.ide.structureView.StructureViewTreeElement
66
import com.intellij.ide.util.treeView.smartTree.TreeElement
77
import com.intellij.navigation.ItemPresentation
8-
import com.intellij.openapi.application.ApplicationManager
98
import com.intellij.openapi.editor.Editor
109
import com.intellij.openapi.project.DumbService
10+
import com.intellij.openapi.roots.TestSourcesFilter
11+
import com.intellij.openapi.vfs.VirtualFile
1112
import com.intellij.psi.NavigatablePsiElement
1213
import com.intellij.psi.PsiElement
1314
import io.kotest.plugin.intellij.Test
1415
import io.kotest.plugin.intellij.TestElement
16+
import io.kotest.plugin.intellij.intentions.testMode
1517
import io.kotest.plugin.intellij.psi.specStyle
1618
import org.jetbrains.kotlin.psi.KtClassOrObject
1719

@@ -26,9 +28,13 @@ class KotestStructureViewExtension : StructureViewExtension {
2628
}
2729

2830
override fun getChildren(parent: PsiElement): Array<StructureViewTreeElement> {
29-
if (ApplicationManager.getApplication().isDispatchThread) {
31+
// we need indices available in order to scan this file because in order to determine if we have
32+
// a spec we need to check if any of the parent classes (which are different files) are spec types
33+
if (DumbService.isDumb(parent.project) && !testMode) {
3034
return emptyArray()
3135
}
36+
val virtualFile: VirtualFile = parent.containingFile?.virtualFile ?: return emptyArray()
37+
if (!TestSourcesFilter.isTestSources(virtualFile, parent.project) && !testMode) return emptyArray()
3238
val ktClassOrObject = parent as? KtClassOrObject ?: return emptyArray()
3339
val spec = ktClassOrObject.specStyle() ?: return emptyArray()
3440
val tests = spec.tests(parent, false)

Diff for: src/test/kotlin/io/kotest/plugin/intellij/intentions/AssertSoftlyIntentionTest.kt

+5-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.kotest.plugin.intellij.intentions
22

3-
import com.intellij.openapi.application.runWriteAction
4-
import com.intellij.openapi.command.CommandProcessor
3+
import com.intellij.openapi.command.WriteCommandAction
54
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
65
import io.kotest.matchers.shouldBe
76
import org.jetbrains.kotlin.idea.core.moveCaret
@@ -31,10 +30,8 @@ class AssertSoftlyIntentionTest : LightJavaCodeInsightFixtureTestCase() {
3130
val intention = myFixture.findSingleIntention("Surround statements with soft assert")
3231
intention.familyName shouldBe "Surround statements with soft assert"
3332

34-
CommandProcessor.getInstance().runUndoTransparentAction {
35-
runWriteAction {
36-
intention.invoke(project, editor, file)
37-
}
33+
WriteCommandAction.runWriteCommandAction(project) {
34+
intention.invoke(project, editor, file)
3835
}
3936

4037
file.text shouldBe """package io.kotest.samples.gradle
@@ -97,10 +94,8 @@ class FunSpecExampleTest : FunSpec({
9794
val intention = myFixture.findSingleIntention("Surround statements with soft assert")
9895
intention.familyName shouldBe "Surround statements with soft assert"
9996

100-
CommandProcessor.getInstance().runUndoTransparentAction {
101-
runWriteAction {
102-
intention.invoke(project, editor, file)
103-
}
97+
WriteCommandAction.runWriteCommandAction(project) {
98+
intention.invoke(project, editor, file)
10499
}
105100

106101
file.text shouldBe """package io.kotest.samples.gradle

0 commit comments

Comments
 (0)