From 67cb16c03a2df0bd29c807fa58e176d1e7f6f463 Mon Sep 17 00:00:00 2001 From: Elam Cohavi Date: Sun, 22 Oct 2023 20:45:47 +0100 Subject: [PATCH 1/3] add infix function completions --- .../org/javacs/kt/completion/Completions.kt | 23 +++++++++++++++- .../kotlin/org/javacs/kt/CompletionsTest.kt | 26 +++++++++++++++++++ .../resources/completions/InfixFunctions.kt | 21 +++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 server/src/test/resources/completions/InfixFunctions.kt diff --git a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt index aefe4cbfe..2fa1a8d1a 100644 --- a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt +++ b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt @@ -249,6 +249,8 @@ private fun completableElement(file: CompiledFile, cursor: Int): KtElement? { ?: el.parent?.parent as? KtQualifiedExpression // ? ?: el as? KtNameReferenceExpression + // x ? y (infix) + ?: el.parent as? KtBinaryExpression } private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement): Sequence { @@ -319,6 +321,12 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme val scope = file.scopeAtPoint(surroundingElement.startOffset) ?: return noResult("No scope at ${file.describePosition(cursor)}", emptySequence()) identifiers(scope) } + // x ? y (infix) + is KtBinaryExpression -> { + if (surroundingElement.operationToken == KtTokens.IDENTIFIER) { + completeMembers(file, cursor, surroundingElement.left!!) + } else emptySequence() + } else -> { LOG.info("{} {} didn't look like a type, a member, or an identifier", surroundingElement::class.simpleName, surroundingElement.text) emptySequence() @@ -326,6 +334,14 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme } } +private fun receiverDescriptors(exp: KtExpression, vararg descriptors: Sequence): Sequence { + val seq = sequenceOf(*descriptors).flatten() + if (exp.parent !is KtBinaryExpression) return seq + + // filter if infix call + return seq.filter { declarationIsInfix(it) } +} + private fun completeMembers(file: CompiledFile, cursor: Int, receiverExpr: KtExpression, unwrapNullable: Boolean = false): Sequence { // thingWithType.? var descriptors = emptySequence() @@ -341,7 +357,7 @@ private fun completeMembers(file: CompiledFile, cursor: Int, receiverExpr: KtExp LOG.debug("Completing members of instance '{}'", receiverType) val members = receiverType.memberScope.getContributedDescriptors().asSequence() val extensions = extensionFunctions(lexicalScope).filter { isExtensionFor(receiverType, it) } - descriptors = members + extensions + descriptors = receiverDescriptors(receiverExpr, members, extensions) if (!isCompanionOfEnum(receiverType) && !isCompanionOfSealed(receiverType)) { return descriptors @@ -370,6 +386,11 @@ private fun ClassDescriptor.getDescriptors(): Sequence { } +private fun declarationIsInfix(declaration: DeclarationDescriptor): Boolean { + val functionDescriptor = declaration as? FunctionDescriptor ?: return false + return functionDescriptor.isInfix +} + private fun isCompanionOfEnum(kotlinType: KotlinType): Boolean { val classDescriptor = TypeUtils.getClassDescriptor(kotlinType) val isCompanion = DescriptorUtils.isCompanionObject(classDescriptor) diff --git a/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt b/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt index b8b71c11e..8a9bd6781 100644 --- a/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt @@ -65,6 +65,32 @@ class InstanceMemberTest : SingleFileTestFixture("completions", "InstanceMember. } } +class InfixMethodTest : SingleFileTestFixture("completions", "InfixFunctions.kt") { + @Test fun `complete member function`() { + val completions = languageServer.textDocumentService.completion(completionParams(file, 10, 11)).get().right!! + val labels = completions.items.map { it.label } + + assertThat(labels, hasItem(startsWith("cmpB"))) + assertThat(labels, not(hasItem(startsWith("cmpA")))) + } + + @Test fun `includes completion for stdlib`() { + val completions = languageServer.textDocumentService.completion(completionParams(file, 15, 9)).get().right!! + val labels = completions.items.map { it.label } + + assertThat(labels, hasSize(2)) + assertThat(labels, hasItem(startsWith("and"))) + assertThat(labels, hasItem("andTo")) + } + + @Test fun `complete extension function`() { + val completions = languageServer.textDocumentService.completion(completionParams(file, 19, 9)).get().right!! + val labels = completions.items.map { it.label } + + assertThat(labels, hasItem("funcA")) + } +} + class InstanceMembersJava : SingleFileTestFixture("completions", "InstanceMembersJava.kt") { @Test fun `convert getFileName to fileName`() { val completions = languageServer.textDocumentService.completion(completionParams(file, 4, 14)).get().right!! diff --git a/server/src/test/resources/completions/InfixFunctions.kt b/server/src/test/resources/completions/InfixFunctions.kt new file mode 100644 index 000000000..9cbaa97e0 --- /dev/null +++ b/server/src/test/resources/completions/InfixFunctions.kt @@ -0,0 +1,21 @@ +class Q { + fun cmpA(): Double = 1.0 + infix fun cmpB(x: Int): Int { return 1 } +} + +infix fun Int.funcA(x: Int): Boolean = x == this +infix fun Int.andTo(v: Int) = v + +private fun memberFunc() { + Q() cm +} + +private fun stdlibFunc() { + val v = 1 + v and +} + +private fun extensionFunc() { + 2 fu 3 +} + From e95d152801c8c70474b1ca5f78149f4757cbdefd Mon Sep 17 00:00:00 2001 From: Elam Cohavi Date: Mon, 6 Nov 2023 09:26:55 +0000 Subject: [PATCH 2/3] support global scope --- .../org/javacs/kt/completion/Completions.kt | 110 +++++++++++------- .../kotlin/org/javacs/kt/CompletionsTest.kt | 18 ++- .../resources/completions/InfixFunctions.kt | 11 ++ 3 files changed, 98 insertions(+), 41 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt index 2fa1a8d1a..2662255a5 100644 --- a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt +++ b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt @@ -47,6 +47,7 @@ import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.TypeUtils import org.jetbrains.kotlin.types.typeUtil.replaceArgumentsWithStarProjections import org.jetbrains.kotlin.types.checker.KotlinTypeChecker +import org.jetbrains.kotlin.utils.addToStdlib.applyIf import java.util.concurrent.TimeUnit // The maximum number of completion items @@ -159,12 +160,14 @@ data class ElementCompletionItems(val items: Sequence, val eleme /** Finds completions based on the element around the user's cursor. */ private fun elementCompletionItems(file: CompiledFile, cursor: Int, config: CompletionConfiguration, partial: String): ElementCompletionItems { - val surroundingElement = completableElement(file, cursor) ?: return ElementCompletionItems(emptySequence()) - val completions = elementCompletions(file, cursor, surroundingElement) - - val matchesName = completions.filter { containsCharactersInOrder(name(it), partial, caseSensitive = false) } - val sorted = matchesName.takeIf { partial.length >= MIN_SORT_LENGTH }?.sortedBy { stringDistance(name(it), partial) } - ?: matchesName.sortedBy { if (name(it).startsWith(partial)) 0 else 1 } + val (surroundingElement, isGlobal) = completableElement(file, cursor) ?: return ElementCompletionItems(emptySequence()) + val completions = elementCompletions(file, cursor, surroundingElement, isGlobal) + .applyIf(isGlobal) { filter { declarationIsInfix(it) } } + .applyIf(surroundingElement.endOffset == cursor) { + filter { containsCharactersInOrder(name(it), partial, caseSensitive = false) } + } + val sorted = completions.takeIf { partial.length >= MIN_SORT_LENGTH }?.sortedBy { stringDistance(name(it), partial) } + ?: completions.sortedBy { if (name(it).startsWith(partial)) 0 else 1 } val visible = sorted.filter(isVisible(file, cursor)) return ElementCompletionItems(visible.map { completionItem(it, surroundingElement, file, config) }, surroundingElement) @@ -230,30 +233,57 @@ private fun isSetter(d: DeclarationDescriptor): Boolean = d.name.identifier.matches(Regex("set[A-Z]\\w+")) && d.valueParameters.size == 1 -private fun completableElement(file: CompiledFile, cursor: Int): KtElement? { - val el = file.parseAtPoint(cursor - 1) ?: return null - // import x.y.? - return el.findParent() - // package x.y.? - ?: el.findParent() - // :? - ?: el as? KtUserType - ?: el.parent as? KtTypeElement - // .? - ?: el as? KtQualifiedExpression - ?: el.parent as? KtQualifiedExpression - // something::? - ?: el as? KtCallableReferenceExpression - ?: el.parent as? KtCallableReferenceExpression - // something.foo() with cursor in the method - ?: el.parent?.parent as? KtQualifiedExpression - // ? - ?: el as? KtNameReferenceExpression - // x ? y (infix) - ?: el.parent as? KtBinaryExpression +private fun isGlobalCall(el: KtElement) = el is KtBlockExpression || el is KtClassBody || el.parent is KtBinaryExpression + +private fun asGlobalCompletable(file: CompiledFile, cursor: Int, el: KtElement): KtElement? { + val psi = file.parse.findElementAt(cursor) ?: return null + val element = when (val e = psi.getPrevSiblingIgnoringWhitespace() ?: psi.parent) { + is KtProperty -> e.children.lastOrNull() + is KtBinaryExpression -> el + else -> e + } + return element as? KtReferenceExpression + ?: element as? KtQualifiedExpression + ?: element as? KtConstantExpression +} + +private fun completableElement(file: CompiledFile, cursor: Int): Pair? { + val parsed = file.parseAtPoint(cursor - 1) ?: return null + val asGlobal = isGlobalCall(parsed) + val el = ( + if (asGlobal) asGlobalCompletable(file, cursor, parsed) else null + ) ?: parsed + + // import x.y.? + val elementAsType = el.findParent() + // package x.y.? + ?: el.findParent() + // :? + ?: el as? KtUserType + ?: el.parent as? KtTypeElement + // .? + ?: el as? KtQualifiedExpression + ?: el.parent as? KtQualifiedExpression + // something::? + ?: el as? KtCallableReferenceExpression + ?: el.parent as? KtCallableReferenceExpression + // something.foo() with cursor in the method + ?: el.parent?.parent as? KtQualifiedExpression + // ? + ?: el as? KtNameReferenceExpression + // x ? y (infix) + ?: el.parent as? KtBinaryExpression + // x() + ?: el as? KtCallExpression + // x (constant) + ?: el as? KtConstantExpression + + return elementAsType?.let { + Pair(it, asGlobal) + } } -private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement): Sequence { +private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement, infixCall: Boolean): Sequence { return when (surroundingElement) { // import x.y.? is KtImportDirective -> { @@ -300,7 +330,8 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme // .? is KtQualifiedExpression -> { LOG.info("Completing member expression '{}'", surroundingElement.text) - completeMembers(file, cursor, surroundingElement.receiverExpression, surroundingElement is KtSafeQualifiedExpression) + val exp = if (infixCall) surroundingElement else surroundingElement.receiverExpression + completeMembers(file, cursor, exp, surroundingElement is KtSafeQualifiedExpression) } is KtCallableReferenceExpression -> { // something::? @@ -318,8 +349,12 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme // ? is KtNameReferenceExpression -> { LOG.info("Completing identifier '{}'", surroundingElement.text) - val scope = file.scopeAtPoint(surroundingElement.startOffset) ?: return noResult("No scope at ${file.describePosition(cursor)}", emptySequence()) - identifiers(scope) + if (infixCall) { + completeMembers(file, surroundingElement.startOffset, surroundingElement) + } else { + val scope = file.scopeAtPoint(surroundingElement.startOffset) ?: return noResult("No scope at ${file.describePosition(cursor)}", emptySequence()) + identifiers(scope) + } } // x ? y (infix) is KtBinaryExpression -> { @@ -327,6 +362,9 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme completeMembers(file, cursor, surroundingElement.left!!) } else emptySequence() } + is KtCallExpression, is KtConstantExpression -> { + completeMembers(file, cursor, surroundingElement as KtExpression) + } else -> { LOG.info("{} {} didn't look like a type, a member, or an identifier", surroundingElement::class.simpleName, surroundingElement.text) emptySequence() @@ -334,14 +372,6 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme } } -private fun receiverDescriptors(exp: KtExpression, vararg descriptors: Sequence): Sequence { - val seq = sequenceOf(*descriptors).flatten() - if (exp.parent !is KtBinaryExpression) return seq - - // filter if infix call - return seq.filter { declarationIsInfix(it) } -} - private fun completeMembers(file: CompiledFile, cursor: Int, receiverExpr: KtExpression, unwrapNullable: Boolean = false): Sequence { // thingWithType.? var descriptors = emptySequence() @@ -357,7 +387,7 @@ private fun completeMembers(file: CompiledFile, cursor: Int, receiverExpr: KtExp LOG.debug("Completing members of instance '{}'", receiverType) val members = receiverType.memberScope.getContributedDescriptors().asSequence() val extensions = extensionFunctions(lexicalScope).filter { isExtensionFor(receiverType, it) } - descriptors = receiverDescriptors(receiverExpr, members, extensions) + descriptors = members + extensions if (!isCompanionOfEnum(receiverType) && !isCompanionOfSealed(receiverType)) { return descriptors diff --git a/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt b/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt index 8a9bd6781..ea43002bc 100644 --- a/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt +++ b/server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt @@ -75,7 +75,7 @@ class InfixMethodTest : SingleFileTestFixture("completions", "InfixFunctions.kt" } @Test fun `includes completion for stdlib`() { - val completions = languageServer.textDocumentService.completion(completionParams(file, 15, 9)).get().right!! + val completions = languageServer.textDocumentService.completion(completionParams(file, 15, 10)).get().right!! val labels = completions.items.map { it.label } assertThat(labels, hasSize(2)) @@ -89,6 +89,22 @@ class InfixMethodTest : SingleFileTestFixture("completions", "InfixFunctions.kt" assertThat(labels, hasItem("funcA")) } + + @Test fun `complete global scope`() { + val completions = languageServer.textDocumentService.completion(completionParams(file, 26, 11)).get().right!! + val labels = completions.items.map { it.label } + + assertThat(labels, hasSize(3)) + assertThat(labels, hasItem("ord")) + } + + @Test fun `has global completions for binary expression`() { + val completions = languageServer.textDocumentService.completion(completionParams(file, 30, 7)).get().right!! + val labels = completions.items.map { it.label } + + assertThat(labels, hasItem("funcA")) + assertThat(labels.filter { it.startsWith("and") }, hasSize(2)) + } } class InstanceMembersJava : SingleFileTestFixture("completions", "InstanceMembersJava.kt") { diff --git a/server/src/test/resources/completions/InfixFunctions.kt b/server/src/test/resources/completions/InfixFunctions.kt index 9cbaa97e0..a0b56d84b 100644 --- a/server/src/test/resources/completions/InfixFunctions.kt +++ b/server/src/test/resources/completions/InfixFunctions.kt @@ -19,3 +19,14 @@ private fun extensionFunc() { 2 fu 3 } +enum class FOO { T, U } +infix fun FOO.ord(n: Int) = this.ordinal == n + +private fun globalEnumFunc() { + FOO.U +} + +private fun globalBinaryExpFunc() { + 4 4 +} + From 71268b168f3836c32ee204e9a31f7e40a50e7902 Mon Sep 17 00:00:00 2001 From: Elam Cohavi Date: Mon, 13 Nov 2023 11:09:11 +0000 Subject: [PATCH 3/3] ci refactor --- .../org/javacs/kt/completion/Completions.kt | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt index 2662255a5..509f1e6b8 100644 --- a/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt +++ b/server/src/main/kotlin/org/javacs/kt/completion/Completions.kt @@ -247,42 +247,44 @@ private fun asGlobalCompletable(file: CompiledFile, cursor: Int, el: KtElement): ?: element as? KtConstantExpression } -private fun completableElement(file: CompiledFile, cursor: Int): Pair? { - val parsed = file.parseAtPoint(cursor - 1) ?: return null - val asGlobal = isGlobalCall(parsed) - val el = ( - if (asGlobal) asGlobalCompletable(file, cursor, parsed) else null - ) ?: parsed - - // import x.y.? - val elementAsType = el.findParent() +private fun KtElement.asKtClass(): KtElement? { + return this.findParent() // import x.y.? // package x.y.? - ?: el.findParent() + ?: this.findParent() // :? - ?: el as? KtUserType - ?: el.parent as? KtTypeElement + ?: this as? KtUserType + ?: this.parent as? KtTypeElement // .? - ?: el as? KtQualifiedExpression - ?: el.parent as? KtQualifiedExpression + ?: this as? KtQualifiedExpression + ?: this.parent as? KtQualifiedExpression // something::? - ?: el as? KtCallableReferenceExpression - ?: el.parent as? KtCallableReferenceExpression + ?: this as? KtCallableReferenceExpression + ?: this.parent as? KtCallableReferenceExpression // something.foo() with cursor in the method - ?: el.parent?.parent as? KtQualifiedExpression + ?: this.parent?.parent as? KtQualifiedExpression // ? - ?: el as? KtNameReferenceExpression + ?: this as? KtNameReferenceExpression // x ? y (infix) - ?: el.parent as? KtBinaryExpression + ?: this.parent as? KtBinaryExpression // x() - ?: el as? KtCallExpression + ?: this as? KtCallExpression // x (constant) - ?: el as? KtConstantExpression + ?: this as? KtConstantExpression +} + +private fun completableElement(file: CompiledFile, cursor: Int): Pair? { + val parsed = file.parseAtPoint(cursor - 1) ?: return null + val asGlobal = isGlobalCall(parsed) + val el = ( + if (asGlobal) asGlobalCompletable(file, cursor, parsed) else null + ) ?: parsed - return elementAsType?.let { + return el.asKtClass()?.let { Pair(it, asGlobal) } } +@Suppress("LongMethod", "ReturnCount", "CyclomaticComplexMethod") private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement, infixCall: Boolean): Sequence { return when (surroundingElement) { // import x.y.?