Skip to content

Commit 5f4917b

Browse files
authored
Merge branch 'stage' into ADFA-2246
2 parents 518465c + b01edf0 commit 5f4917b

6 files changed

Lines changed: 88 additions & 59 deletions

File tree

‎.github/workflows/debug.yml‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,6 @@ jobs:
134134
run: |
135135
flox activate -d flox/base -- ./gradlew :plugin-api:apiCheck --no-daemon
136136
137-
- name: Clean build cache before assembly
138-
run: |
139-
echo "Cleaning stale build artifacts..."
140-
rm -rf app/build/
141-
echo "Build cache cleaned successfully"
142-
143137
- name: Assemble Universal APK
144138
run: |
145139
echo "gradle_time_start=$(date +%s)" >> $GITHUB_ENV
@@ -383,11 +377,14 @@ jobs:
383377
exit 1
384378
fi
385379
386-
- name: Clean up build folder after upload
380+
- name: Clean up APK output after upload
387381
run: |
388-
echo "Cleaning up build folder after Firebase upload..."
389-
rm -rf app/build/
390-
echo "Build folder cleanup completed"
382+
echo "Removing built APK(s) after Firebase upload..."
383+
# Only remove the packaged APKs (prevents 'Find APK file' from picking a
384+
# stale APK on the next run). Compiled intermediates are preserved so the
385+
# next build compiles :app incrementally instead of from scratch.
386+
rm -rf app/build/outputs/apk/
387+
echo "APK output cleanup completed"
391388
392389
- name: Send Rich Slack Notification
393390
env:

‎app/build.gradle.kts‎

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import org.json.JSONObject
99
import java.io.BufferedInputStream
1010
import java.io.BufferedOutputStream
1111
import java.io.FileInputStream
12-
import java.io.FileNotFoundException
1312
import java.io.FileOutputStream
1413
import java.net.HttpURLConnection
1514
import java.net.URI
@@ -27,39 +26,6 @@ import java.util.zip.ZipInputStream
2726
import java.util.zip.ZipOutputStream
2827
import kotlin.reflect.jvm.javaMethod
2928

30-
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
31-
32-
fun TaskContainer.registerD8Task(
33-
taskName: String,
34-
inputJar: File,
35-
outputDex: File,
36-
): org.gradle.api.tasks.TaskProvider<Exec> {
37-
val androidSdkDir = android.sdkDirectory.absolutePath
38-
val buildToolsVersion = android.buildToolsVersion // Gets the version from your project
39-
val d8Executable =
40-
File(
41-
"$androidSdkDir/build-tools/$buildToolsVersion/" +
42-
if (isWindows) "d8.bat" else "d8",
43-
)
44-
45-
if (!d8Executable.exists()) {
46-
throw FileNotFoundException("D8 executable not found at: ${d8Executable.absolutePath}")
47-
}
48-
49-
return register<Exec>(taskName) {
50-
inputs.file(inputJar)
51-
outputs.file(outputDex)
52-
53-
commandLine(
54-
d8Executable.absolutePath,
55-
"--release", // Enables optimizations
56-
"--output",
57-
outputDex.parent, // D8 outputs to a directory
58-
inputJar.absolutePath,
59-
)
60-
}
61-
}
62-
6329
plugins {
6430
id("com.android.application")
6531
id("kotlin-android")

‎lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class ScopeCompletionProvider(
6666
val trees = Trees.instance(task.task)
6767
val list: MutableList<CompletionItem> = ArrayList()
6868
val scope = trees.getScope(path)
69+
val matchLevels = HashMap<String, MatchLevel>()
6970
val filter =
7071
Predicate<CharSequence?> {
7172
if (it == null || it.isEmpty()) {
@@ -77,7 +78,9 @@ class ScopeCompletionProvider(
7778
name = it.substring(0, it.lastIndexOf('('))
7879
}
7980

80-
return@Predicate matchLevel(name, partial) != NO_MATCH
81+
val level = matchLevel(name, partial)
82+
matchLevels[name.toString()] = level
83+
return@Predicate level != NO_MATCH
8184
}
8285

8386
abortCompletionIfCancelled()
@@ -87,7 +90,7 @@ class ScopeCompletionProvider(
8790
name = name.substring(0, name.lastIndexOf('('))
8891
}
8992

90-
val matchLevel = matchLevel(name, partial)
93+
val matchLevel = matchLevels.getOrDefault(name, NO_MATCH)
9194

9295
if (member.kind == METHOD) {
9396
val method = member as ExecutableElement

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.itsaky.androidide.lsp.models.CompletionItemKind
1717
import com.itsaky.androidide.lsp.models.CompletionParams
1818
import com.itsaky.androidide.lsp.models.CompletionResult
1919
import com.itsaky.androidide.lsp.models.InsertTextFormat
20+
import com.itsaky.androidide.lsp.models.MatchLevel
2021
import com.itsaky.androidide.preferences.utils.indentationString
2122
import com.itsaky.androidide.progress.ICancelChecker
2223
import 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+
757773
context(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

763781
private fun determineCompletionContext(element: PsiElement): CompletionContext {
764782
// Walk up to find a qualified expression where we're the selector

‎lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.itsaky.androidide.lsp.kotlin.utils
33
import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment
44
import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext
55
import com.itsaky.androidide.lsp.kotlin.completion.DeclarationKind
6+
import com.itsaky.androidide.lsp.models.MatchLevel
67
import org.jetbrains.kotlin.analysis.api.KaSession
78
import org.jetbrains.kotlin.analysis.api.components.KaScopeContext
89
import org.jetbrains.kotlin.analysis.api.scopes.KaScope
@@ -45,7 +46,10 @@ internal data class AnalysisContext(
4546
val existingModifiers: Set<KtModifierKeywordToken>,
4647
val isInsideModifierList: Boolean,
4748
val partial: String,
48-
)
49+
) {
50+
/** Per-request memo of match level by candidate name. Not part of data-class identity. */
51+
val matchLevelCache: MutableMap<String, MatchLevel> = HashMap()
52+
}
4953

5054
/**
5155
* Resolves [AnalysisContext] at the given offset in the given [KtFile].
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.itsaky.androidide.lsp.kotlin.completion
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import com.itsaky.androidide.lsp.models.MatchLevel
5+
import org.junit.Test
6+
import org.junit.runner.RunWith
7+
import org.junit.runners.JUnit4
8+
9+
@RunWith(JUnit4::class)
10+
class MatchLevelMemoTest {
11+
12+
@Test
13+
fun `includes fuzzy non-prefix matches`() {
14+
// "toStirng" is a transposition of "toString" — not a prefix, but a
15+
// high fuzzy ratio (> 59), so Java admits it and K2 must too.
16+
val level = memoizedMatchLevel(HashMap(), name = "toString", partial = "toStirng")
17+
assertThat(level).isEqualTo(MatchLevel.PARTIAL_MATCH)
18+
}
19+
20+
@Test
21+
fun `keeps prefix matches in their tier`() {
22+
val level = memoizedMatchLevel(HashMap(), name = "toString", partial = "toS")
23+
assertThat(level).isEqualTo(MatchLevel.CASE_SENSITIVE_PREFIX)
24+
}
25+
26+
@Test
27+
fun `excludes candidates below the fuzzy threshold`() {
28+
val level = memoizedMatchLevel(HashMap(), name = "toString", partial = "xyzw")
29+
assertThat(level).isEqualTo(MatchLevel.NO_MATCH)
30+
}
31+
32+
@Test
33+
fun `computes match level once per name and reuses the cache`() {
34+
// Pre-seed a deliberately wrong value. A recompute would return
35+
// CASE_SENSITIVE_EQUAL; getting NO_MATCH back proves the cache was reused.
36+
val cache = HashMap<String, MatchLevel>().apply { put("toString", MatchLevel.NO_MATCH) }
37+
val level = memoizedMatchLevel(cache, name = "toString", partial = "toString")
38+
assertThat(level).isEqualTo(MatchLevel.NO_MATCH)
39+
assertThat(cache).hasSize(1)
40+
}
41+
}

0 commit comments

Comments
 (0)