Skip to content

Commit 0dc2017

Browse files
Merge branch 'stage' into ADFA-5446-Extension-Manager-uninstall-broken
2 parents 68d8748 + ffaa75b commit 0dc2017

28 files changed

Lines changed: 1321 additions & 208 deletions

File tree

‎.github/workflows/debug.yml‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,15 @@ jobs:
228228
- name: Assemble Universal APK
229229
run: |
230230
echo "gradle_time_start=$(date +%s)" >> $GITHUB_ENV
231-
flox activate -d flox/base -- ./gradlew :app:assembleV8Debug --no-daemon
231+
set -o pipefail
232+
flox activate -d flox/base -- ./gradlew :app:assembleV8Debug --no-daemon 2>&1 | tee "$RUNNER_TEMP/assemble.log"
232233
echo "gradle_time_end=$(date +%s)" >> $GITHUB_ENV
234+
# D8 logs this (non-fatal) once per class when the pinned R8 is older than the Kotlin
235+
# metadata version, burying every real diagnostic. See the `r8` pin in libs.versions.toml.
236+
if grep -q "Unexpected error during rewriting of Kotlin metadata" "$RUNNER_TEMP/assemble.log"; then
237+
echo "::error::D8 could not rewrite Kotlin metadata. Most likely the R8 pinned in gradle/libs.versions.toml is older than the Kotlin metadata version (bump 'r8'), or the pin no longer takes effect on the build classpath. See ADFA-6822."
238+
exit 1
239+
fi
233240
234241
235242
- name: Find APK file

‎app/src/main/java/com/itsaky/androidide/quickbuild/QuickBuildOutputLines.kt‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,7 @@ fun quickBuildOutputLines(
8383
// of narrating upcoming work.
8484
listOf("the rebuild failed - save a fix to retry.")
8585
} else {
86-
listOf(
87-
"a full build is needed - ${describe(transition.reason)}. " +
88-
"Tap Quick Build to rebuild.",
89-
)
86+
listOf("a full build is needed - ${describe(transition.reason)}. Starting it now.")
9087
}
9188
}
9289

‎app/src/test/java/com/itsaky/androidide/quickbuild/QuickBuildOutputLinesTest.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,16 @@ class QuickBuildOutputLinesTest {
251251
}
252252

253253
@Test
254-
fun `invalidation reads as information and names a next step`() {
254+
fun `invalidation reads as information and says the full build is starting`() {
255255
val emitted =
256256
lines(
257257
QuickBuildStatus.UpToDate(4L, buildDurationMillis = null),
258258
QuickBuildStatus.NeedsFullBuild(InvalidationReason.MANIFEST_CHANGED, 4L),
259259
).joinToString("")
260260

261261
assertThat(emitted).contains("the manifest changed")
262-
assertThat(emitted).contains("Tap Quick Build")
262+
assertThat(emitted).contains("Starting it now")
263+
assertThat(emitted).doesNotContain("Tap")
263264
// Not a failure - a full build is the normal answer to an unabsorbable edit.
264265
assertThat(emitted).doesNotContain("failed")
265266
}

‎app/src/test/java/com/itsaky/androidide/quickbuild/QuickBuildStatusBarTest.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ class QuickBuildStatusBarTest {
320320
}
321321

322322
@Test
323-
fun `an invalidation names the full-build ask`() {
323+
fun `an invalidation says the full build is starting`() {
324324
val shown =
325325
update(
326326
QuickBuildStatus.UpToDate(4L, null),

‎build.gradle.kts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ jacoco {
6262

6363
buildscript {
6464
dependencies {
65+
// Must stay first: pins a D8/R8 that understands the Kotlin metadata version (ADFA-6822).
66+
classpath(libs.android.r8)
6567
classpath(libs.kotlin.gradle.plugin)
6668
classpath(libs.nav.safe.args.gradle.plugin)
6769
}

‎gradle/libs.versions.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ gson = "2.10.1"
1919
junit-jupiter = "5.10.2"
2020
anroidx-test-core = "2.2.0"
2121
koinAndroid = "4.1.1"
22+
# A Kotlin minor bump may need a newer `r8` (see its entry below, ADFA-6822).
2223
kotlin = "2.3.0"
2324
kotlin-coroutines = "1.9.0"
2425
kotlinxCoroutinesCore = "1.10.2"
@@ -37,6 +38,9 @@ ksp = "2.3.6"
3738
antlr4 = "4.13.1"
3839
androidx-work = "2.10.0"
3940
androidx-espresso = "3.5.1"
41+
# Pinned ahead of AGP on the root buildscript classpath: AGP 8.8.2's bundled D8 8.8.34 cannot rewrite
42+
# Kotlin 2.3 metadata. Bump with `kotlin`; remove once AGP is upgraded (ADFA-6837).
43+
r8 = "8.13.25"
4044
retrofit = "2.11.0"
4145
markwon = "4.6.2"
4246
maven-publish-plugin = "0.27.0"
@@ -324,6 +328,7 @@ tooling-slf4j = { module = "org.slf4j:slf4j-api", version = "2.0.17" }
324328
snakeyaml-engine = { module = "org.snakeyaml:snakeyaml-engine", version = "2.10" }
325329

326330
# Classpaths
331+
android-r8 = { module = "com.android.tools:r8", version.ref = "r8" }
327332
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
328333
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
329334
nav-safe-args-gradle-plugin = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "androidx-navigation" }

‎lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KtFileMetadataIndex.kt‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@ import org.appdevforall.codeonthego.indexing.api.indexQuery
88
import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataDescriptor.KEY_IS_INDEXED
99
import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataDescriptor.KEY_PACKAGE
1010
import java.io.Closeable
11+
import java.util.concurrent.CopyOnWriteArrayList
12+
13+
/**
14+
* A completed write to a [KtFileMetadataIndex].
15+
*/
16+
sealed interface KtFileMetadataWrite {
17+
/** The record for [filePath] was inserted or replaced; its package is [packageFqName]. */
18+
data class Upserted(
19+
val filePath: String,
20+
val packageFqName: String,
21+
) : KtFileMetadataWrite
22+
23+
/** The records for [filePaths] were removed. */
24+
data class Removed(
25+
val filePaths: Collection<String>,
26+
) : KtFileMetadataWrite
27+
28+
/** Every record was removed. */
29+
data object Cleared : KtFileMetadataWrite
30+
}
31+
32+
/**
33+
* Receives each [KtFileMetadataWrite] after it has been applied to the index.
34+
*/
35+
fun interface KtFileMetadataWriteListener {
36+
/** Called synchronously on the writing thread once [write] is visible to readers. */
37+
fun onWrite(write: KtFileMetadataWrite)
38+
}
1139

1240
/**
1341
* An index of [KtFileMetadata] entries, one per Kotlin source file.
@@ -40,20 +68,42 @@ class KtFileMetadataIndex(
4068
)
4169
}
4270

71+
private val listeners = CopyOnWriteArrayList<KtFileMetadataWriteListener>()
72+
73+
/**
74+
* Registers [listener] to be told about every later write.
75+
*
76+
* Closing the returned handle unregisters it.
77+
*/
78+
fun addWriteListener(listener: KtFileMetadataWriteListener): AutoCloseable {
79+
listeners.add(listener)
80+
return AutoCloseable { listeners.remove(listener) }
81+
}
82+
83+
private fun notifyListeners(write: KtFileMetadataWrite) {
84+
listeners.forEach { it.onWrite(write) }
85+
}
86+
4387
/**
4488
* Insert or replace the metadata record for a single file.
4589
*
4690
* Because [KtFileMetadata.key] and [KtFileMetadata.sourceId] are both the
4791
* file path, a file has at most one row and an insert replaces it.
4892
*/
49-
suspend fun upsert(metadata: KtFileMetadata) = backing.insert(metadata)
93+
suspend fun upsert(metadata: KtFileMetadata) {
94+
backing.insert(metadata)
95+
notifyListeners(KtFileMetadataWrite.Upserted(metadata.filePath, metadata.packageFqName))
96+
}
5097

5198
/**
5299
* Remove the metadata record for [filePath].
53100
*
54101
* No-op if the file is not in the index.
55102
*/
56-
suspend fun remove(filePath: String) = backing.removeBySource(filePath)
103+
suspend fun remove(filePath: String) {
104+
backing.removeBySource(filePath)
105+
notifyListeners(KtFileMetadataWrite.Removed(listOf(filePath)))
106+
}
57107

58108
/**
59109
* Remove the metadata records for every path in [filePaths] as a single
@@ -63,7 +113,10 @@ class KtFileMetadataIndex(
63113
* transaction instead of N - see [Index.removeBySources]. Paths not present in
64114
* the index are ignored.
65115
*/
66-
suspend fun removeAll(filePaths: Collection<String>) = backing.removeBySources(filePaths)
116+
suspend fun removeAll(filePaths: Collection<String>) {
117+
backing.removeBySources(filePaths)
118+
notifyListeners(KtFileMetadataWrite.Removed(filePaths))
119+
}
67120

68121
/**
69122
* Return the [KtFileMetadata] for [filePath], or `null` if the file is
@@ -154,7 +207,10 @@ class KtFileMetadataIndex(
154207
).map { it.filePath }
155208

156209
/** Remove all records from the index. */
157-
suspend fun clear() = backing.clear()
210+
suspend fun clear() {
211+
backing.clear()
212+
notifyListeners(KtFileMetadataWrite.Cleared)
213+
}
158214

159215
/** Whether the backing index still serves reads and writes. */
160216
val isOpen: Boolean
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package org.appdevforall.codeonthego.indexing.jvm
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import kotlinx.coroutines.test.runTest
5+
import org.appdevforall.codeonthego.indexing.InMemoryIndex
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
import org.junit.runners.JUnit4
9+
import java.time.Instant
10+
11+
@RunWith(JUnit4::class)
12+
class KtFileMetadataIndexWriteListenerTest {
13+
private val index = KtFileMetadataIndex(InMemoryIndex(KtFileMetadataDescriptor))
14+
private val writes = mutableListOf<KtFileMetadataWrite>()
15+
16+
private fun metadata(
17+
path: String,
18+
pkg: String,
19+
) = KtFileMetadata(path, pkg, Instant.EPOCH, 1L)
20+
21+
@Test
22+
fun upsertReportsPathAndPackage() =
23+
runTest {
24+
index.addWriteListener { writes += it }
25+
26+
index.upsert(metadata("/a/A.kt", "com.a"))
27+
28+
assertThat(writes).containsExactly(KtFileMetadataWrite.Upserted("/a/A.kt", "com.a"))
29+
}
30+
31+
@Test
32+
fun removeAndRemoveAllReportPaths() =
33+
runTest {
34+
index.addWriteListener { writes += it }
35+
36+
index.remove("/a/A.kt")
37+
index.removeAll(listOf("/a/B.kt", "/a/C.kt"))
38+
39+
assertThat(writes)
40+
.containsExactly(
41+
KtFileMetadataWrite.Removed(listOf("/a/A.kt")),
42+
KtFileMetadataWrite.Removed(listOf("/a/B.kt", "/a/C.kt")),
43+
).inOrder()
44+
}
45+
46+
@Test
47+
fun clearReportsCleared() =
48+
runTest {
49+
index.addWriteListener { writes += it }
50+
51+
index.clear()
52+
53+
assertThat(writes).containsExactly(KtFileMetadataWrite.Cleared)
54+
}
55+
56+
@Test
57+
fun listenerSeesTheWriteAlreadyApplied() =
58+
runTest {
59+
var pathsSeenInListener: List<String>? = null
60+
index.addWriteListener {
61+
pathsSeenInListener = index.getFilePathsForPackage("com.a").toList()
62+
}
63+
64+
index.upsert(metadata("/a/A.kt", "com.a"))
65+
66+
assertThat(pathsSeenInListener).containsExactly("/a/A.kt")
67+
}
68+
69+
@Test
70+
fun closedRegistrationIsNotCalled() =
71+
runTest {
72+
index.addWriteListener { writes += it }.close()
73+
74+
index.upsert(metadata("/a/A.kt", "com.a"))
75+
76+
assertThat(writes).isEmpty()
77+
}
78+
}

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -475,12 +475,13 @@ class KotlinLanguageServer : ILanguageServer {
475475
@Suppress("unused")
476476
fun onFileDeleted(event: FileDeletionEvent) {
477477
val path = event.file.toPath()
478-
if (!DocumentUtils.isKotlinFile(path)) {
478+
// The file is already gone, so only its extension can say it was Kotlin.
479+
if (!DocumentUtils.hasKotlinExtension(path)) {
479480
return
480481
}
481482

482483
scope.launch {
483-
runCatching { compiler?.compilationEnvironmentFor(path) }
484+
runCatching { compiler?.compilationEnvironmentForKotlinPath(path) }
484485
.getOrNull()
485486
?.onFileRemoved(path)
486487
}
@@ -495,7 +496,8 @@ class KotlinLanguageServer : ILanguageServer {
495496
scope.launch {
496497
// One read: a rebuild can swap the compiler between two reads of the property.
497498
val compiler = compiler ?: return@launch
498-
val oldIsKotlinFile = DocumentUtils.isKotlinFile(fromPath)
499+
// The old path no longer exists, so only its extension can say it was Kotlin.
500+
val oldIsKotlinFile = DocumentUtils.hasKotlinExtension(fromPath)
499501
val newIsKotlinFile = DocumentUtils.isKotlinFile(toPath)
500502

501503
if (!oldIsKotlinFile && newIsKotlinFile) {
@@ -511,17 +513,16 @@ class KotlinLanguageServer : ILanguageServer {
511513
// only the old file was a Kotlin file
512514
// so just remove it from the index
513515
compiler
514-
.compilationEnvironmentFor(fromPath)
516+
.compilationEnvironmentForKotlinPath(fromPath)
515517
?.onFileRemoved(fromPath)
516518
return@launch
517519
}
518520

519-
val fromKind = runCatching { compiler.compilationKindFor(fromPath) }.getOrNull()
520-
val toKind = runCatching { compiler.compilationKindFor(toPath) }.getOrNull()
521-
val fromEnv = fromKind?.let { compiler.compilationEnvironmentFor(it) }
522-
val toEnv = toKind?.let { compiler.compilationEnvironmentFor(it) }
521+
// Path-based, so a .kts side yields no environment instead of the kind-based overload's throw.
522+
val fromEnv = runCatching { compiler.compilationEnvironmentForKotlinPath(fromPath) }.getOrNull()
523+
val toEnv = runCatching { compiler.compilationEnvironmentForKotlinPath(toPath) }.getOrNull()
523524

524-
if (fromKind != null && fromEnv == toEnv && toEnv != null) {
525+
if (fromEnv != null && fromEnv == toEnv) {
525526
// file was renamed within the same compilation environment
526527
toEnv.onFileMoved(fromPath, toPath)
527528
return@launch

0 commit comments

Comments
 (0)