Skip to content

Commit bd76446

Browse files
authored
Infix code completion (#521)
* add infix function completions * support global scope * ci refactor
1 parent 7861c6f commit bd76446

File tree

3 files changed

+156
-29
lines changed

3 files changed

+156
-29
lines changed

server/src/main/kotlin/org/javacs/kt/completion/Completions.kt

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import org.jetbrains.kotlin.types.KotlinType
4747
import org.jetbrains.kotlin.types.TypeUtils
4848
import org.jetbrains.kotlin.types.typeUtil.replaceArgumentsWithStarProjections
4949
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
50+
import org.jetbrains.kotlin.utils.addToStdlib.applyIf
5051
import java.util.concurrent.TimeUnit
5152

5253
// The maximum number of completion items
@@ -159,12 +160,14 @@ data class ElementCompletionItems(val items: Sequence<CompletionItem>, val eleme
159160

160161
/** Finds completions based on the element around the user's cursor. */
161162
private fun elementCompletionItems(file: CompiledFile, cursor: Int, config: CompletionConfiguration, partial: String): ElementCompletionItems {
162-
val surroundingElement = completableElement(file, cursor) ?: return ElementCompletionItems(emptySequence())
163-
val completions = elementCompletions(file, cursor, surroundingElement)
164-
165-
val matchesName = completions.filter { containsCharactersInOrder(name(it), partial, caseSensitive = false) }
166-
val sorted = matchesName.takeIf { partial.length >= MIN_SORT_LENGTH }?.sortedBy { stringDistance(name(it), partial) }
167-
?: matchesName.sortedBy { if (name(it).startsWith(partial)) 0 else 1 }
163+
val (surroundingElement, isGlobal) = completableElement(file, cursor) ?: return ElementCompletionItems(emptySequence())
164+
val completions = elementCompletions(file, cursor, surroundingElement, isGlobal)
165+
.applyIf(isGlobal) { filter { declarationIsInfix(it) } }
166+
.applyIf(surroundingElement.endOffset == cursor) {
167+
filter { containsCharactersInOrder(name(it), partial, caseSensitive = false) }
168+
}
169+
val sorted = completions.takeIf { partial.length >= MIN_SORT_LENGTH }?.sortedBy { stringDistance(name(it), partial) }
170+
?: completions.sortedBy { if (name(it).startsWith(partial)) 0 else 1 }
168171
val visible = sorted.filter(isVisible(file, cursor))
169172

170173
return ElementCompletionItems(visible.map { completionItem(it, surroundingElement, file, config) }, surroundingElement)
@@ -230,28 +233,59 @@ private fun isSetter(d: DeclarationDescriptor): Boolean =
230233
d.name.identifier.matches(Regex("set[A-Z]\\w+")) &&
231234
d.valueParameters.size == 1
232235

233-
private fun completableElement(file: CompiledFile, cursor: Int): KtElement? {
234-
val el = file.parseAtPoint(cursor - 1) ?: return null
235-
// import x.y.?
236-
return el.findParent<KtImportDirective>()
237-
// package x.y.?
238-
?: el.findParent<KtPackageDirective>()
239-
// :?
240-
?: el as? KtUserType
241-
?: el.parent as? KtTypeElement
242-
// .?
243-
?: el as? KtQualifiedExpression
244-
?: el.parent as? KtQualifiedExpression
245-
// something::?
246-
?: el as? KtCallableReferenceExpression
247-
?: el.parent as? KtCallableReferenceExpression
248-
// something.foo() with cursor in the method
249-
?: el.parent?.parent as? KtQualifiedExpression
250-
// ?
251-
?: el as? KtNameReferenceExpression
236+
private fun isGlobalCall(el: KtElement) = el is KtBlockExpression || el is KtClassBody || el.parent is KtBinaryExpression
237+
238+
private fun asGlobalCompletable(file: CompiledFile, cursor: Int, el: KtElement): KtElement? {
239+
val psi = file.parse.findElementAt(cursor) ?: return null
240+
val element = when (val e = psi.getPrevSiblingIgnoringWhitespace() ?: psi.parent) {
241+
is KtProperty -> e.children.lastOrNull()
242+
is KtBinaryExpression -> el
243+
else -> e
244+
}
245+
return element as? KtReferenceExpression
246+
?: element as? KtQualifiedExpression
247+
?: element as? KtConstantExpression
248+
}
249+
250+
private fun KtElement.asKtClass(): KtElement? {
251+
return this.findParent<KtImportDirective>() // import x.y.?
252+
// package x.y.?
253+
?: this.findParent<KtPackageDirective>()
254+
// :?
255+
?: this as? KtUserType
256+
?: this.parent as? KtTypeElement
257+
// .?
258+
?: this as? KtQualifiedExpression
259+
?: this.parent as? KtQualifiedExpression
260+
// something::?
261+
?: this as? KtCallableReferenceExpression
262+
?: this.parent as? KtCallableReferenceExpression
263+
// something.foo() with cursor in the method
264+
?: this.parent?.parent as? KtQualifiedExpression
265+
// ?
266+
?: this as? KtNameReferenceExpression
267+
// x ? y (infix)
268+
?: this.parent as? KtBinaryExpression
269+
// x()
270+
?: this as? KtCallExpression
271+
// x (constant)
272+
?: this as? KtConstantExpression
273+
}
274+
275+
private fun completableElement(file: CompiledFile, cursor: Int): Pair<KtElement, Boolean>? {
276+
val parsed = file.parseAtPoint(cursor - 1) ?: return null
277+
val asGlobal = isGlobalCall(parsed)
278+
val el = (
279+
if (asGlobal) asGlobalCompletable(file, cursor, parsed) else null
280+
) ?: parsed
281+
282+
return el.asKtClass()?.let {
283+
Pair(it, asGlobal)
284+
}
252285
}
253286

254-
private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement): Sequence<DeclarationDescriptor> {
287+
@Suppress("LongMethod", "ReturnCount", "CyclomaticComplexMethod")
288+
private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingElement: KtElement, infixCall: Boolean): Sequence<DeclarationDescriptor> {
255289
return when (surroundingElement) {
256290
// import x.y.?
257291
is KtImportDirective -> {
@@ -298,7 +332,8 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme
298332
// .?
299333
is KtQualifiedExpression -> {
300334
LOG.info("Completing member expression '{}'", surroundingElement.text)
301-
completeMembers(file, cursor, surroundingElement.receiverExpression, surroundingElement is KtSafeQualifiedExpression)
335+
val exp = if (infixCall) surroundingElement else surroundingElement.receiverExpression
336+
completeMembers(file, cursor, exp, surroundingElement is KtSafeQualifiedExpression)
302337
}
303338
is KtCallableReferenceExpression -> {
304339
// something::?
@@ -316,8 +351,21 @@ private fun elementCompletions(file: CompiledFile, cursor: Int, surroundingEleme
316351
// ?
317352
is KtNameReferenceExpression -> {
318353
LOG.info("Completing identifier '{}'", surroundingElement.text)
319-
val scope = file.scopeAtPoint(surroundingElement.startOffset) ?: return noResult("No scope at ${file.describePosition(cursor)}", emptySequence())
320-
identifiers(scope)
354+
if (infixCall) {
355+
completeMembers(file, surroundingElement.startOffset, surroundingElement)
356+
} else {
357+
val scope = file.scopeAtPoint(surroundingElement.startOffset) ?: return noResult("No scope at ${file.describePosition(cursor)}", emptySequence())
358+
identifiers(scope)
359+
}
360+
}
361+
// x ? y (infix)
362+
is KtBinaryExpression -> {
363+
if (surroundingElement.operationToken == KtTokens.IDENTIFIER) {
364+
completeMembers(file, cursor, surroundingElement.left!!)
365+
} else emptySequence()
366+
}
367+
is KtCallExpression, is KtConstantExpression -> {
368+
completeMembers(file, cursor, surroundingElement as KtExpression)
321369
}
322370
else -> {
323371
LOG.info("{} {} didn't look like a type, a member, or an identifier", surroundingElement::class.simpleName, surroundingElement.text)
@@ -370,6 +418,11 @@ private fun ClassDescriptor.getDescriptors(): Sequence<DeclarationDescriptor> {
370418

371419
}
372420

421+
private fun declarationIsInfix(declaration: DeclarationDescriptor): Boolean {
422+
val functionDescriptor = declaration as? FunctionDescriptor ?: return false
423+
return functionDescriptor.isInfix
424+
}
425+
373426
private fun isCompanionOfEnum(kotlinType: KotlinType): Boolean {
374427
val classDescriptor = TypeUtils.getClassDescriptor(kotlinType)
375428
val isCompanion = DescriptorUtils.isCompanionObject(classDescriptor)

server/src/test/kotlin/org/javacs/kt/CompletionsTest.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,48 @@ class InstanceMemberTest : SingleFileTestFixture("completions", "InstanceMember.
6565
}
6666
}
6767

68+
class InfixMethodTest : SingleFileTestFixture("completions", "InfixFunctions.kt") {
69+
@Test fun `complete member function`() {
70+
val completions = languageServer.textDocumentService.completion(completionParams(file, 10, 11)).get().right!!
71+
val labels = completions.items.map { it.label }
72+
73+
assertThat(labels, hasItem(startsWith("cmpB")))
74+
assertThat(labels, not(hasItem(startsWith("cmpA"))))
75+
}
76+
77+
@Test fun `includes completion for stdlib`() {
78+
val completions = languageServer.textDocumentService.completion(completionParams(file, 15, 10)).get().right!!
79+
val labels = completions.items.map { it.label }
80+
81+
assertThat(labels, hasSize(2))
82+
assertThat(labels, hasItem(startsWith("and")))
83+
assertThat(labels, hasItem("andTo"))
84+
}
85+
86+
@Test fun `complete extension function`() {
87+
val completions = languageServer.textDocumentService.completion(completionParams(file, 19, 9)).get().right!!
88+
val labels = completions.items.map { it.label }
89+
90+
assertThat(labels, hasItem("funcA"))
91+
}
92+
93+
@Test fun `complete global scope`() {
94+
val completions = languageServer.textDocumentService.completion(completionParams(file, 26, 11)).get().right!!
95+
val labels = completions.items.map { it.label }
96+
97+
assertThat(labels, hasSize(3))
98+
assertThat(labels, hasItem("ord"))
99+
}
100+
101+
@Test fun `has global completions for binary expression`() {
102+
val completions = languageServer.textDocumentService.completion(completionParams(file, 30, 7)).get().right!!
103+
val labels = completions.items.map { it.label }
104+
105+
assertThat(labels, hasItem("funcA"))
106+
assertThat(labels.filter { it.startsWith("and") }, hasSize(2))
107+
}
108+
}
109+
68110
class InstanceMembersJava : SingleFileTestFixture("completions", "InstanceMembersJava.kt") {
69111
@Test fun `convert getFileName to fileName`() {
70112
val completions = languageServer.textDocumentService.completion(completionParams(file, 4, 14)).get().right!!
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class Q {
2+
fun cmpA(): Double = 1.0
3+
infix fun cmpB(x: Int): Int { return 1 }
4+
}
5+
6+
infix fun Int.funcA(x: Int): Boolean = x == this
7+
infix fun Int.andTo(v: Int) = v
8+
9+
private fun memberFunc() {
10+
Q() cm
11+
}
12+
13+
private fun stdlibFunc() {
14+
val v = 1
15+
v and
16+
}
17+
18+
private fun extensionFunc() {
19+
2 fu 3
20+
}
21+
22+
enum class FOO { T, U }
23+
infix fun FOO.ord(n: Int) = this.ordinal == n
24+
25+
private fun globalEnumFunc() {
26+
FOO.U
27+
}
28+
29+
private fun globalBinaryExpFunc() {
30+
4 4
31+
}
32+

0 commit comments

Comments
 (0)