diff --git a/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt b/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt index 5b8891b7a05..c2235ea1abe 100644 --- a/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt +++ b/plugins/scripting/scripting-ide-services-test/test/org/jetbrains/kotlin/scripting/ide_services/ReplCompletionAndErrorsAnalysisTest.kt @@ -8,6 +8,9 @@ package org.jetbrains.kotlin.scripting.ide_services import junit.framework.TestCase import kotlinx.coroutines.runBlocking import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices +import org.jetbrains.kotlin.scripting.ide_services.compiler.completion +import org.jetbrains.kotlin.scripting.ide_services.compiler.filterOutShadowedDescriptors +import org.jetbrains.kotlin.scripting.ide_services.compiler.nameFilter import org.jetbrains.kotlin.scripting.ide_services.test_util.SourceCodeTestImpl import org.jetbrains.kotlin.scripting.ide_services.test_util.simpleScriptCompilationConfiguration import org.jetbrains.kotlin.scripting.ide_services.test_util.toList @@ -45,7 +48,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { code = "v.mema." cursor = 7 expect { - completions.mode(ComparisonType.INCLUDES) + completions.mode = ComparisonType.INCLUDES addCompletion("memx", "memx", "Int", "property") addCompletion("memy", "memy", "String", "property") } @@ -71,7 +74,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { code = testCode cursor = testCursor ?: testCode.length expect { - completions.mode(ComparisonType.EQUALS) + completions.mode = ComparisonType.EQUALS } } @@ -99,7 +102,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { cursor = 17 code = "import java.lang." expect { - completions.mode(ComparisonType.INCLUDES) + completions.mode = ComparisonType.INCLUDES addCompletion("Process", "Process", " (java.lang)", "class") } } @@ -175,7 +178,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { val v = BClass("KKK", AClass(5, "25")) """.trimIndent() expect { - errors.mode(ComparisonType.EQUALS) + errors.mode = ComparisonType.EQUALS } } @@ -224,12 +227,69 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { foo """.trimIndent() expect { - errors.mode(ComparisonType.EQUALS) + errors.mode = ComparisonType.EQUALS resultType = "Int" } } } + @Test + fun testNameFilter() = test { + run { + code = """ + val xxxyyy = 42 + val yyyxxx = 43 + """.trimIndent() + doCompile + } + + run { + code = "xxx" + cursor = 3 + expect { + addCompletion("xxxyyy", "xxxyyy", "Int", "property") + } + } + + run { + code = "xxx" + cursor = 3 + compilationConfiguration = ScriptCompilationConfiguration { + completion { + nameFilter { name, namePart -> name.endsWith(namePart) } + } + } + expect { + addCompletion("yyyxxx", "yyyxxx", "Int", "property") + } + } + } + + @Test + fun testShadowedDescriptors() = test { + for (i in 1..2) + run { + code = """ + val xxxyyy = 42 + """.trimIndent() + doCompile + } + + for ((flag, expectedSize) in listOf(true to 1, false to 2)) + run { + code = "xxx" + cursor = 3 + compilationConfiguration = ScriptCompilationConfiguration { + completion { + filterOutShadowedDescriptors(flag) + } + } + expect { + completions.size = expectedSize + } + } + } + class TestConf { private val runs = mutableListOf() @@ -261,6 +321,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { } var cursor: Int? = null + var compilationConfiguration: ScriptCompilationConfiguration? = null var code: String = "" private var _expected: Expected = Expected(this) @@ -270,7 +331,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { } fun collect(): Pair { - return RunRequest(cursor, code, _doCompile, _doComplete, _doErrorCheck) to _expected.collect() + return RunRequest(cursor, code, _doCompile, _doComplete, _doErrorCheck, compilationConfiguration) to _expected.collect() } class Expected(private val run: Run) { @@ -310,7 +371,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { } enum class ComparisonType { - INCLUDES, EQUALS, DONT_CHECK + COMPARE_SIZE, INCLUDES, EQUALS, DONT_CHECK } data class RunRequest( @@ -319,20 +380,29 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { val doCompile: Boolean, val doComplete: Boolean, val doErrorCheck: Boolean, + val compilationConfiguration: ScriptCompilationConfiguration? ) - class ExpectedList(private val runProperty: KProperty0) { + interface ExpectedOptions { + val mode: ComparisonType + val size: Int + } + + class ExpectedList(private val runProperty: KProperty0) : ExpectedOptions { val list = mutableListOf() - private var _compMode = ComparisonType.DONT_CHECK - fun mode() = _compMode - fun mode(mode: ComparisonType) { - _compMode = mode - } + override var mode = ComparisonType.DONT_CHECK + override var size = 0 + set(value) { + if (mode == ComparisonType.DONT_CHECK) + mode = ComparisonType.COMPARE_SIZE + runProperty.get() + field = value + } fun add(elem: T) { - if (_compMode == ComparisonType.DONT_CHECK) - _compMode = ComparisonType.EQUALS + if (mode == ComparisonType.DONT_CHECK) + mode = ComparisonType.EQUALS runProperty.get() list.add(elem) } @@ -373,37 +443,43 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { snippets: List ): List> { val compiler = KJvmReplCompilerWithIdeServices() - return snippets.map { (cursor, snippetText, doCompile, doComplete, doErrorCheck) -> - val pos = SourceCode.Position(0, 0, cursor) - val codeLine = nextCodeLine(snippetText) - val completionRes = if (doComplete && cursor != null) { - val res = compiler.complete(codeLine, pos, compilationConfiguration) - res.toList().filter { it.tail != "keyword" } - } else { - emptyList() + return snippets.map { runRequest -> + with(runRequest) { + val newCompilationConfiguration = this.compilationConfiguration?.let { + ScriptCompilationConfiguration(compilationConfiguration, it) + } ?: compilationConfiguration + + val pos = SourceCode.Position(0, 0, cursor) + val codeLine = nextCodeLine(code) + val completionRes = if (doComplete && cursor != null) { + val res = compiler.complete(codeLine, pos, newCompilationConfiguration) + res.toList().filter { it.tail != "keyword" } + } else { + emptyList() + } + + val analysisResult = if (doErrorCheck) { + val codeLineForErrorCheck = nextCodeLine(code) + compiler.analyze(codeLineForErrorCheck, SourceCode.Position(0, 0), newCompilationConfiguration).valueOrNull() + } else { + null + } ?: ReplAnalyzerResult() + + val errorsSequence = analysisResult[ReplAnalyzerResult.analysisDiagnostics]!! + val resultType = analysisResult[ReplAnalyzerResult.renderedResultType] + + if (doCompile) { + val codeLineForCompilation = nextCodeLine(code) + compiler.compile(codeLineForCompilation, newCompilationConfiguration) + } + + ActualResult(completionRes, errorsSequence.toList(), resultType).asSuccess() } - - val analysisResult = if (doErrorCheck) { - val codeLineForErrorCheck = nextCodeLine(snippetText) - compiler.analyze(codeLineForErrorCheck, SourceCode.Position(0, 0), compilationConfiguration).valueOrNull() - } else { - null - }?: ReplAnalyzerResult() - - val errorsSequence = analysisResult[ReplAnalyzerResult.analysisDiagnostics]!! - val resultType = analysisResult[ReplAnalyzerResult.renderedResultType] - - if (doCompile) { - val codeLineForCompilation = nextCodeLine(snippetText) - compiler.compile(codeLineForCompilation, compilationConfiguration) - } - - ActualResult(completionRes, errorsSequence.toList(), resultType).asSuccess() } } - private fun checkLists(index: Int, checkName: String, expected: List, actual: List, compType: ComparisonType) { - when (compType) { + private fun checkLists(index: Int, checkName: String, expected: List, actual: List, options: ExpectedOptions) { + when (options.mode) { ComparisonType.EQUALS -> Assert.assertEquals( "#$index ($checkName): Expected $expected, got $actual", expected, @@ -413,6 +489,11 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { "#$index ($checkName): Expected $actual to include $expected", actual.containsAll(expected) ) + ComparisonType.COMPARE_SIZE -> Assert.assertEquals( + "#$index ($checkName): Expected list size to be equal to ${options.size}, but was ${actual.size}", + options.size, + actual.size + ) ComparisonType.DONT_CHECK -> { } } @@ -431,11 +512,11 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() { val (expectedCompletions, expectedErrors, expectedResultType) = expectedIter.next() val (completionsRes, errorsRes, resultType) = res.value - checkLists(index, "completions", expectedCompletions.list, completionsRes, expectedCompletions.mode()) + checkLists(index, "completions", expectedCompletions.list, completionsRes, expectedCompletions) val expectedErrorsWithPath = expectedErrors.list.map { it.copy(sourcePath = errorsRes.firstOrNull()?.sourcePath) } - checkLists(index, "errors", expectedErrorsWithPath, errorsRes, expectedErrors.mode()) + checkLists(index, "errors", expectedErrorsWithPath, errorsRes, expectedErrors) assertEquals("Analysis result types are different", expectedResultType, resultType) } } diff --git a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/CompletionOptions.kt b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/CompletionOptions.kt new file mode 100644 index 00000000000..b620079ebff --- /dev/null +++ b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/CompletionOptions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.scripting.ide_services.compiler + +import kotlin.script.experimental.api.ScriptCompilationConfigurationKeys +import kotlin.script.experimental.util.PropertiesCollection + +interface ReplCompletionOptionsKeys + +open class ReplCompletionOptionsBuilder : PropertiesCollection.Builder(), ReplCompletionOptionsKeys { + companion object : ReplCompletionOptionsKeys +} + +fun ReplCompletionOptionsBuilder.filterOutShadowedDescriptors(value: Boolean) { + this[filterOutShadowedDescriptors] = value +} + +fun ReplCompletionOptionsBuilder.nameFilter(value: (String, String) -> Boolean) { + this[nameFilter] = value +} + +val ReplCompletionOptionsKeys.filterOutShadowedDescriptors by PropertiesCollection.key(true) +val ReplCompletionOptionsKeys.nameFilter + by PropertiesCollection.key<(String, String) -> Boolean>({ name, namePart -> name.startsWith(namePart) }) + +@Suppress("unused") +val ScriptCompilationConfigurationKeys.completion + get() = ReplCompletionOptionsBuilder() diff --git a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/KJvmReplCompilerWithIdeServices.kt b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/KJvmReplCompilerWithIdeServices.kt index 669f8ebcc36..04aa07e598b 100644 --- a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/KJvmReplCompilerWithIdeServices.kt +++ b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/KJvmReplCompilerWithIdeServices.kt @@ -54,7 +54,8 @@ class KJvmReplCompilerWithIdeServices(hostConfiguration: ScriptingHostConfigurat bindingContext, resolutionFacade, moduleDescriptor, - cursorAbs + cursorAbs, + configuration ).asSuccess(messageCollector.diagnostics) } } diff --git a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt index cd84df9dda1..29219291b8a 100644 --- a/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt +++ b/plugins/scripting/scripting-ide-services/src/org/jetbrains/kotlin/scripting/ide_services/compiler/impl/KJvmReplCompleter.kt @@ -30,11 +30,15 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.LexicalScope import org.jetbrains.kotlin.resolve.scopes.MemberScope.Companion.ALL_NAME_FILTER +import org.jetbrains.kotlin.scripting.ide_services.compiler.completion +import org.jetbrains.kotlin.scripting.ide_services.compiler.filterOutShadowedDescriptors +import org.jetbrains.kotlin.scripting.ide_services.compiler.nameFilter import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.asFlexibleType import org.jetbrains.kotlin.types.isFlexible import java.io.File import java.util.* +import kotlin.script.experimental.api.ScriptCompilationConfiguration import kotlin.script.experimental.api.SourceCodeCompletionVariant fun getKJvmCompletion( @@ -42,8 +46,16 @@ fun getKJvmCompletion( bindingContext: BindingContext, resolutionFacade: KotlinResolutionFacadeForRepl, moduleDescriptor: ModuleDescriptor, - cursor: Int -) = KJvmReplCompleter(ktScript, bindingContext, resolutionFacade, moduleDescriptor, cursor).getCompletion() + cursor: Int, + configuration: ScriptCompilationConfiguration +) = KJvmReplCompleter( + ktScript, + bindingContext, + resolutionFacade, + moduleDescriptor, + cursor, + configuration +).getCompletion() // Insert a constant string right after a cursor position to make this identifiable as a simple reference // For example, code line @@ -60,7 +72,8 @@ private class KJvmReplCompleter( private val bindingContext: BindingContext, private val resolutionFacade: KotlinResolutionFacadeForRepl, private val moduleDescriptor: ModuleDescriptor, - private val cursor: Int + private val cursor: Int, + private val configuration: ScriptCompilationConfiguration ) { private fun getElementAt(cursorPos: Int): PsiElement? { @@ -72,6 +85,9 @@ private class KJvmReplCompleter( } fun getCompletion() = sequence gen@{ + val filterOutShadowedDescriptors = configuration[ScriptCompilationConfiguration.completion.filterOutShadowedDescriptors]!! + val nameFilter = configuration[ScriptCompilationConfiguration.completion.nameFilter]!! + val element = getElementAt(cursor) var descriptors: Collection? = null @@ -109,9 +125,9 @@ private class KJvmReplCompleter( ).getReferenceVariants( simpleExpression, DescriptorKindFilter.ALL, - { name: Name -> !name.isSpecial && name.identifier.startsWith(prefix) }, + { name: Name -> !name.isSpecial && nameFilter(name.identifier, prefix) }, filterOutJavaGettersAndSetters = true, - filterOutShadowed = false, // setting to true makes it slower up to 4 times + filterOutShadowed = filterOutShadowedDescriptors, // setting to true makes it slower up to 4 times excludeNonInitializedVariable = true, useReceiverType = null ) @@ -177,7 +193,8 @@ private class KJvmReplCompleter( receiverExpression, CallTypeAndReceiver.DOT(receiverExpression), DescriptorKindFilter.ALL, - ALL_NAME_FILTER + ALL_NAME_FILTER, + filterOutShadowed = filterOutShadowedDescriptors, ) } } else { @@ -223,7 +240,7 @@ private class KJvmReplCompleter( .forEach { val descriptor = it.first val (rawName, presentableText, tailText, completionText) = it.second - if (rawName.startsWith(prefix)) { + if (nameFilter(rawName, prefix)) { val fullName: String = formatName( presentableText