diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index ff370606b7c..6ebe2aa9e1a 100755 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -1146,6 +1146,10 @@ fun main(args: Array) { testClass { model("basic/java8") } + + testClass() { + model("incrementalResolve") + } } //TODO: move these tests into idea-completion module diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt index 43063136b49..dc9ddb5a698 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/project/KotlinCodeBlockModificationListener.kt @@ -28,6 +28,7 @@ import com.intellij.psi.PsiElement import com.intellij.psi.impl.PsiModificationTrackerImpl import com.intellij.psi.util.PsiModificationTracker import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType import org.jetbrains.kotlin.psi.psiUtil.isAncestor import org.jetbrains.kotlin.psi.psiUtil.parents @@ -51,7 +52,7 @@ class KotlinCodeBlockModificationListener( override fun modelChanged(event: PomModelEvent) { val changeSet = event.getChangeSet(treeAspect) as TreeChangeEvent? ?: return val file = changeSet.rootElement.psi.containingFile as? KtFile ?: return - if (changeSet.changedElements.any { !isInsideCodeBlock(it.psi) }) { + if (changeSet.changedElements.any { getInsideCodeBlockModificationScope(it.psi) == null }) { if (file.isPhysical) { modificationTracker.incCounter() } @@ -67,39 +68,38 @@ class KotlinCodeBlockModificationListener( file.putUserData(FILE_OUT_OF_BLOCK_MODIFICATION_COUNT, count + 1) } - private fun isInsideCodeBlock(element: PsiElement): Boolean { - val lambda = KtPsiUtil.getTopmostParentOfTypes(element, KtLambdaExpression::class.java) + fun getInsideCodeBlockModificationScope(element: PsiElement): KtElement? { + val lambda = element.getTopmostParentOfType() if (lambda is KtLambdaExpression) { - if (KtPsiUtil.getTopmostParentOfTypes(lambda, KtSuperTypeCallEntry::class.java) != null) { - return true - } + lambda.getTopmostParentOfType() + ?.let { return it } } - val blockDeclaration = KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) ?: return false - if (blockDeclaration.parents.any { it !is KtClassBody && it !is KtClassOrObject && it !is KtFile }) return false // should not be local declaration + val blockDeclaration = KtPsiUtil.getTopmostParentOfTypes(element, *BLOCK_DECLARATION_TYPES) ?: return null + if (blockDeclaration.parents.any { it !is KtClassBody && it !is KtClassOrObject && it !is KtFile }) return null // should not be local declaration when (blockDeclaration) { is KtNamedFunction -> { if (blockDeclaration.hasBlockBody()) { - return blockDeclaration.bodyExpression.isAncestor(element) + return blockDeclaration.bodyExpression?.takeIf { it.isAncestor(element) } } else if (blockDeclaration.hasDeclaredReturnType()) { - return blockDeclaration.initializer.isAncestor(element) + return blockDeclaration.initializer?.takeIf { it.isAncestor(element) } } } is KtProperty -> { for (accessor in blockDeclaration.accessors) { - if (accessor.initializer.isAncestor(element) || accessor.bodyExpression.isAncestor(element)) { - return true - } + (accessor.initializer ?: accessor.bodyExpression) + ?.takeIf { it.isAncestor(element) } + ?.let { return it } } } else -> throw IllegalStateException() } - return false + return null } fun isBlockDeclaration(declaration: KtDeclaration): Boolean { diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBindingContextProvider.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBindingContextProvider.kt new file mode 100644 index 00000000000..9a9ff3762f5 --- /dev/null +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionBindingContextProvider.kt @@ -0,0 +1,171 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.completion + +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.idea.analysis.analyzeInContext +import org.jetbrains.kotlin.idea.project.KotlinCodeBlockModificationListener +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.psi.KtBlockExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf +import org.jetbrains.kotlin.psi.psiUtil.siblings +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.CompositeBindingContext +import org.jetbrains.kotlin.resolve.bindingContextUtil.getDataFlowInfoBefore +import org.jetbrains.kotlin.resolve.calls.context.ContextDependency +import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.resolve.scopes.LexicalScope +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull +import java.lang.ref.SoftReference +import java.util.* + +class CompletionBindingContextProvider(project: Project) { + private val LOG = Logger.getInstance(CompletionBindingContextProvider::class.java) + + @TestOnly + internal var TEST_LOG: StringBuilder? = null + + companion object { + fun getInstance(project: Project): CompletionBindingContextProvider + = project.getComponent(CompletionBindingContextProvider::class.java) + } + + private class CompletionData( + val block: KtBlockExpression, + val prevStatement: KtExpression?, + val psiElementsBeforeAndAfter: List, + val bindingContext: BindingContext, + val statementResolutionScope: LexicalScope, + val statementDataFlowInfo: DataFlowInfo, + val debugText: String + ) + + private data class PsiElementData(val element: PsiElement, val level: Int) + + private class DataHolder { + private var reference: SoftReference? = null + + var data: CompletionData? + get() = reference?.get() + set(value) { reference = value?.let { SoftReference(it) } } + } + + private var prevCompletionDataCache: CachedValue = CachedValuesManager.getManager(project).createCachedValue( + { CachedValueProvider.Result.create(DataHolder(), PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT) }, + false) + + + fun getBindingContext(position: PsiElement, resolutionFacade: ResolutionFacade): BindingContext { + assert(!position.isPhysical) // position is in synthetic file + + val inStatement = position.findStatementInBlock() + val block = inStatement?.parent as KtBlockExpression? + val prevStatement = inStatement?.siblings(forward = false, withItself = false)?.firstIsInstanceOrNull() + val modificationScope = inStatement?.let { KotlinCodeBlockModificationListener.getInsideCodeBlockModificationScope(it) } + + val psiElementsBeforeAndAfter = modificationScope?.let { collectPsiElementsBeforeAndAfter(modificationScope, inStatement) } + + val prevCompletionData = prevCompletionDataCache.value.data + if (prevCompletionData == null) { + log("No up-to-date data from previous completion\n") + } + else if (block != prevCompletionData.block) { + log("Not in the same block\n") + } + else if (prevStatement != prevCompletionData.prevStatement) { + log("Previous statement is not the same\n") + } + else if (psiElementsBeforeAndAfter != prevCompletionData.psiElementsBeforeAndAfter) { + log("PSI-tree has changed inside current scope\n") + } + else if (inStatement.isTooComplex()) { + log("Current statement is too complex to use optimization\n") + } + else { + log("Statement position is the same - analyzing only one statement:\n${inStatement.text.prependIndent(" ")}\n") + LOG.debug("Reusing data from completion of \"${prevCompletionData.debugText}\"") + + //TODO: expected type? + val statementContext = inStatement.analyzeInContext(scope = prevCompletionData.statementResolutionScope, + contextExpression = block, + dataFlowInfo = prevCompletionData.statementDataFlowInfo, + isStatement = true, + contextDependency = ContextDependency.DEPENDENT) + // we do not update prevCompletionDataCache because the same data should work + return CompositeBindingContext.create(listOf(statementContext, prevCompletionData.bindingContext)) + } + + val bindingContext = resolutionFacade.analyze(position.parentsWithSelf.firstIsInstance(), BodyResolveMode.PARTIAL_FOR_COMPLETION) + prevCompletionDataCache.value.data = if (block != null && modificationScope != null) { + val resolutionScope = inStatement.getResolutionScope(bindingContext, resolutionFacade) + val dataFlowInfo = bindingContext.getDataFlowInfoBefore(inStatement) + CompletionData(block, prevStatement, psiElementsBeforeAndAfter!!, bindingContext, resolutionScope, dataFlowInfo, + debugText = position.text) + } + else { + null + } + + return bindingContext + } + + private fun log(message: String) { + TEST_LOG?.append(message) + LOG.debug(message) + } + + private fun collectPsiElementsBeforeAndAfter(scope: PsiElement, statement: KtExpression): List { + return ArrayList().apply { addElementsInTree(scope, 0, statement) } + } + + private fun MutableList.addElementsInTree(root: PsiElement, initialLevel: Int, skipSubtree: PsiElement) { + if (root == skipSubtree) return + add(PsiElementData(root, initialLevel)) + var child = root.firstChild + while (child != null) { + if (child !is PsiWhiteSpace && child !is PsiComment && child !is PsiErrorElement) { + addElementsInTree(child, initialLevel + 1, skipSubtree) + } + child = child.nextSibling + } + } + + private fun PsiElement.findStatementInBlock(): KtExpression? { + return parents.filterIsInstance().firstOrNull { it.parent is KtBlockExpression } + } + + private fun KtExpression.isTooComplex(): Boolean { + return anyDescendantOfType { it.statements.size > 1 } + } +} \ No newline at end of file diff --git a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt index 8aa60f93ba1..faad46e5bce 100644 --- a/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt +++ b/idea/idea-completion/src/org/jetbrains/kotlin/idea/completion/CompletionSession.kt @@ -39,7 +39,6 @@ import org.jetbrains.kotlin.idea.project.TargetPlatformDetector import org.jetbrains.kotlin.idea.references.mainReference import org.jetbrains.kotlin.idea.util.* import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.resolve.descriptorUtil.module import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode @@ -48,7 +47,6 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.checker.KotlinTypeChecker import org.jetbrains.kotlin.types.typeUtil.makeNotNullable -import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import java.util.* class CompletionSessionConfiguration( @@ -104,7 +102,7 @@ abstract class CompletionSession( } } - protected val bindingContext = resolutionFacade.analyze(position.parentsWithSelf.firstIsInstance(), BodyResolveMode.PARTIAL_FOR_COMPLETION) + protected val bindingContext = CompletionBindingContextProvider.getInstance(project).getBindingContext(position, resolutionFacade) protected val inDescriptor = position.getResolutionScope(bindingContext, resolutionFacade).ownerDescriptor private val kotlinIdentifierStartPattern = StandardPatterns.character().javaIdentifierStart().andNot(singleCharPattern('$')) diff --git a/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.kt b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.kt new file mode 100644 index 00000000000..6a100944dac --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.kt @@ -0,0 +1,12 @@ +fun foo(p1: Int, p2: String): Int { + val v1 = p2 + val v2 = 123 + return +} + +// TYPE: ".hashCode()" +// COMPLETION_TYPE: SMART +// EXIST: v1 +// EXIST: v2 +// EXIST: p1 +// ABSENT: p2 diff --git a/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.log b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.log new file mode 100644 index 00000000000..ae4c423df65 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged.log @@ -0,0 +1,2 @@ +No up-to-date data from previous completion +PSI-tree has changed inside current scope diff --git a/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.kt b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.kt new file mode 100644 index 00000000000..a3a0a899430 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.kt @@ -0,0 +1,12 @@ +fun foo(p: Int, p1: String): Int { + val v1 = p + val v2 = 123 + return +} + +// TYPE: "1" +// COMPLETION_TYPE: SMART +// ABSENT: v1 +// EXIST: v2 +// EXIST: p +// ABSENT: p1 diff --git a/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.log b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.log new file mode 100644 index 00000000000..ae4c423df65 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.log @@ -0,0 +1,2 @@ +No up-to-date data from previous completion +PSI-tree has changed inside current scope diff --git a/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.kt b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.kt new file mode 100644 index 00000000000..6e4e7fac6e0 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.kt @@ -0,0 +1,8 @@ +fun foo(p: Any): String { + if (p !is String) return "" + r +} + +// TYPE: "eturn " +// COMPLETION_TYPE: SMART +// EXIST: p diff --git a/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.log b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.log new file mode 100644 index 00000000000..ceab9e19cc9 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.log @@ -0,0 +1,3 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + return IntellijIdeaRulezzz diff --git a/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.kt b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.kt new file mode 100644 index 00000000000..75fc106b72b --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.kt @@ -0,0 +1,10 @@ +fun foo(p: Any): String { + println(1) + if ( +} + +fun bar(s: String): Boolean = true + +// TYPE: "p is String && bar(" +// COMPLETION_TYPE: SMART +// EXIST: p diff --git a/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.log b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.log new file mode 100644 index 00000000000..5797704fac3 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.log @@ -0,0 +1,4 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + if (p is String && bar(IntellijIdeaRulezzz$ + diff --git a/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.kt b/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.kt new file mode 100644 index 00000000000..df16c1c67f2 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.kt @@ -0,0 +1,17 @@ +fun foo(p1: Int, p2: String): String { + val v1 = p1.toString() + val v2 = 123 + if (p1 > 0) { + print(1) + print(2) + } + else + r +} + +// TYPE: "eturn " +// COMPLETION_TYPE: SMART +// ABSENT: p1 +// EXIST: p2 +// EXIST: v1 +// ABSENT: v2 diff --git a/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.log b/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.log new file mode 100644 index 00000000000..c6436fa82d0 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.log @@ -0,0 +1,2 @@ +No up-to-date data from previous completion +Current statement is too complex to use optimization diff --git a/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.kt b/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.kt new file mode 100644 index 00000000000..72f835d59ad --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.kt @@ -0,0 +1,10 @@ +fun foo(p: String?): String { + println() + bar(p!!, ) +} + +fun bar(s: String, p: Int) { } + +// BACKSPACES: 4 +// TYPE: "." +// EXIST: { itemText: "substring", attributes: "grayed" } \ No newline at end of file diff --git a/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.log b/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.log new file mode 100644 index 00000000000..8b6ecd01d21 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.log @@ -0,0 +1,3 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + bar(p.IntellijIdeaRulezzz$ diff --git a/idea/idea-completion/testData/incrementalResolve/noPrevStatement.kt b/idea/idea-completion/testData/incrementalResolve/noPrevStatement.kt new file mode 100644 index 00000000000..ba132eb6d5c --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/noPrevStatement.kt @@ -0,0 +1,8 @@ +fun foo(p1: Int, p2: String): String { + r +} + +// TYPE: "eturn " +// COMPLETION_TYPE: SMART +// ABSENT: p1 +// EXIST: p2 diff --git a/idea/idea-completion/testData/incrementalResolve/noPrevStatement.log b/idea/idea-completion/testData/incrementalResolve/noPrevStatement.log new file mode 100644 index 00000000000..ceab9e19cc9 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/noPrevStatement.log @@ -0,0 +1,3 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + return IntellijIdeaRulezzz diff --git a/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.kt b/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.kt new file mode 100644 index 00000000000..d4f995faed9 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.kt @@ -0,0 +1,10 @@ +fun foo(): Int { + println() + return +} + +val xxx + +// TYPE: " = 1" +// COMPLETION_TYPE: SMART +// EXIST: xxx diff --git a/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.log b/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.log new file mode 100644 index 00000000000..c9f43e58f75 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/outOfBlockModification.log @@ -0,0 +1,2 @@ +No up-to-date data from previous completion +No up-to-date data from previous completion diff --git a/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.kt b/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.kt new file mode 100644 index 00000000000..baf85abb41a --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.kt @@ -0,0 +1,17 @@ +fun foo(p1: Any, p2: Any) { + if (p1 !is String) return + val v1 = "a" + val v2 = 123 + println("a") + bar(x) + if (p2 !is String) return +} + +fun bar(s: String){} + +// BACKSPACES: 1 +// COMPLETION_TYPE: SMART +// EXIST: v1 +// ABSENT: v2 +// EXIST: p1 +// ABSENT: p2 diff --git a/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.log b/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.log new file mode 100644 index 00000000000..96466d41698 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.log @@ -0,0 +1,3 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + bar(IntellijIdeaRulezzz$ diff --git a/idea/idea-completion/testData/incrementalResolve/sameStatement.kt b/idea/idea-completion/testData/incrementalResolve/sameStatement.kt new file mode 100644 index 00000000000..30c8b133ef9 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/sameStatement.kt @@ -0,0 +1,12 @@ +fun foo(p1: Int, p2: String): String { + val v1 = p1.toString() + val v2 = 123 + r +} + +// TYPE: "eturn " +// COMPLETION_TYPE: SMART +// ABSENT: p1 +// EXIST: p2 +// EXIST: v1 +// ABSENT: v2 diff --git a/idea/idea-completion/testData/incrementalResolve/sameStatement.log b/idea/idea-completion/testData/incrementalResolve/sameStatement.log new file mode 100644 index 00000000000..ceab9e19cc9 --- /dev/null +++ b/idea/idea-completion/testData/incrementalResolve/sameStatement.log @@ -0,0 +1,3 @@ +No up-to-date data from previous completion +Statement position is the same - analyzing only one statement: + return IntellijIdeaRulezzz diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/AbstractCompletionIncrementalResolveTest.kt b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/AbstractCompletionIncrementalResolveTest.kt new file mode 100644 index 00000000000..67a4953f309 --- /dev/null +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/AbstractCompletionIncrementalResolveTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.completion.test + +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import org.jetbrains.kotlin.idea.completion.CompletionBindingContextProvider +import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor +import org.jetbrains.kotlin.idea.util.application.executeWriteCommand +import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform +import org.jetbrains.kotlin.test.InTextDirectivesUtils +import org.jetbrains.kotlin.test.KotlinTestUtils +import java.io.File + +abstract class AbstractCompletionIncrementalResolveTest : KotlinLightCodeInsightFixtureTestCase() { + private val BEFORE_MARKER = "" // position to invoke completion before + private val CHANGE_MARKER = "" // position to insert text specified by "TYPE" directive + private val TYPE_DIRECTIVE_PREFIX = "// TYPE:" + private val BACKSPACES_DIRECTIVE_PREFIX = "// BACKSPACES:" + + override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE + + protected fun doTest(testPath: String) { + val file = File(testPath) + val hasCaretMarker = FileUtil.loadFile(file, true).contains("") + myFixture.configureByFile(testPath) + + val document = myFixture.editor.document + val beforeMarkerOffset = document.text.indexOf(BEFORE_MARKER) + assertTrue("\"$BEFORE_MARKER\" is missing in file \"$testPath\"", beforeMarkerOffset >= 0) + + val changeMarkerOffset = document.text.indexOf(CHANGE_MARKER) + assertTrue("\"$CHANGE_MARKER\" is missing in file \"$testPath\"", changeMarkerOffset >= 0) + + val textToType = InTextDirectivesUtils.findArrayWithPrefixes(document.text, TYPE_DIRECTIVE_PREFIX).singleOrNull() + ?.let { StringUtil.unquoteString(it) } + val backspaceCount = InTextDirectivesUtils.getPrefixedInt(document.text, BACKSPACES_DIRECTIVE_PREFIX) + assertTrue("At least one of \"$TYPE_DIRECTIVE_PREFIX\" and \"$BACKSPACES_DIRECTIVE_PREFIX\" should be defined", + textToType != null || backspaceCount != null) + + val beforeMarker = document.createRangeMarker(beforeMarkerOffset, beforeMarkerOffset + BEFORE_MARKER.length) + val changeMarker = document.createRangeMarker(changeMarkerOffset, changeMarkerOffset + CHANGE_MARKER.length) + changeMarker.isGreedyToRight = true + + project.executeWriteCommand("") { + document.deleteString(beforeMarker.startOffset, beforeMarker.endOffset) + document.deleteString(changeMarker.startOffset, changeMarker.endOffset) + } + + val caretMarker = if (hasCaretMarker) + document.createRangeMarker(editor.caretModel.offset, editor.caretModel.offset) + else + null + editor.caretModel.moveToOffset(beforeMarker.startOffset) + + val testLog = StringBuilder() + CompletionBindingContextProvider.getInstance(project).TEST_LOG = testLog + + myFixture.complete(CompletionType.BASIC) + + project.executeWriteCommand("") { + if (backspaceCount != null) { + document.deleteString(changeMarker.startOffset - backspaceCount, changeMarker.startOffset) + } + if (textToType != null) { + document.insertString(changeMarker.startOffset, textToType) + } + } + + if (caretMarker != null) { + editor.caretModel.moveToOffset(caretMarker.startOffset) + } + else { + editor.caretModel.moveToOffset(changeMarker.endOffset) + } + + testCompletion(FileUtil.loadFile(file, true), + JvmPlatform, + { completionType, count -> myFixture.complete(completionType, count) }, + additionalValidDirectives = listOf(TYPE_DIRECTIVE_PREFIX, BACKSPACES_DIRECTIVE_PREFIX)) + + KotlinTestUtils.assertEqualsToFile(File(file.parent, file.nameWithoutExtension + ".log"), testLog.toString()) + } +} \ No newline at end of file diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionIncrementalResolveTestGenerated.java b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionIncrementalResolveTestGenerated.java new file mode 100644 index 00000000000..0939e0dcbba --- /dev/null +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionIncrementalResolveTestGenerated.java @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.completion.test; + +import com.intellij.testFramework.TestDataPath; +import org.jetbrains.kotlin.test.JUnit3RunnerWithInners; +import org.jetbrains.kotlin.test.KotlinTestUtils; +import org.jetbrains.kotlin.test.TargetBackend; +import org.jetbrains.kotlin.test.TestMetadata; +import org.junit.runner.RunWith; + +import java.io.File; +import java.util.regex.Pattern; + +/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */ +@SuppressWarnings("all") +@TestMetadata("idea/idea-completion/testData/incrementalResolve") +@TestDataPath("$PROJECT_ROOT") +@RunWith(JUnit3RunnerWithInners.class) +public class CompletionIncrementalResolveTestGenerated extends AbstractCompletionIncrementalResolveTest { + public void testAllFilesPresentInIncrementalResolve() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/idea-completion/testData/incrementalResolve"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true); + } + + @TestMetadata("codeAboveChanged.kt") + public void testCodeAboveChanged() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/codeAboveChanged.kt"); + doTest(fileName); + } + + @TestMetadata("codeAboveChanged2.kt") + public void testCodeAboveChanged2() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/codeAboveChanged2.kt"); + doTest(fileName); + } + + @TestMetadata("dataFlowInfoFromPrevStatement.kt") + public void testDataFlowInfoFromPrevStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromPrevStatement.kt"); + doTest(fileName); + } + + @TestMetadata("dataFlowInfoFromSameStatement.kt") + public void testDataFlowInfoFromSameStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/dataFlowInfoFromSameStatement.kt"); + doTest(fileName); + } + + @TestMetadata("doNotAnalyzeComplexStatement.kt") + public void testDoNotAnalyzeComplexStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/doNotAnalyzeComplexStatement.kt"); + doTest(fileName); + } + + @TestMetadata("noDataFlowFromOldStatement.kt") + public void testNoDataFlowFromOldStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/noDataFlowFromOldStatement.kt"); + doTest(fileName); + } + + @TestMetadata("noPrevStatement.kt") + public void testNoPrevStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/noPrevStatement.kt"); + doTest(fileName); + } + + @TestMetadata("outOfBlockModification.kt") + public void testOutOfBlockModification() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/outOfBlockModification.kt"); + doTest(fileName); + } + + @TestMetadata("prevStatementNotResolved.kt") + public void testPrevStatementNotResolved() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/prevStatementNotResolved.kt"); + doTest(fileName); + } + + @TestMetadata("sameStatement.kt") + public void testSameStatement() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/idea-completion/testData/incrementalResolve/sameStatement.kt"); + doTest(fileName); + } +} diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionTestUtil.kt b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionTestUtil.kt index 21611511bf4..588c0bda9ee 100644 --- a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionTestUtil.kt +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/CompletionTestUtil.kt @@ -27,13 +27,20 @@ val RELATIVE_COMPLETION_TEST_DATA_BASE_PATH = "idea/idea-completion/testData" val COMPLETION_TEST_DATA_BASE_PATH = KotlinTestUtils.getHomeDirectory() + "/" + RELATIVE_COMPLETION_TEST_DATA_BASE_PATH -fun testCompletion(fileText: String, platform: TargetPlatform?, complete: (CompletionType, Int) -> Array?, defaultCompletionType: CompletionType = CompletionType.BASIC, defaultInvocationCount: Int = 0) { +fun testCompletion( + fileText: String, + platform: TargetPlatform?, + complete: (CompletionType, Int) -> Array?, + defaultCompletionType: CompletionType = CompletionType.BASIC, + defaultInvocationCount: Int = 0, + additionalValidDirectives: Collection = emptyList() +) { testWithAutoCompleteSetting(fileText) { val completionType = ExpectedCompletionUtils.getCompletionType(fileText) ?: defaultCompletionType val invocationCount = ExpectedCompletionUtils.getInvocationCount(fileText) ?: defaultInvocationCount val items = complete(completionType, invocationCount) ?: emptyArray() - ExpectedCompletionUtils.assertDirectivesValid(fileText) + ExpectedCompletionUtils.assertDirectivesValid(fileText, additionalValidDirectives) val expected = ExpectedCompletionUtils.itemsShouldExist(fileText, platform) val unexpected = ExpectedCompletionUtils.itemsShouldAbsent(fileText, platform) diff --git a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/ExpectedCompletionUtils.kt b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/ExpectedCompletionUtils.kt index b0c161bf753..3da166bf9e3 100644 --- a/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/ExpectedCompletionUtils.kt +++ b/idea/idea-completion/tests/org/jetbrains/kotlin/idea/completion/test/ExpectedCompletionUtils.kt @@ -221,8 +221,8 @@ object ExpectedCompletionUtils { return !InTextDirectivesUtils.findLinesWithPrefixesRemoved(fileText, WITH_ORDER_PREFIX).isEmpty() } - fun assertDirectivesValid(fileText: String) { - InTextDirectivesUtils.assertHasUnknownPrefixes(fileText, KNOWN_PREFIXES) + fun assertDirectivesValid(fileText: String, additionalValidDirectives: Collection = emptyList()) { + InTextDirectivesUtils.assertHasUnknownPrefixes(fileText, KNOWN_PREFIXES + additionalValidDirectives) } fun assertContainsRenderedItems(expected: Array, items: Array, checkOrder: Boolean, nothingElse: Boolean) { diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 189a5a6b038..bb24ed68432 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -47,6 +47,10 @@ org.jetbrains.kotlin.idea.caches.KotlinPackageContentModificationListener + + org.jetbrains.kotlin.idea.completion.CompletionBindingContextProvider + org.jetbrains.kotlin.idea.completion.CompletionBindingContextProvider + diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/copyConcatenatedStringToClipboard/ConcatenatedStringGenerator.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/copyConcatenatedStringToClipboard/ConcatenatedStringGenerator.kt index caad9d3223a..a45d0262d78 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/copyConcatenatedStringToClipboard/ConcatenatedStringGenerator.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/copyConcatenatedStringToClipboard/ConcatenatedStringGenerator.kt @@ -20,11 +20,12 @@ import org.jetbrains.kotlin.descriptors.PropertyDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType +import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall class ConcatenatedStringGenerator { fun create(element: KtBinaryExpression): String { - val binaryExpression = KtPsiUtil.getTopmostParentOfTypes(element, KtBinaryExpression::class.java) as? KtBinaryExpression ?: element + val binaryExpression = element.getTopmostParentOfType () ?: element val stringBuilder = StringBuilder() binaryExpression.appendTo(stringBuilder) return stringBuilder.toString()