Skip to content

Commit d64bc64

Browse files
committed
refactor: autocomplete
1 parent cf487bc commit d64bc64

File tree

7 files changed

+190
-240
lines changed

7 files changed

+190
-240
lines changed

app/src/main/kotlin/org/kotlinlsp/actions/Autocomplete.kt

Lines changed: 0 additions & 77 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.kotlinlsp.actions.autocomplete
2+
3+
import com.intellij.psi.util.leavesAroundOffset
4+
import com.intellij.psi.util.parentOfType
5+
import org.eclipse.lsp4j.CompletionItem
6+
import org.jetbrains.kotlin.psi.*
7+
import org.kotlinlsp.index.Index
8+
9+
fun autocompleteAction(ktFile: KtFile, offset: Int, index: Index): List<CompletionItem> {
10+
val leaf = ktFile.leavesAroundOffset(offset).asSequence()
11+
.toList().last().first
12+
13+
val prefix = leaf.text.substring(0, offset - leaf.textRange.startOffset)
14+
val completingElement = leaf.parentOfType<KtElement>() ?: ktFile
15+
16+
if (completingElement is KtNameReferenceExpression) {
17+
if (completingElement.parent is KtDotQualifiedExpression) {
18+
return autoCompletionDotExpression(ktFile, offset, index, completingElement.parent as KtDotQualifiedExpression, prefix)
19+
} else {
20+
return autoCompletionGeneric(ktFile, offset, index, completingElement, prefix)
21+
}
22+
}
23+
24+
if (completingElement is KtValueArgumentList) {
25+
return emptyList() // TODO: function call arguments
26+
}
27+
28+
return autoCompletionGeneric(ktFile, offset, index, completingElement, prefix)
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.kotlinlsp.actions.autocomplete
2+
3+
import org.eclipse.lsp4j.CompletionItemKind
4+
import org.eclipse.lsp4j.CompletionItemLabelDetails
5+
import org.eclipse.lsp4j.InsertTextFormat
6+
import org.kotlinlsp.index.db.Declaration
7+
8+
fun Declaration.completionKind(): CompletionItemKind =
9+
when (this) {
10+
is Declaration.EnumEntry -> CompletionItemKind.EnumMember
11+
is Declaration.Class -> when (type) {
12+
Declaration.Class.Type.CLASS -> CompletionItemKind.Class
13+
Declaration.Class.Type.ABSTRACT_CLASS -> CompletionItemKind.Class
14+
Declaration.Class.Type.INTERFACE -> CompletionItemKind.Interface
15+
Declaration.Class.Type.ENUM_CLASS -> CompletionItemKind.Enum
16+
Declaration.Class.Type.OBJECT -> CompletionItemKind.Module
17+
Declaration.Class.Type.ANNOTATION_CLASS -> CompletionItemKind.Interface
18+
}
19+
is Declaration.Function -> CompletionItemKind.Function
20+
is Declaration.Field -> CompletionItemKind.Field
21+
}
22+
23+
fun Declaration.details(): CompletionItemLabelDetails = when (this) {
24+
is Declaration.Class -> CompletionItemLabelDetails().apply {
25+
detail = " (${fqName})"
26+
}
27+
is Declaration.EnumEntry -> CompletionItemLabelDetails().apply {
28+
detail = ": $enumFqName"
29+
}
30+
is Declaration.Function -> CompletionItemLabelDetails().apply {
31+
detail = "(${parameters.joinToString(", ") { param -> "${param.name}: ${param.type}" }}): $returnType (${fqName})"
32+
}
33+
is Declaration.Field -> CompletionItemLabelDetails().apply {
34+
detail = ": $type (${fqName})"
35+
}
36+
}
37+
38+
fun Declaration.insertInfo(): Pair<String, InsertTextFormat> = when (this) {
39+
is Declaration.Class -> name to InsertTextFormat.PlainText
40+
is Declaration.EnumEntry -> "${enumFqName.substringAfterLast('.')}.${name}" to InsertTextFormat.PlainText
41+
is Declaration.Function -> "${name}(${
42+
parameters.mapIndexed { index, param -> "\${${index + 1}:${param.name}}" }.joinToString(", ")
43+
})" to InsertTextFormat.Snippet
44+
45+
is Declaration.Field -> name to InsertTextFormat.PlainText
46+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.kotlinlsp.actions.autocomplete
2+
3+
import com.intellij.openapi.util.text.StringUtil
4+
import org.eclipse.lsp4j.CompletionItem
5+
import org.eclipse.lsp4j.Position
6+
import org.eclipse.lsp4j.TextEdit
7+
import org.jetbrains.kotlin.analysis.api.analyze
8+
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
9+
import org.jetbrains.kotlin.psi.KtFile
10+
import org.jetbrains.kotlin.psi.KtImportDirective
11+
import org.kotlinlsp.index.Index
12+
import org.kotlinlsp.index.queries.getCompletions
13+
14+
fun autoCompletionDotExpression(
15+
ktFile: KtFile,
16+
offset: Int,
17+
index: Index,
18+
completingElement: KtDotQualifiedExpression,
19+
prefix: String
20+
): List<CompletionItem> {
21+
val receiverType = analyze(completingElement) {
22+
completingElement.receiverExpression.expressionType.toString()
23+
}
24+
val existingImports = ktFile.importList?.children?.filterIsInstance<KtImportDirective>() ?: emptyList()
25+
val (importInsertionOffset, newlineCount) = if (existingImports.isEmpty()) {
26+
ktFile.packageDirective?.textRange?.let { it.endOffset to 2 } ?: (ktFile.textRange.startOffset to 0)
27+
} else {
28+
existingImports.last().textRange.endOffset to 1
29+
}
30+
val importInsertionPosition =
31+
StringUtil.offsetToLineColumn(ktFile.text, importInsertionOffset).let { Position(it.line, it.column) }
32+
33+
return index
34+
.getCompletions(prefix, "", receiverType) // TODO: ThisRef
35+
.map { decl ->
36+
val (inserted, insertionType) = decl.insertInfo()
37+
38+
CompletionItem().apply {
39+
label = decl.name
40+
labelDetails = decl.details()
41+
kind = decl.completionKind()
42+
insertText = inserted
43+
insertTextFormat = insertionType
44+
additionalTextEdits = emptyList()
45+
}
46+
}
47+
.toList()
48+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package org.kotlinlsp.actions.completions
1+
package org.kotlinlsp.actions.autocomplete
22

33
import com.intellij.openapi.util.text.StringUtil
44
import com.intellij.psi.util.parentOfType
@@ -25,26 +25,84 @@ import org.jetbrains.kotlin.psi.KtLoopExpression
2525
import org.jetbrains.kotlin.psi.KtParameter
2626
import org.jetbrains.kotlin.psi.KtProperty
2727
import org.jetbrains.kotlin.types.Variance
28-
import org.kotlinlsp.actions.completionKind
2928
import org.kotlinlsp.index.Index
3029
import org.kotlinlsp.index.db.Declaration
3130
import org.kotlinlsp.index.queries.getCompletions
3231

3332
private val newlines = arrayOf("", "\n", "\n\n")
34-
@OptIn(KaExperimentalApi::class)
33+
3534
fun autoCompletionGeneric(ktFile: KtFile, offset: Int, index: Index, completingElement: KtElement, prefix: String): List<CompletionItem> {
36-
val localVariableCompletions: List<CompletionItem> = analyze(completingElement) {
37-
ktFile.scopeContext(completingElement).scopes.flatMap {
38-
if (it.kind !is KaScopeKind.LocalScope) return@flatMap emptyList()
35+
val localVariableCompletions = fetchLocalCompletions(ktFile, offset, completingElement, prefix)
36+
37+
val existingImports = ktFile.importList?.children?.filterIsInstance<KtImportDirective>() ?: emptyList()
38+
val (importInsertionOffset, newlineCount) = if (existingImports.isEmpty()) {
39+
ktFile.packageDirective?.textRange?.let { it.endOffset to 2 } ?: (ktFile.textRange.startOffset to 0)
40+
} else {
41+
existingImports.last().textRange.endOffset to 1
42+
}
43+
val importInsertionPosition =
44+
StringUtil.offsetToLineColumn(ktFile.text, importInsertionOffset).let { Position(it.line, it.column) }
45+
46+
val completions = index
47+
.getCompletions(prefix, "", "") // TODO: ThisRef
48+
.map { decl ->
49+
val additionalEdits = mutableListOf<TextEdit>()
50+
51+
if (decl is Declaration.Class) {
52+
val exists = existingImports.any {
53+
it.importedFqName?.asString() == decl.fqName
54+
}
55+
if (!exists) {
56+
val importText = "import ${decl.fqName}"
57+
val edit = TextEdit().apply {
58+
range = Range(importInsertionPosition, importInsertionPosition)
59+
newText = "${newlines[newlineCount]}$importText"
60+
}
61+
additionalEdits.add(edit)
62+
}
63+
}
64+
65+
val (inserted, insertionType) = decl.insertInfo()
66+
67+
CompletionItem().apply {
68+
label = decl.name
69+
labelDetails = decl.details()
70+
kind = decl.completionKind()
71+
insertText = inserted
72+
insertTextFormat = insertionType
73+
additionalTextEdits = additionalEdits
74+
}
75+
}
76+
77+
return localVariableCompletions + completions
78+
}
79+
80+
@OptIn(KaExperimentalApi::class)
81+
private fun fetchLocalCompletions(
82+
ktFile: KtFile,
83+
offset: Int,
84+
completingElement: KtElement,
85+
prefix: String
86+
): List<CompletionItem> = analyze(completingElement) {
87+
ktFile
88+
.scopeContext(completingElement)
89+
.scopes
90+
.filter { it.kind is KaScopeKind.LocalScope }
91+
.flatMap {
3992
it.scope.declarations.mapNotNull { decl ->
4093
if (!decl.name.toString().startsWith(prefix)) return@mapNotNull null
4194
val psi = decl.psi ?: return@mapNotNull null
95+
4296
// TODO: This is a hack to get the correct offset for function literals, can analysis tell us if a declaration is accessible?
4397
val declOffset = if (psi is KtFunctionLiteral) psi.textRange.startOffset else psi.textRange.endOffset
4498
if (declOffset >= offset) return@mapNotNull null
4599

46100
val detail = when (decl) {
47-
is KaVariableSymbol -> decl.returnType.render(KaTypeRendererForSource.WITH_SHORT_NAMES, Variance.INVARIANT)
101+
is KaVariableSymbol -> decl.returnType.render(
102+
KaTypeRendererForSource.WITH_SHORT_NAMES,
103+
Variance.INVARIANT
104+
)
105+
48106
else -> "Missing ${decl.javaClass.simpleName}"
49107
}
50108

@@ -56,6 +114,7 @@ fun autoCompletionGeneric(ktFile: KtFile, offset: Int, index: Index, completingE
56114
loop.text.replace(loop.body!!.text, "")
57115
} else psi.text
58116
}
117+
59118
is KtFunctionLiteral -> decl.name // TODO: Show the function call containing the lambda?
60119
else -> "TODO: Preview for ${psi.javaClass.simpleName}"
61120
}
@@ -75,66 +134,4 @@ fun autoCompletionGeneric(ktFile: KtFile, offset: Int, index: Index, completingE
75134
}
76135
}.toList()
77136
}
78-
}
79-
80-
val existingImports = ktFile.importList?.children?.filterIsInstance<KtImportDirective>() ?: emptyList()
81-
val (importInsertionOffset, newlineCount) = if (existingImports.isEmpty()) {
82-
ktFile.packageDirective?.textRange?.let { it.endOffset to 2 } ?: (ktFile.textRange.startOffset to 0)
83-
} else {
84-
existingImports.last().textRange.endOffset to 1
85-
}
86-
val importInsertionPosition = StringUtil.offsetToLineColumn(ktFile.text, importInsertionOffset).let { Position(it.line, it.column) }
87-
88-
val completions = index.getCompletions(prefix, "", "") // TODO: ThisRef
89-
.mapNotNull { decl ->
90-
val additionalEdits = mutableListOf<TextEdit>()
91-
92-
if (decl is Declaration.Class) {
93-
val exists = existingImports.any {
94-
it.importedFqName?.asString() == decl.fqName
95-
}
96-
if (!exists) {
97-
val importText = "import ${decl.fqName}"
98-
val edit = TextEdit().apply {
99-
range = Range(importInsertionPosition, importInsertionPosition)
100-
newText = "${newlines[newlineCount]}$importText"
101-
}
102-
additionalEdits.add(edit)
103-
}
104-
}
105-
106-
val detail = when (decl) {
107-
is Declaration.Class -> CompletionItemLabelDetails().apply {
108-
detail = " (${decl.fqName})"
109-
}
110-
is Declaration.EnumEntry -> CompletionItemLabelDetails().apply {
111-
detail = ": ${decl.enumFqName}"
112-
}
113-
is Declaration.Function -> CompletionItemLabelDetails().apply {
114-
detail = "(${decl.parameters.joinToString(", ") { param -> "${param.name}: ${param.type}" }}): ${decl.returnType} (${decl.fqName})"
115-
}
116-
is Declaration.Field -> CompletionItemLabelDetails().apply {
117-
detail = ": ${decl.type} (${decl.fqName})"
118-
}
119-
}
120-
121-
val (inserted, insertionType) = when (decl) {
122-
is Declaration.Class -> decl.name to InsertTextFormat.PlainText
123-
is Declaration.EnumEntry -> "${decl.enumFqName.substringAfterLast('.')}.${decl.name}" to InsertTextFormat.PlainText
124-
is Declaration.Function -> "${decl.name}(${
125-
decl.parameters.mapIndexed { index, param -> "\${${index+1}:${param.name}}" }.joinToString(", ")
126-
})" to InsertTextFormat.Snippet
127-
is Declaration.Field -> decl.name to InsertTextFormat.PlainText
128-
}
129-
130-
CompletionItem().apply {
131-
label = decl.name
132-
labelDetails = detail
133-
kind = decl.completionKind()
134-
insertText = inserted
135-
insertTextFormat = insertionType
136-
additionalTextEdits = additionalEdits
137-
}
138-
}
139-
return localVariableCompletions + completions
140137
}

0 commit comments

Comments
 (0)