Skip to content

Commit 4129a47

Browse files
committed
feat: suggest local and imported extension functions
Signed-off-by: Akash Yadav <akashyadav@appdevforall.org>
1 parent 2f982a7 commit 4129a47

File tree

1 file changed

+112
-32
lines changed

1 file changed

+112
-32
lines changed

lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt

Lines changed: 112 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.jetbrains.kotlin.analysis.api.analyzeCopy
1616
import org.jetbrains.kotlin.analysis.api.projectStructure.KaDanglingFileResolutionMode
1717
import org.jetbrains.kotlin.analysis.api.renderer.types.KaTypeRenderer
1818
import org.jetbrains.kotlin.analysis.api.renderer.types.impl.KaTypeRendererForSource
19+
import org.jetbrains.kotlin.analysis.api.scopes.KaScope
1920
import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol
2021
import org.jetbrains.kotlin.analysis.api.symbols.KaClassKind
2122
import org.jetbrains.kotlin.analysis.api.symbols.KaClassSymbol
@@ -31,13 +32,13 @@ import org.jetbrains.kotlin.analysis.api.symbols.KaTypeAliasSymbol
3132
import org.jetbrains.kotlin.analysis.api.symbols.KaTypeParameterSymbol
3233
import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
3334
import org.jetbrains.kotlin.analysis.api.symbols.name
35+
import org.jetbrains.kotlin.analysis.api.symbols.receiverType
3436
import org.jetbrains.kotlin.analysis.api.types.KaClassType
3537
import org.jetbrains.kotlin.analysis.api.types.KaType
3638
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
3739
import org.jetbrains.kotlin.name.Name
3840
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
3941
import org.jetbrains.kotlin.psi.KtElement
40-
import org.jetbrains.kotlin.psi.KtFile
4142
import org.jetbrains.kotlin.psi.KtQualifiedExpression
4243
import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression
4344
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
@@ -92,21 +93,48 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult
9293
resolutionMode = KaDanglingFileResolutionMode.PREFER_SELF,
9394
) {
9495
val completionContext = determineCompletionContext(elementAtOffset)
96+
97+
// Find the nearest KtElement parent for scope resolution
98+
val ktElement = elementAtOffset.getParentOfType<KtElement>(strict = false)
99+
val scopeContext = ktElement?.let { element -> completionKtFile.scopeContext(element) }
100+
val compositeScope = scopeContext?.compositeScope()
95101
val items = mutableListOf<CompletionItem>()
96102

97-
when (completionContext) {
98-
CompletionContext.Scope -> collectScopeCompletions(
99-
element = elementAtOffset,
100-
file = completionKtFile,
101-
partial = partial,
102-
to = items
103+
if (ktElement == null) {
104+
logger.error(
105+
"Cannot find parent of element {} with partial {}",
106+
elementAtOffset,
107+
partial
103108
)
104109

105-
CompletionContext.Member -> collectMemberCompletions(
106-
element = elementAtOffset,
107-
partial = partial,
108-
to = items
110+
return@analyzeCopy CompletionResult.EMPTY
111+
}
112+
113+
if (compositeScope == null) {
114+
logger.error(
115+
"Unable to get CompositeScope for element {} with partial {}",
116+
compositeScope,
117+
partial
109118
)
119+
return@analyzeCopy CompletionResult.EMPTY
120+
}
121+
122+
when (completionContext) {
123+
CompletionContext.Scope ->
124+
collectScopeCompletions(
125+
ktElement = ktElement,
126+
scope = compositeScope,
127+
partial = partial,
128+
to = items
129+
)
130+
131+
CompletionContext.Member ->
132+
collectMemberCompletions(
133+
scope = compositeScope,
134+
element = elementAtOffset,
135+
partial = partial,
136+
to = items
137+
)
110138
}
111139

112140
CompletionResult(items)
@@ -122,6 +150,7 @@ fun CompilationEnvironment.complete(params: CompletionParams): CompletionResult
122150
}
123151

124152
private fun KaSession.collectMemberCompletions(
153+
scope: KaScope,
125154
element: PsiElement,
126155
partial: String,
127156
to: MutableList<CompletionItem>
@@ -140,12 +169,22 @@ private fun KaSession.collectMemberCompletions(
140169
return
141170
}
142171

172+
logger.info(
173+
"Complete members of {}: {} [{}] matching '{}'",
174+
receiver,
175+
receiverType,
176+
receiver.text,
177+
partial
178+
)
179+
143180
collectMembersFromType(receiverType, partial, to)
144181

145182
if (qualifiedExpr is KtSafeQualifiedExpression) {
146183
val nonNullType = receiverType.withNullability(isMarkedNullable = false)
147184
collectMembersFromType(nonNullType, partial, to)
148185
}
186+
187+
collectExtensionFunctions(scope, partial, receiverType, to)
149188
}
150189

151190
@OptIn(KaExperimentalApi::class)
@@ -156,8 +195,15 @@ private fun KaSession.collectMembersFromType(
156195
) {
157196
val typeScope = receiverType.scope
158197
if (typeScope != null) {
159-
to += toCompletionItems(typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) }.map { it.symbol }, partial)
160-
to += toCompletionItems(typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) }, partial)
198+
val callables =
199+
typeScope.getCallableSignatures { name -> matchesPrefix(name, partial) }
200+
.map { it.symbol }
201+
202+
val classifiers =
203+
typeScope.getClassifierSymbols { name -> matchesPrefix(name, partial) }
204+
205+
to += toCompletionItems(callables, partial)
206+
to += toCompletionItems(classifiers, partial)
161207
return
162208
}
163209

@@ -166,22 +212,37 @@ private fun KaSession.collectMembersFromType(
166212
val classSymbol = classType.symbol as? KaClassSymbol ?: return
167213
val memberScope = classSymbol.memberScope
168214

169-
to += toCompletionItems(memberScope.callables { name -> matchesPrefix(name, partial) }, partial)
170-
to += toCompletionItems(memberScope.classifiers { name -> matchesPrefix(name, partial) }, partial)
215+
val callables = memberScope.callables { name -> matchesPrefix(name, partial) }
216+
val classifiers = memberScope.classifiers { name -> matchesPrefix(name, partial) }
217+
218+
to += toCompletionItems(callables, partial)
219+
to += toCompletionItems(classifiers, partial)
171220
}
172221

173-
private fun KaSession.collectScopeCompletions(
174-
element: PsiElement,
175-
file: KtFile,
222+
private fun KaSession.collectExtensionFunctions(
223+
scope: KaScope,
176224
partial: String,
225+
receiverType: KaType,
177226
to: MutableList<CompletionItem>
178227
) {
179-
// Find the nearest KtElement parent for scope resolution
180-
val ktElement = element.getParentOfType<KtElement>(strict = false)
181-
if (ktElement == null) {
182-
logger.error("Cannot find parent of element {} with partial {}", element, partial)
183-
return
184-
}
228+
val extensionSymbols =
229+
scope.callables { name -> matchesPrefix(name, partial) }
230+
.filter { symbol ->
231+
if (!symbol.isExtension) return@filter false
232+
233+
val extReceiverType = symbol.receiverType ?: return@filter false
234+
receiverType.isSubtypeOf(extReceiverType)
235+
}
236+
237+
to += toCompletionItems(extensionSymbols, partial)
238+
}
239+
240+
private fun KaSession.collectScopeCompletions(
241+
ktElement: KtElement,
242+
scope: KaScope,
243+
partial: String,
244+
to: MutableList<CompletionItem>,
245+
) {
185246

186247
logger.info(
187248
"Complete scope members of {}: [{}] matching '{}'",
@@ -190,21 +251,30 @@ private fun KaSession.collectScopeCompletions(
190251
partial
191252
)
192253

193-
val scopeContext = file.scopeContext(ktElement)
194-
val compositeScope = scopeContext.compositeScope()
195-
196-
to += toCompletionItems(compositeScope.callables { name -> matchesPrefix(name, partial) }, partial)
197-
to += toCompletionItems(compositeScope.classifiers { name -> matchesPrefix(name, partial) }, partial)
254+
to += toCompletionItems(
255+
scope.callables { name -> matchesPrefix(name, partial) },
256+
partial
257+
)
258+
to += toCompletionItems(
259+
scope.classifiers { name -> matchesPrefix(name, partial) },
260+
partial
261+
)
198262
}
199263

200264
@JvmName("callablesToCompletionItems")
201-
private fun KaSession.toCompletionItems(callables: Sequence<KaCallableSymbol>, partial: String): Sequence<CompletionItem> =
265+
private fun KaSession.toCompletionItems(
266+
callables: Sequence<KaCallableSymbol>,
267+
partial: String
268+
): Sequence<CompletionItem> =
202269
callables.mapNotNull {
203270
callableSymbolToCompletionItem(it, partial)
204271
}
205272

206273
@JvmName("classifiersToCompletionItems")
207-
private fun KaSession.toCompletionItems(classifiers: Sequence<KaClassifierSymbol>, partial: String): Sequence<CompletionItem> =
274+
private fun KaSession.toCompletionItems(
275+
classifiers: Sequence<KaClassifierSymbol>,
276+
partial: String
277+
): Sequence<CompletionItem> =
208278
classifiers.mapNotNull {
209279
classifierSymbolToCompletionItem(it, partial)
210280
}
@@ -284,7 +354,11 @@ private fun KaSession.classifierSymbolToCompletionItem(
284354
val item = createSymbolCompletionItem(symbol, partial) ?: return null
285355
item.detail = when (symbol) {
286356
is KaClassSymbol -> symbol.classId?.asFqNameString() ?: ""
287-
is KaTypeAliasSymbol -> renderName(symbol.expandedType, KaTypeRendererForSource.WITH_QUALIFIED_NAMES)
357+
is KaTypeAliasSymbol -> renderName(
358+
symbol.expandedType,
359+
KaTypeRendererForSource.WITH_QUALIFIED_NAMES
360+
)
361+
288362
is KaTypeParameterSymbol -> item.ideLabel
289363
}
290364
return item
@@ -347,6 +421,12 @@ private fun partialIdentifier(prefix: String): String {
347421
}
348422

349423
private fun matchesPrefix(name: Name, partial: String): Boolean {
424+
logger.info(
425+
"'{}' matches '{}': {}",
426+
name,
427+
partial,
428+
name.asString().startsWith(partial, ignoreCase = true)
429+
)
350430
if (partial.isEmpty()) return true
351431
return name.asString().startsWith(partial, ignoreCase = true)
352432
}

0 commit comments

Comments
 (0)