@@ -17,6 +17,7 @@ import com.itsaky.androidide.lsp.models.CompletionItemKind
1717import com.itsaky.androidide.lsp.models.CompletionParams
1818import com.itsaky.androidide.lsp.models.CompletionResult
1919import com.itsaky.androidide.lsp.models.InsertTextFormat
20+ import com.itsaky.androidide.lsp.models.MatchLevel
2021import com.itsaky.androidide.preferences.utils.indentationString
2122import com.itsaky.androidide.progress.ICancelChecker
2223import com.itsaky.androidide.progress.ProgressManager
@@ -241,11 +242,11 @@ private fun KaSession.collectMembersFromType(
241242 val typeScope = receiverType.scope
242243 if (typeScope != null ) {
243244 val callables =
244- typeScope.getCallableSignatures { name -> matchesPrefix (name) }
245+ typeScope.getCallableSignatures { name -> matchesFilter (name) }
245246 .map { it.symbol }
246247
247248 val classifiers =
248- typeScope.getClassifierSymbols { name -> matchesPrefix (name) }
249+ typeScope.getClassifierSymbols { name -> matchesFilter (name) }
249250
250251 to + = toCompletionItems(callables)
251252 to + = toCompletionItems(classifiers)
@@ -257,8 +258,8 @@ private fun KaSession.collectMembersFromType(
257258 val classSymbol = classType.symbol as ? KaClassSymbol ? : return
258259 val memberScope = classSymbol.memberScope
259260
260- val callables = memberScope.callables { name -> matchesPrefix (name) }
261- val classifiers = memberScope.classifiers { name -> matchesPrefix (name) }
261+ val callables = memberScope.callables { name -> matchesFilter (name) }
262+ val classifiers = memberScope.classifiers { name -> matchesFilter (name) }
262263
263264 to + = toCompletionItems(callables)
264265 to + = toCompletionItems(classifiers)
@@ -270,7 +271,7 @@ private fun KaSession.collectExtensionFunctions(
270271 to : MutableList <CompletionItem >
271272) {
272273 val extensionSymbols =
273- ctx.scope.callables { name -> matchesPrefix (name) }
274+ ctx.scope.callables { name -> matchesFilter (name) }
274275 .filter { symbol ->
275276 if (! symbol.isExtension) return @filter false
276277
@@ -303,7 +304,7 @@ private fun KaSession.collectScopeCompletions(
303304 )
304305
305306 val callables =
306- scope.callables { name -> matchesPrefix (name) }
307+ scope.callables { name -> matchesFilter (name) }
307308 .filter { symbol ->
308309
309310 abortIfCancelled()
@@ -318,7 +319,7 @@ private fun KaSession.collectScopeCompletions(
318319 }
319320 }
320321
321- val classifiers = scope.classifiers { name -> matchesPrefix (name) }
322+ val classifiers = scope.classifiers { name -> matchesFilter (name) }
322323
323324 to + = toCompletionItems(callables)
324325 to + = toCompletionItems(classifiers)
@@ -685,7 +686,7 @@ private fun KaSession.ktCompletionItem(
685686 val item = KotlinCompletionItem ()
686687 item.ideLabel = name
687688 item.completionKind = kind
688- item.matchLevel = CompletionItem .matchLevel(item.ideLabel, ctx.partial )
689+ item.matchLevel = matchLevelFor(name )
689690
690691 return item
691692}
@@ -754,11 +755,28 @@ private fun partialIdentifier(prefix: String): String {
754755 return prefix.takeLastWhile { char -> Character .isJavaIdentifierPart(char) }
755756}
756757
758+ /* *
759+ * Returns the [MatchLevel] of [name] against [partial], memoized in [cache].
760+ *
761+ * Match level depends only on (name, partial), so memoizing by name is safe even
762+ * when multiple symbols share a name. This is the single place match level is
763+ * computed for a completion request; both the inclusion predicate and item
764+ * creation route through it so [CompletionItem.matchLevel] runs at most once per
765+ * distinct candidate name.
766+ */
767+ internal fun memoizedMatchLevel (
768+ cache : MutableMap <String , MatchLevel >,
769+ name : String ,
770+ partial : String ,
771+ ): MatchLevel = cache.getOrPut(name) { CompletionItem .matchLevel(name, partial) }
772+
757773context(ctx: AnalysisContext )
758- private fun matchesPrefix (name : Name ): Boolean {
759- if (ctx.partial.isEmpty()) return true
760- return name.asString().startsWith(ctx.partial, ignoreCase = true )
761- }
774+ private fun matchLevelFor (name : String ): MatchLevel =
775+ memoizedMatchLevel(ctx.matchLevelCache, name, ctx.partial)
776+
777+ context(ctx: AnalysisContext )
778+ private fun matchesFilter (name : Name ): Boolean =
779+ matchLevelFor(name.asString()) != MatchLevel .NO_MATCH
762780
763781private fun determineCompletionContext (element : PsiElement ): CompletionContext {
764782 // Walk up to find a qualified expression where we're the selector
0 commit comments