Add scripting REPL completion options

nameFilter - way of filtering descriptors by their names;
filterOutShadowedDescriptors - if true, filters out descriptors
shadowed by descriptors in latter-compiled snippets. May slow down
completion performance.
This commit is contained in:
Ilya Muradyan
2020-05-13 03:19:47 +03:00
committed by Ilya Chernikov
parent 0ab9b3639b
commit bbe00b2fdc
4 changed files with 182 additions and 52 deletions
@@ -8,6 +8,9 @@ package org.jetbrains.kotlin.scripting.ide_services
import junit.framework.TestCase import junit.framework.TestCase
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.jetbrains.kotlin.scripting.ide_services.compiler.KJvmReplCompilerWithIdeServices 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.SourceCodeTestImpl
import org.jetbrains.kotlin.scripting.ide_services.test_util.simpleScriptCompilationConfiguration import org.jetbrains.kotlin.scripting.ide_services.test_util.simpleScriptCompilationConfiguration
import org.jetbrains.kotlin.scripting.ide_services.test_util.toList import org.jetbrains.kotlin.scripting.ide_services.test_util.toList
@@ -45,7 +48,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
code = "v.mema." code = "v.mema."
cursor = 7 cursor = 7
expect { expect {
completions.mode(ComparisonType.INCLUDES) completions.mode = ComparisonType.INCLUDES
addCompletion("memx", "memx", "Int", "property") addCompletion("memx", "memx", "Int", "property")
addCompletion("memy", "memy", "String", "property") addCompletion("memy", "memy", "String", "property")
} }
@@ -71,7 +74,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
code = testCode code = testCode
cursor = testCursor ?: testCode.length cursor = testCursor ?: testCode.length
expect { expect {
completions.mode(ComparisonType.EQUALS) completions.mode = ComparisonType.EQUALS
} }
} }
@@ -99,7 +102,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
cursor = 17 cursor = 17
code = "import java.lang." code = "import java.lang."
expect { expect {
completions.mode(ComparisonType.INCLUDES) completions.mode = ComparisonType.INCLUDES
addCompletion("Process", "Process", " (java.lang)", "class") addCompletion("Process", "Process", " (java.lang)", "class")
} }
} }
@@ -175,7 +178,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
val v = BClass("KKK", AClass(5, "25")) val v = BClass("KKK", AClass(5, "25"))
""".trimIndent() """.trimIndent()
expect { expect {
errors.mode(ComparisonType.EQUALS) errors.mode = ComparisonType.EQUALS
} }
} }
@@ -224,12 +227,69 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
foo foo
""".trimIndent() """.trimIndent()
expect { expect {
errors.mode(ComparisonType.EQUALS) errors.mode = ComparisonType.EQUALS
resultType = "Int" 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 { class TestConf {
private val runs = mutableListOf<Run>() private val runs = mutableListOf<Run>()
@@ -261,6 +321,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
} }
var cursor: Int? = null var cursor: Int? = null
var compilationConfiguration: ScriptCompilationConfiguration? = null
var code: String = "" var code: String = ""
private var _expected: Expected = Expected(this) private var _expected: Expected = Expected(this)
@@ -270,7 +331,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
} }
fun collect(): Pair<RunRequest, ExpectedResult> { fun collect(): Pair<RunRequest, ExpectedResult> {
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) { class Expected(private val run: Run) {
@@ -310,7 +371,7 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
} }
enum class ComparisonType { enum class ComparisonType {
INCLUDES, EQUALS, DONT_CHECK COMPARE_SIZE, INCLUDES, EQUALS, DONT_CHECK
} }
data class RunRequest( data class RunRequest(
@@ -319,20 +380,29 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
val doCompile: Boolean, val doCompile: Boolean,
val doComplete: Boolean, val doComplete: Boolean,
val doErrorCheck: Boolean, val doErrorCheck: Boolean,
val compilationConfiguration: ScriptCompilationConfiguration?
) )
class ExpectedList<T>(private val runProperty: KProperty0<Unit>) { interface ExpectedOptions {
val mode: ComparisonType
val size: Int
}
class ExpectedList<T>(private val runProperty: KProperty0<Unit>) : ExpectedOptions {
val list = mutableListOf<T>() val list = mutableListOf<T>()
private var _compMode = ComparisonType.DONT_CHECK override var mode = ComparisonType.DONT_CHECK
fun mode() = _compMode override var size = 0
fun mode(mode: ComparisonType) { set(value) {
_compMode = mode if (mode == ComparisonType.DONT_CHECK)
} mode = ComparisonType.COMPARE_SIZE
runProperty.get()
field = value
}
fun add(elem: T) { fun add(elem: T) {
if (_compMode == ComparisonType.DONT_CHECK) if (mode == ComparisonType.DONT_CHECK)
_compMode = ComparisonType.EQUALS mode = ComparisonType.EQUALS
runProperty.get() runProperty.get()
list.add(elem) list.add(elem)
} }
@@ -373,37 +443,43 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
snippets: List<RunRequest> snippets: List<RunRequest>
): List<ResultWithDiagnostics<ActualResult>> { ): List<ResultWithDiagnostics<ActualResult>> {
val compiler = KJvmReplCompilerWithIdeServices() val compiler = KJvmReplCompilerWithIdeServices()
return snippets.map { (cursor, snippetText, doCompile, doComplete, doErrorCheck) -> return snippets.map { runRequest ->
val pos = SourceCode.Position(0, 0, cursor) with(runRequest) {
val codeLine = nextCodeLine(snippetText) val newCompilationConfiguration = this.compilationConfiguration?.let {
val completionRes = if (doComplete && cursor != null) { ScriptCompilationConfiguration(compilationConfiguration, it)
val res = compiler.complete(codeLine, pos, compilationConfiguration) } ?: compilationConfiguration
res.toList().filter { it.tail != "keyword" }
} else { val pos = SourceCode.Position(0, 0, cursor)
emptyList() 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 <T> checkLists(index: Int, checkName: String, expected: List<T>, actual: List<T>, compType: ComparisonType) { private fun <T> checkLists(index: Int, checkName: String, expected: List<T>, actual: List<T>, options: ExpectedOptions) {
when (compType) { when (options.mode) {
ComparisonType.EQUALS -> Assert.assertEquals( ComparisonType.EQUALS -> Assert.assertEquals(
"#$index ($checkName): Expected $expected, got $actual", "#$index ($checkName): Expected $expected, got $actual",
expected, expected,
@@ -413,6 +489,11 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
"#$index ($checkName): Expected $actual to include $expected", "#$index ($checkName): Expected $actual to include $expected",
actual.containsAll(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 -> { ComparisonType.DONT_CHECK -> {
} }
} }
@@ -431,11 +512,11 @@ class ReplCompletionAndErrorsAnalysisTest : TestCase() {
val (expectedCompletions, expectedErrors, expectedResultType) = expectedIter.next() val (expectedCompletions, expectedErrors, expectedResultType) = expectedIter.next()
val (completionsRes, errorsRes, resultType) = res.value 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 { val expectedErrorsWithPath = expectedErrors.list.map {
it.copy(sourcePath = errorsRes.firstOrNull()?.sourcePath) 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) assertEquals("Analysis result types are different", expectedResultType, resultType)
} }
} }
@@ -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()
@@ -54,7 +54,8 @@ class KJvmReplCompilerWithIdeServices(hostConfiguration: ScriptingHostConfigurat
bindingContext, bindingContext,
resolutionFacade, resolutionFacade,
moduleDescriptor, moduleDescriptor,
cursorAbs cursorAbs,
configuration
).asSuccess(messageCollector.diagnostics) ).asSuccess(messageCollector.diagnostics)
} }
} }
@@ -30,11 +30,15 @@ import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter import org.jetbrains.kotlin.resolve.scopes.DescriptorKindFilter
import org.jetbrains.kotlin.resolve.scopes.LexicalScope import org.jetbrains.kotlin.resolve.scopes.LexicalScope
import org.jetbrains.kotlin.resolve.scopes.MemberScope.Companion.ALL_NAME_FILTER 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.KotlinType
import org.jetbrains.kotlin.types.asFlexibleType import org.jetbrains.kotlin.types.asFlexibleType
import org.jetbrains.kotlin.types.isFlexible import org.jetbrains.kotlin.types.isFlexible
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.SourceCodeCompletionVariant import kotlin.script.experimental.api.SourceCodeCompletionVariant
fun getKJvmCompletion( fun getKJvmCompletion(
@@ -42,8 +46,16 @@ fun getKJvmCompletion(
bindingContext: BindingContext, bindingContext: BindingContext,
resolutionFacade: KotlinResolutionFacadeForRepl, resolutionFacade: KotlinResolutionFacadeForRepl,
moduleDescriptor: ModuleDescriptor, moduleDescriptor: ModuleDescriptor,
cursor: Int cursor: Int,
) = KJvmReplCompleter(ktScript, bindingContext, resolutionFacade, moduleDescriptor, cursor).getCompletion() 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 // Insert a constant string right after a cursor position to make this identifiable as a simple reference
// For example, code line // For example, code line
@@ -60,7 +72,8 @@ private class KJvmReplCompleter(
private val bindingContext: BindingContext, private val bindingContext: BindingContext,
private val resolutionFacade: KotlinResolutionFacadeForRepl, private val resolutionFacade: KotlinResolutionFacadeForRepl,
private val moduleDescriptor: ModuleDescriptor, private val moduleDescriptor: ModuleDescriptor,
private val cursor: Int private val cursor: Int,
private val configuration: ScriptCompilationConfiguration
) { ) {
private fun getElementAt(cursorPos: Int): PsiElement? { private fun getElementAt(cursorPos: Int): PsiElement? {
@@ -72,6 +85,9 @@ private class KJvmReplCompleter(
} }
fun getCompletion() = sequence<SourceCodeCompletionVariant> gen@{ fun getCompletion() = sequence<SourceCodeCompletionVariant> gen@{
val filterOutShadowedDescriptors = configuration[ScriptCompilationConfiguration.completion.filterOutShadowedDescriptors]!!
val nameFilter = configuration[ScriptCompilationConfiguration.completion.nameFilter]!!
val element = getElementAt(cursor) val element = getElementAt(cursor)
var descriptors: Collection<DeclarationDescriptor>? = null var descriptors: Collection<DeclarationDescriptor>? = null
@@ -109,9 +125,9 @@ private class KJvmReplCompleter(
).getReferenceVariants( ).getReferenceVariants(
simpleExpression, simpleExpression,
DescriptorKindFilter.ALL, DescriptorKindFilter.ALL,
{ name: Name -> !name.isSpecial && name.identifier.startsWith(prefix) }, { name: Name -> !name.isSpecial && nameFilter(name.identifier, prefix) },
filterOutJavaGettersAndSetters = true, 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, excludeNonInitializedVariable = true,
useReceiverType = null useReceiverType = null
) )
@@ -177,7 +193,8 @@ private class KJvmReplCompleter(
receiverExpression, receiverExpression,
CallTypeAndReceiver.DOT(receiverExpression), CallTypeAndReceiver.DOT(receiverExpression),
DescriptorKindFilter.ALL, DescriptorKindFilter.ALL,
ALL_NAME_FILTER ALL_NAME_FILTER,
filterOutShadowed = filterOutShadowedDescriptors,
) )
} }
} else { } else {
@@ -223,7 +240,7 @@ private class KJvmReplCompleter(
.forEach { .forEach {
val descriptor = it.first val descriptor = it.first
val (rawName, presentableText, tailText, completionText) = it.second val (rawName, presentableText, tailText, completionText) = it.second
if (rawName.startsWith(prefix)) { if (nameFilter(rawName, prefix)) {
val fullName: String = val fullName: String =
formatName( formatName(
presentableText presentableText