From 0788740c6a65159122644a5807c7ff726093934e Mon Sep 17 00:00:00 2001 From: David Schachter Date: Tue, 4 Aug 2026 16:03:04 -0700 Subject: [PATCH 01/11] ADFA-5010: Decouple lsp/jvm-symbol-index from kotlin-analysis-api Part 1 of moving the embedded Kotlin Analysis API off the main DEX (ADFA-5010). This module only needed kotlin-analysis-api for a VirtualFile-taking scan() overload that no call site in the repo actually uses (real callers use the JarFile-based overload) and for bytecode parsing via the jar's internally shaded ASM classes (org.jetbrains.org.objectweb.asm.*). Delete the dead VirtualFile overloads and switch the ASM usage to a real org.ow2.asm:asm dependency instead of relying on kotlin-analysis-api's internal shaded copy, so this module -- shared by both the Java and Kotlin LSPs -- no longer needs the 27MB compiler jar on its classpath at all. Co-Authored-By: Claude Sonnet 5 --- gradle/libs.versions.toml | 2 ++ lsp/jvm-symbol-index/build.gradle.kts | 2 +- .../indexing/jvm/CombinedJarScanner.kt | 31 +++------------- .../indexing/jvm/JarSymbolScanner.kt | 36 ++++--------------- .../indexing/jvm/KotlinMetadataScanner.kt | 28 +++------------ 5 files changed, 18 insertions(+), 81 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c4e15649b..ad541db938 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ agp = "8.8.2" agp-tooling = "8.11.0" androidx-sqlite = "2.6.2" appcompatVersion = "1.7.1" +asm = "9.7.1" colorpickerview = "2.3.0" commonsCompress = "1.27.1" tukaaniXz = "1.9" @@ -103,6 +104,7 @@ androidx-security-crypto = { module = "androidx.security:security-crypto", versi androidx-sqlite-ktx = { module = "androidx.sqlite:sqlite-ktx", version.ref = "androidx-sqlite" } androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "androidx-sqlite" } androidx-viewpager2-v110beta02 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } +asm = { module = "org.ow2.asm:asm", version.ref = "asm" } colorpickerview = { module = "com.github.skydoves:colorpickerview", version.ref = "colorpickerview" } commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" } tukaani-xz = { module = "org.tukaani:xz", version.ref = "tukaaniXz" } diff --git a/lsp/jvm-symbol-index/build.gradle.kts b/lsp/jvm-symbol-index/build.gradle.kts index a26497c9c2..985d5465ce 100644 --- a/lsp/jvm-symbol-index/build.gradle.kts +++ b/lsp/jvm-symbol-index/build.gradle.kts @@ -10,6 +10,7 @@ android { } dependencies { + api(libs.asm) api(libs.google.protobuf.java) api(libs.google.protobuf.kotlin) api(libs.kotlinx.coroutines.core) @@ -19,7 +20,6 @@ dependencies { api(projects.logger) api(projects.lsp.indexing) api(projects.lsp.jvmSymbolModels) - api(projects.subprojects.kotlinAnalysisApi) api(projects.subprojects.projects) testImplementation(projects.testing.unit) diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt index 33256609df..77485ef904 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt @@ -1,12 +1,9 @@ package org.appdevforall.codeonthego.indexing.jvm -import org.jetbrains.kotlin.analysis.api.KaImplementationDetail -import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.org.objectweb.asm.AnnotationVisitor -import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassVisitor -import org.jetbrains.org.objectweb.asm.Opcodes +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes import org.slf4j.LoggerFactory import java.io.ByteArrayOutputStream import java.nio.file.Path @@ -21,26 +18,6 @@ object CombinedJarScanner { private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) - @OptIn(KaImplementationDetail::class) - fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Sequence = sequence { - val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) - for (vf in allFiles) { - if (!vf.name.endsWith(".class")) continue - if (vf.name == "module-info.class" || vf.name == "package-info.class") continue - try { - val bytes = vf.contentsToByteArray() - val symbols = if (hasKotlinMetadata(bytes)) { - KotlinMetadataScanner.parseKotlinClass(bytes.inputStream(), sourceId) - } else { - JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) - } - symbols?.forEach { yield(it) } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", vf.path, e.message) - } - } - } - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Sequence = sequence { val jar = try { JarFile(jarPath.toFile()) diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt index 2c30a93372..68d8fbd0d0 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt @@ -4,16 +4,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import org.jetbrains.kotlin.analysis.api.KaImplementationDetail -import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.org.objectweb.asm.AnnotationVisitor -import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassVisitor -import org.jetbrains.org.objectweb.asm.FieldVisitor -import org.jetbrains.org.objectweb.asm.MethodVisitor -import org.jetbrains.org.objectweb.asm.Opcodes -import org.jetbrains.org.objectweb.asm.Type +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.FieldVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type import org.slf4j.LoggerFactory import java.io.InputStream import java.nio.file.Path @@ -32,25 +29,6 @@ object JarSymbolScanner { private val log = LoggerFactory.getLogger(JarSymbolScanner::class.java) - @OptIn(KaImplementationDetail::class) - fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Flow = flow { - val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) - for (vf in allFiles) { - if (!vf.name.endsWith(".class")) continue - if (vf.name == "module-info.class" || vf.name == "package-info.class") continue - try { - vf.contentsToByteArray().inputStream().use { input -> - for (symbol in parseClassFile(input, sourceId)) { - emit(symbol) - } - } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", vf.path, e.message) - } - } - } - .flowOn(Dispatchers.IO) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { val jar = try { JarFile(jarPath.toFile()) diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt index 8c11a91c54..8ce85dfe85 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt @@ -4,13 +4,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn -import org.jetbrains.kotlin.analysis.api.KaImplementationDetail -import org.jetbrains.kotlin.analysis.api.impl.base.util.LibraryUtils -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.org.objectweb.asm.AnnotationVisitor -import org.jetbrains.org.objectweb.asm.ClassReader -import org.jetbrains.org.objectweb.asm.ClassVisitor -import org.jetbrains.org.objectweb.asm.Opcodes +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Opcodes import org.slf4j.LoggerFactory import java.io.InputStream import java.nio.file.Path @@ -53,23 +50,6 @@ object KotlinMetadataScanner { private val log = LoggerFactory.getLogger(KotlinMetadataScanner::class.java) - @OptIn(KaImplementationDetail::class) - fun scan(rootVf: VirtualFile, sourceId: String = rootVf.path): Flow = flow { - val allFiles = LibraryUtils.getAllVirtualFilesFromRoot(rootVf, includeRoot = true) - for (vf in allFiles) { - if (!vf.name.endsWith(".class")) continue - if (vf.name == "module-info.class") continue - try { - vf.contentsToByteArray().inputStream().use { input -> - parseKotlinClass(input, sourceId)?.forEach { emit(it) } - } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", vf.path, e.message) - } - } - } - .flowOn(Dispatchers.IO) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { val jar = try { JarFile(jarPath.toFile()) From a1b8042c77e8410b1738ce237f2a2891db442608 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Tue, 4 Aug 2026 18:07:53 -0700 Subject: [PATCH 02/11] ADFA-5010: WIP - split lsp/kotlin into api/impl and carry the compiler off-dex Part 2 of moving the embedded Kotlin Analysis API off the main DEX. Splits lsp/kotlin into a thin shell (lsp/kotlin: LSP wiring + a new DexClassLoader-based KotlinCompilerLoader) and lsp/kotlin-api (shared interfaces: IKotlinCompilationEnvironment, IKotlinCompilerSession(Factory)). The analysis-api-heavy implementation moves to a new lsp/kotlin-compiler-impl module, which subprojects/kotlin-compiler-carrier (a never-installed com.android.application module) packages into kotlin-compiler-carrier.apk. app/build.gradle.kts copies that APK into assets so it ships in the base APK without D8 ever merging its classes into app's own dex; KotlinCompilerLoader extracts and loads it lazily on first Kotlin file interaction. Verified: all touched modules compile, the carrier APK builds (assembleV8Release), and a full :app:assembleV8Debug succeeds end-to-end. Not done yet: spotlessApply still reports 5 ktlint violations (one real 188-char line, three wildcard imports, one line-length report on a line that doesn't look violating and needs investigation) in lsp/kotlin-compiler-impl files carried over from lsp/kotlin. Commit as WIP to unblock other work; formatting/commit-splitting/on-device verification/Jira update still to follow. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + ARCHITECTURE.md | 3 +- app/build.gradle.kts | 39 ++ .../indexing/jvm/CombinedJarScanner.kt | 85 ++-- .../indexing/jvm/JarSymbolScanner.kt | 305 +++++++------ .../indexing/jvm/KotlinMetadataScanner.kt | 407 +++++++++++------- lsp/kotlin-api/build.gradle.kts | 27 ++ lsp/kotlin-api/src/main/AndroidManifest.xml | 18 + .../lsp/kotlin/api/DiagnosticAction.kt | 27 ++ .../api/IKotlinCompilationEnvironment.kt | 49 +++ .../lsp/kotlin/api/IKotlinCompilerSession.kt | 30 ++ .../api/IKotlinCompilerSessionFactory.kt | 22 + .../lsp/kotlin/api/KtSourceFileIndexKeys.kt | 15 + lsp/kotlin-compiler-impl/build.gradle.kts | 43 ++ .../src/main/AndroidManifest.xml | 18 + .../lsp/kotlin/KotlinCodeActionsMenu.kt | 0 .../lsp/kotlin/actions/AddImportAction.kt | 4 +- .../kotlin/actions/ImplementMembersAction.kt | 2 +- .../lsp/kotlin/actions/NullSafetyAction.kt | 8 +- .../kotlin/actions/OrganizeImportsAction.kt | 2 +- .../AbstractCompilationEnvironment.kt | 0 .../kotlin/compiler/CompilationEnvironment.kt | 42 +- .../lsp/kotlin/compiler/CompilationKind.kt | 9 +- .../lsp/kotlin/compiler/Compiler.kt | 0 .../lsp/kotlin/compiler/CompilerExts.kt | 0 .../IncrementalModificationTracker.kt | 14 +- .../KotlinApplicationEnvironmentPin.kt | 3 +- .../KotlinCompilerSessionFactoryImpl.kt | 44 ++ .../compiler/KotlinCompilerSessionImpl.kt | 46 ++ .../lsp/kotlin/compiler/KotlinProjectModel.kt | 60 +-- .../lsp/kotlin/compiler/ReadWriteLock.kt | 2 +- .../lsp/kotlin/compiler/WorkspaceExts.kt | 100 +++++ .../lsp/kotlin/compiler/index/IndexCommand.kt | 29 ++ .../lsp/kotlin/compiler/index/IndexWorker.kt | 0 .../kotlin/compiler/index/KtSymbolIndex.kt | 6 +- .../kotlin/compiler/index/ScanningWorker.kt | 62 +++ .../compiler/index/SourceFileIndexer.kt | 0 .../lsp/kotlin/compiler/index/WorkerQueue.kt | 10 +- .../compiler/modules/AbstractKtModule.kt | 2 +- .../compiler/modules/AbstractSourceModule.kt | 5 +- .../compiler/modules/AnalysisScheduler.kt | 0 .../modules/AnalysisThreadContext.java | 0 .../modules/CancelCheckerProgressIndicator.kt | 0 .../lsp/kotlin/compiler/modules/KtFileExts.kt | 0 .../compiler/modules/KtLibraryModule.kt | 68 +-- .../lsp/kotlin/compiler/modules/KtModule.kt | 0 .../kotlin/compiler/modules/KtSourceModule.kt | 54 ++- .../modules/NotUnderContentRootModule.kt | 21 +- .../registrar/AnalysisApiServiceProvider.kt | 103 +++-- .../registrar/AnalysisApiServiceProviders.kt | 46 +- .../LspAnalysisApiServiceRegistrar.kt | 10 +- .../services/AnalysisPermissionOptions.kt | 2 +- .../compiler/services/AnnotationsResolver.kt | 69 +-- .../compiler/services/DeclarationsProvider.kt | 60 ++- .../services/DirectInheritorsProvider.kt | 66 +-- .../JavaModuleAccessibilityChecker.kt | 15 +- .../services/JavaModuleAnnotationsProvider.kt | 9 +- .../kotlin/compiler/services/KtLspService.kt | 3 +- .../compiler/services/LanguageSettings.kt | 2 +- .../services/ModificationTrackerFactory.kt | 4 +- .../services/ModuleDependentsProvider.kt | 41 +- .../services/NoOpAsyncExecutionService.kt | 192 +++++++++ .../compiler/services/PackageProvider.kt | 41 +- .../compiler/services/PlatformSettings.kt | 2 +- .../services/ProjectStructureProvider.kt | 39 +- .../services/ResolutionScopeProvider.kt | 2 +- .../compiler/services/WriteAccessGuard.kt | 8 +- .../lsp/kotlin/compiler/util/SLF4JLogger.kt | 72 ++++ .../completion/AdvancedKotlinEditHandler.kt | 5 +- .../completion/BaseKotlinEditHandler.kt | 6 +- .../kotlin/completion/CompletionContext.kt | 3 +- .../lsp/kotlin/completion/ContextKeywords.kt | 2 +- .../lsp/kotlin/completion/ContextResolver.kt | 106 +++-- .../kotlin/completion/DeclarationContext.kt | 19 +- .../completion/KotlinAutoImportEditHandler.kt | 5 +- .../kotlin/completion/KotlinCompletionItem.kt | 31 +- .../kotlin/completion/KotlinCompletions.kt | 0 .../completion/KotlinSnippetRepository.kt | 9 +- .../kotlin/completion/KotlinSnippetScope.kt | 7 +- .../lsp/kotlin/completion/ModifierFilter.kt | 2 +- .../diagnostic/KotlinDiagnosticProvider.kt | 21 +- .../lsp/kotlin/navigation/GoToDefinition.kt | 0 .../lsp/kotlin/navigation/ReferenceAtCaret.kt | 0 .../signaturehelp/ActiveParameterResolver.kt | 0 .../signaturehelp/CallAtCursorFinder.kt | 0 .../signaturehelp/KotlinSignatureHelp.kt | 0 .../signaturehelp/SignatureInfoBuilder.kt | 0 .../lsp/kotlin/utils/AbstractMemberStubs.kt | 0 .../lsp/kotlin/utils/ContextKeywords.kt | 92 ++++ .../lsp/kotlin/utils/ContextResolver.kt | 102 +++-- .../androidide/lsp/kotlin/utils/EditExts.kt | 63 +-- .../lsp/kotlin/utils/ImportOrganizer.kt | 0 .../lsp/kotlin/utils/ImportUsageCollector.kt | 3 + .../lsp/kotlin/utils/ModifierFilter.kt | 2 +- .../lsp/kotlin/utils/NullSafetyFix.kt | 0 .../androidide/lsp/kotlin/utils/SymbolExts.kt | 2 +- .../kotlin/utils/SymbolVisibilityChecker.kt | 15 +- .../lsp/kotlin/utils/TypeRendering.kt | 0 .../lsp/kotlin/utils/VirtualFileExts.kt | 6 +- .../main/resources/META-INF/kt-lsp/kt-lsp.xml | 0 lsp/kotlin/build.gradle.kts | 2 +- .../lsp/kotlin/KotlinLanguageServer.kt | 173 +++----- .../kotlin/actions/BaseKotlinCodeAction.kt | 12 +- .../lsp/kotlin/compiler/WorkspaceExts.kt | 93 ---- .../lsp/kotlin/compiler/index/IndexCommand.kt | 15 - .../kotlin/compiler/index/ScanningWorker.kt | 60 --- .../services/NoOpAsyncExecutionService.kt | 157 ------- .../lsp/kotlin/compiler/util/SLF4JLogger.kt | 57 --- .../lsp/kotlin/loader/KotlinCompilerLoader.kt | 120 ++++++ .../lsp/kotlin/utils/ContextKeywords.kt | 74 ---- settings.gradle.kts | 3 + .../kotlin-compiler-carrier/build.gradle.kts | 25 ++ .../src/main/AndroidManifest.xml | 24 ++ 113 files changed, 2349 insertions(+), 1435 deletions(-) create mode 100644 lsp/kotlin-api/build.gradle.kts create mode 100644 lsp/kotlin-api/src/main/AndroidManifest.xml create mode 100644 lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/DiagnosticAction.kt create mode 100644 lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilationEnvironment.kt create mode 100644 lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt create mode 100644 lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSessionFactory.kt create mode 100644 lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/KtSourceFileIndexKeys.kt create mode 100644 lsp/kotlin-compiler-impl/build.gradle.kts create mode 100644 lsp/kotlin-compiler-impl/src/main/AndroidManifest.xml rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt (96%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt (98%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt (94%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt (97%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt (87%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt (89%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt (71%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt (99%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionFactoryImpl.kt create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt (76%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt (99%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt (98%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt (96%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt (98%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt (84%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt (75%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt (87%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt (57%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt (65%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt (92%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt (89%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt (84%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt (84%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt (81%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt (89%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt (79%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt (96%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt (67%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt (71%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt (79%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt (88%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt (65%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt (96%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt (88%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt (73%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt (55%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt (97%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt (81%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt (75%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt (88%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt (89%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/ActiveParameterResolver.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/CallAtCursorFinder.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/KotlinSignatureHelp.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/SignatureInfoBuilder.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/AbstractMemberStubs.kt (100%) create mode 100644 lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt (72%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt (69%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportOrganizer.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/NullSafetyFix.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt (99%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt (91%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/TypeRendering.kt (100%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt (53%) rename lsp/{kotlin => kotlin-compiler-impl}/src/main/resources/META-INF/kt-lsp/kt-lsp.xml (100%) delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt delete mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt create mode 100644 subprojects/kotlin-compiler-carrier/build.gradle.kts create mode 100644 subprojects/kotlin-compiler-carrier/src/main/AndroidManifest.xml diff --git a/.gitignore b/.gitignore index af44d5bf1c..f81dcfa003 100755 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,7 @@ assets/gradle-*.zip app/src/release/assets/database/documentation.db app/src/main/assets/database/documentation app/src/main/assets/database/documentation.db +app/src/main/assets/kotlin-compiler-carrier.apk # AI plugin development artifacts (moved to plugin-examples repo) *.cgp diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4be5ac177e..588294ba61 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -59,7 +59,8 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil |---|---|---| | Application | `app` | The IDE itself — activities, fragments, services, DI, agent, web server. Wires everything together. | | Build engine | `subprojects:tooling-api*`, `gradle-plugin*`, `subprojects:projects`, `subprojects:builder-model-impl` | Runs a real Gradle build of the user's project out-of-process and streams events back. | -| Language tooling | `lsp:{api,java,kotlin,xml,indexing,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. | +| Language tooling | `lsp:{api,java,kotlin,kotlin-api,kotlin-compiler-impl,xml,indexing,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting. `lsp:kotlin` is a thin shell (LSP wiring, `DexClassLoader` loader); `lsp:kotlin-api` holds the interfaces shared across the split; `lsp:kotlin-compiler-impl` carries the actual Kotlin Analysis API dependency and is never resident in the app's main dex — see `subprojects:kotlin-compiler-carrier` below (ADFA-5010). | +| On-device compiler carrier | `subprojects:kotlin-analysis-api`, `subprojects:kotlin-compiler-carrier` | Packages `lsp:kotlin-compiler-impl` + the downloaded Kotlin Analysis API jar into a standalone (never-installed) APK shipped in `app`'s assets; `lsp:kotlin`'s `KotlinCompilerLoader` extracts and `DexClassLoader`-loads it lazily on first Kotlin file interaction, keeping the ~28MB dependency graph out of the main app dex (ADFA-5010). | | UI design tooling | `layouteditor`, `uidesigner`, `xml-inflater`, `vectormaster`, `compose-preview` | Visual/XML design surfaces for the *user's* app. | | Shell | `termux:{termux-app,termux-shared,termux-view,termux-emulator}` | Embedded Termux shell and terminal. | | Plugin system | `plugin-api`, `plugin-api:plugin-builder`, `plugin-manager` | In-app plugin SDK + manager — `AndroidManifest.xml` `` contract, permissions, extensions. See [plugin-api.md](docs/plugin-api.md) for the API surface & compatibility policy. | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2f4fdf7ddc..aa67d0abad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -36,6 +36,15 @@ plugins { alias(libs.plugins.google.services) } +// Forces :subprojects:kotlin-compiler-carrier to configure eagerly, ahead of app's own +// configuration. Without this, org.gradle.configureondemand=true (gradle.properties) only +// reaches that project lazily via copyKotlinCompilerCarrierToAssets's cross-project dependsOn, +// which trips "DefaultClassLoaderScope must be locked before it can be used to compute a +// classpath" for this (com.android.application) target specifically -- plugin-api, referenced +// the same way by copyPluginApiJarToAssets below, doesn't hit this because app already +// configures it naturally as a real compile dependency. +evaluationDependsOn(":subprojects:kotlin-compiler-carrier") + fun propOrEnv(name: String): String = project.findProperty(name) as String? ?: System.getenv(name) @@ -409,6 +418,36 @@ tasks.register("downloadDocDb") { } } +// Copies the Kotlin Analysis API "carrier" APK -- built by :subprojects:kotlin-compiler-carrier, +// which bundles the isolated lsp:kotlin-compiler-impl module + the downloaded analysis-api jar -- +// straight into app's own assets, so it ships in the base APK and D8 never merges it into app's own +// classes*.dex. KotlinCompilerLoader (lsp:kotlin) extracts and DexClassLoader-loads it lazily on +// first Kotlin file interaction (ADFA-5010). Unlike plugin-api.jar/the big installer zips below, this +// doesn't need the root assets/ + assets-.zip pipeline -- it's a few MB, not hundreds, and +// belongs in the base APK unconditionally rather than an optional first-run download. +tasks.register("copyKotlinCompilerCarrierToAssets") { + // Deliberately avoids project(":subprojects:kotlin-compiler-carrier").layout... here: eagerly + // cross-referencing that (com.android.application) project's layout from within app's own + // configuration trips a "classloader scope must be locked" Gradle failure under + // org.gradle.configureondemand=true. A plain rootDir-relative path sidesteps it; only the + // (lazy, string-based) dependsOn below needs to resolve the other project's task. + dependsOn(":subprojects:kotlin-compiler-carrier:assembleV8Release") + val sourceFile = + rootProject.layout.projectDirectory + .dir("subprojects/kotlin-compiler-carrier/build/outputs/apk/v8/release") + .file("kotlin-compiler-carrier-v8-release-unsigned.apk") + val destFile = layout.projectDirectory.file("src/main/assets/kotlin-compiler-carrier.apk") + inputs.file(sourceFile) + outputs.file(destFile) + doLast { + sourceFile.asFile.copyTo(destFile.asFile, overwrite = true) + } +} + +tasks.named("preBuild") { + dependsOn("copyKotlinCompilerCarrierToAssets") +} + tasks.register("copyPluginApiJarToAssets") { dependsOn(":plugin-api:createPluginApiJar") val sourceFile = project(":plugin-api").layout.buildDirectory.file("libs/plugin-api-1.0.0.jar") diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt index 77485ef904..337370ce40 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/CombinedJarScanner.kt @@ -15,57 +15,66 @@ import kotlin.io.path.pathString * [KotlinMetadataScanner] for Kotlin classes, [JarSymbolScanner] for Java. */ object CombinedJarScanner { - private val log = LoggerFactory.getLogger(CombinedJarScanner::class.java) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Sequence = sequence { - val jar = try { - JarFile(jarPath.toFile()) - } catch (e: Exception) { - log.warn("Failed to open JAR: {}", jarPath, e) - return@sequence - } + fun scan( + jarPath: Path, + sourceId: String = jarPath.pathString, + ): Sequence = + sequence { + val jar = + try { + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@sequence + } - jar.use { - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - if (!entry.name.endsWith(".class")) continue - if (entry.name == "module-info.class" || entry.name == "package-info.class") continue + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class" || entry.name == "package-info.class") continue - try { - val bytes = jar.getInputStream(entry).use { input -> - val buf = ByteArrayOutputStream(entry.size.toInt().coerceAtLeast(1024)) - input.copyTo(buf) - buf.toByteArray() - } + try { + val bytes = + jar.getInputStream(entry).use { input -> + val buf = ByteArrayOutputStream(entry.size.toInt().coerceAtLeast(1024)) + input.copyTo(buf) + buf.toByteArray() + } - val symbols = if (hasKotlinMetadata(bytes)) { - KotlinMetadataScanner.parseKotlinClass(bytes.inputStream(), sourceId) - } else { - JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) - } + val symbols = + if (hasKotlinMetadata(bytes)) { + KotlinMetadataScanner.parseKotlinClass(bytes.inputStream(), sourceId) + } else { + JarSymbolScanner.parseClassFile(bytes.inputStream(), sourceId) + } - symbols?.forEach { yield(it) } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", entry.name, e.message) + symbols?.forEach { yield(it) } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", entry.name, e.message) + } } } } - } private fun hasKotlinMetadata(classBytes: ByteArray): Boolean { var found = false try { - ClassReader(classBytes).accept(object : ClassVisitor(Opcodes.ASM9) { - override fun visitAnnotation( - descriptor: String?, - visible: Boolean - ): AnnotationVisitor? { - if (descriptor == "Lkotlin/Metadata;") found = true - return null - } - }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) + ClassReader(classBytes).accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitAnnotation( + descriptor: String?, + visible: Boolean, + ): AnnotationVisitor? { + if (descriptor == "Lkotlin/Metadata;") found = true + return null + } + }, + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES, + ) } catch (_: Exception) { } return found diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt index 68d8fbd0d0..f848259e3e 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/JarSymbolScanner.kt @@ -26,45 +26,51 @@ import kotlin.io.path.pathString * semantics like extensions, suspend, or nullable types. */ object JarSymbolScanner { - private val log = LoggerFactory.getLogger(JarSymbolScanner::class.java) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { - val jar = try { - JarFile(jarPath.toFile()) - } catch (e: Exception) { - log.warn("Failed to open JAR: {}", jarPath, e) - return@flow - } - - jar.use { - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - if (!entry.name.endsWith(".class")) continue - if (entry.name == "module-info.class") continue - if (entry.name == "package-info.class") continue - + fun scan( + jarPath: Path, + sourceId: String = jarPath.pathString, + ): Flow = + flow { + val jar = try { - jar.getInputStream(entry).use { input -> - for (symbol in parseClassFile(input, sourceId)) { - emit(symbol) + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@flow + } + + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class") continue + if (entry.name == "package-info.class") continue + + try { + jar.getInputStream(entry).use { input -> + for (symbol in parseClassFile(input, sourceId)) { + emit(symbol) + } } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", entry.name, e.message) } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", entry.name, e.message) } } - } - } - .flowOn(Dispatchers.IO) + }.flowOn(Dispatchers.IO) - internal fun parseClassFile(input: InputStream, sourceId: String): List { + internal fun parseClassFile( + input: InputStream, + sourceId: String, + ): List { val reader = ClassReader(input) val collector = SymbolCollector(sourceId) reader.accept( collector, - ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES, ) return collector.symbols } @@ -72,7 +78,6 @@ object JarSymbolScanner { private class SymbolCollector( private val sourceId: String, ) : ClassVisitor(Opcodes.ASM9) { - val symbols = mutableListOf() private var className = "" @@ -87,8 +92,11 @@ object JarSymbolScanner { private var classDeprecated = false override fun visit( - version: Int, access: Int, name: String, - signature: String?, superName: String?, + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, interfaces: Array?, ) { className = name @@ -107,7 +115,10 @@ object JarSymbolScanner { isInnerClass = name.contains('$') } - override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + override fun visitAnnotation( + descriptor: String?, + visible: Boolean, + ): AnnotationVisitor? { if (descriptor == "Ljava/lang/Deprecated;") classDeprecated = true if (descriptor == "Lkotlin/Metadata;") isKotlinClass = true return null @@ -116,23 +127,32 @@ object JarSymbolScanner { override fun visitEnd() { if (!isPublicOrProtected(classAccess)) return - val isAnonymous = isInnerClass && - shortClassName.split('.').last().firstOrNull()?.isDigit() == true + val isAnonymous = + isInnerClass && + shortClassName + .split('.') + .last() + .firstOrNull() + ?.isDigit() == true if (isAnonymous) return val kind = classKindFromAccess(classAccess) val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - val supertypes = buildList { - superName?.let { - if (it != "java/lang/Object") add(it) + val supertypes = + buildList { + superName?.let { + if (it != "java/lang/Object") add(it) + } + interfaces?.forEach { add(it) } } - interfaces?.forEach { add(it) } - } - val containingClass = if (isInnerClass) { - className.substringBeforeLast('$') - } else "" + val containingClass = + if (isInnerClass) { + className.substringBeforeLast('$') + } else { + "" + } symbols.add( JvmSymbol( @@ -145,22 +165,26 @@ object JarSymbolScanner { language = language, visibility = visibilityFromAccess(classAccess), isDeprecated = classDeprecated, - data = JvmClassInfo( - internalName = className, - containingClassName = containingClass, - supertypeNames = supertypes, - isAbstract = hasFlag(classAccess, Opcodes.ACC_ABSTRACT), - isFinal = hasFlag(classAccess, Opcodes.ACC_FINAL), - isInner = isInnerClass && !hasFlag(classAccess, Opcodes.ACC_STATIC), - isStatic = isInnerClass && hasFlag(classAccess, Opcodes.ACC_STATIC), - ), - ) + data = + JvmClassInfo( + internalName = className, + containingClassName = containingClass, + supertypeNames = supertypes, + isAbstract = hasFlag(classAccess, Opcodes.ACC_ABSTRACT), + isFinal = hasFlag(classAccess, Opcodes.ACC_FINAL), + isInner = isInnerClass && !hasFlag(classAccess, Opcodes.ACC_STATIC), + isStatic = isInnerClass && hasFlag(classAccess, Opcodes.ACC_STATIC), + ), + ), ) } override fun visitMethod( - access: Int, name: String, descriptor: String, - signature: String?, exceptions: Array?, + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array?, ): MethodVisitor? { if (!isPublicOrProtected(access)) return null if (!isPublicOrProtected(classAccess)) return null @@ -178,26 +202,28 @@ object JarSymbolScanner { val kind = if (isConstructor) JvmSymbolKind.CONSTRUCTOR else JvmSymbolKind.FUNCTION val language = if (isKotlinClass) JvmSourceLanguage.KOTLIN else JvmSourceLanguage.JAVA - val parameters = paramTypes.map { type -> - JvmParameterInfo( - name = "", // not available without -parameters flag - typeName = typeToName(type), - typeDisplayName = typeToDisplayName(type), - ) - } + val parameters = + paramTypes.map { type -> + JvmParameterInfo( + name = "", // not available without -parameters flag + typeName = typeToName(type), + typeDisplayName = typeToDisplayName(type), + ) + } val fqName = "$className#$methodName" val key = "$fqName(${parameters.joinToString(",") { it.typeName }})" - val signatureDisplay = buildString { - append("(") - append(parameters.joinToString(", ") { it.typeDisplayName }) - append(")") - if (!isConstructor) { - append(": ") - append(typeToDisplayName(returnType)) + val signatureDisplay = + buildString { + append("(") + append(parameters.joinToString(", ") { it.typeDisplayName }) + append(")") + if (!isConstructor) { + append(": ") + append(typeToDisplayName(returnType)) + } } - } symbols.add( JvmSymbol( @@ -210,26 +236,30 @@ object JarSymbolScanner { language = language, visibility = visibilityFromAccess(access), isDeprecated = classDeprecated, - data = JvmFunctionInfo( - containingClassName = className, - returnTypeName = typeToName(returnType), - returnTypeDisplayName = typeToDisplayName(returnType), - parameterCount = paramTypes.size, - parameters = parameters, - signatureDisplay = signatureDisplay, - isStatic = hasFlag(access, Opcodes.ACC_STATIC), - isAbstract = hasFlag(access, Opcodes.ACC_ABSTRACT), - isFinal = hasFlag(access, Opcodes.ACC_FINAL), - ), - ) + data = + JvmFunctionInfo( + containingClassName = className, + returnTypeName = typeToName(returnType), + returnTypeDisplayName = typeToDisplayName(returnType), + parameterCount = paramTypes.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + isStatic = hasFlag(access, Opcodes.ACC_STATIC), + isAbstract = hasFlag(access, Opcodes.ACC_ABSTRACT), + isFinal = hasFlag(access, Opcodes.ACC_FINAL), + ), + ), ) return null } override fun visitField( - access: Int, name: String, descriptor: String, - signature: String?, value: Any?, + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any?, ): FieldVisitor? { if (!isPublicOrProtected(access)) return null if (!isPublicOrProtected(classAccess)) return null @@ -251,67 +281,74 @@ object JarSymbolScanner { language = language, visibility = visibilityFromAccess(access), isDeprecated = classDeprecated, - data = JvmFieldInfo( - containingClassName = className, - typeName = typeToName(fieldType), - typeDisplayName = typeToDisplayName(fieldType), - isStatic = hasFlag(access, Opcodes.ACC_STATIC), - isFinal = hasFlag(access, Opcodes.ACC_FINAL), - constantValue = value?.toString() ?: "", - ), - ) + data = + JvmFieldInfo( + containingClassName = className, + typeName = typeToName(fieldType), + typeDisplayName = typeToDisplayName(fieldType), + isStatic = hasFlag(access, Opcodes.ACC_STATIC), + isFinal = hasFlag(access, Opcodes.ACC_FINAL), + constantValue = value?.toString() ?: "", + ), + ), ) return null } - private fun isPublicOrProtected(access: Int) = - hasFlag(access, Opcodes.ACC_PUBLIC) || hasFlag(access, Opcodes.ACC_PROTECTED) + private fun isPublicOrProtected(access: Int) = hasFlag(access, Opcodes.ACC_PUBLIC) || hasFlag(access, Opcodes.ACC_PROTECTED) - private fun hasFlag(access: Int, flag: Int) = (access and flag) != 0 + private fun hasFlag( + access: Int, + flag: Int, + ) = (access and flag) != 0 - private fun classKindFromAccess(access: Int) = when { - hasFlag(access, Opcodes.ACC_ANNOTATION) -> JvmSymbolKind.ANNOTATION_CLASS - hasFlag(access, Opcodes.ACC_ENUM) -> JvmSymbolKind.ENUM - hasFlag(access, Opcodes.ACC_INTERFACE) -> JvmSymbolKind.INTERFACE - else -> JvmSymbolKind.CLASS - } + private fun classKindFromAccess(access: Int) = + when { + hasFlag(access, Opcodes.ACC_ANNOTATION) -> JvmSymbolKind.ANNOTATION_CLASS + hasFlag(access, Opcodes.ACC_ENUM) -> JvmSymbolKind.ENUM + hasFlag(access, Opcodes.ACC_INTERFACE) -> JvmSymbolKind.INTERFACE + else -> JvmSymbolKind.CLASS + } - private fun visibilityFromAccess(access: Int) = when { - hasFlag(access, Opcodes.ACC_PUBLIC) -> JvmVisibility.PUBLIC - hasFlag(access, Opcodes.ACC_PROTECTED) -> JvmVisibility.PROTECTED - hasFlag(access, Opcodes.ACC_PRIVATE) -> JvmVisibility.PRIVATE - else -> JvmVisibility.PACKAGE_PRIVATE - } + private fun visibilityFromAccess(access: Int) = + when { + hasFlag(access, Opcodes.ACC_PUBLIC) -> JvmVisibility.PUBLIC + hasFlag(access, Opcodes.ACC_PROTECTED) -> JvmVisibility.PROTECTED + hasFlag(access, Opcodes.ACC_PRIVATE) -> JvmVisibility.PRIVATE + else -> JvmVisibility.PACKAGE_PRIVATE + } - private fun typeToName(type: Type): String = when (type.sort) { - Type.VOID -> "V" - Type.BOOLEAN -> "Z" - Type.BYTE -> "B" - Type.CHAR -> "C" - Type.SHORT -> "S" - Type.INT -> "I" - Type.LONG -> "J" - Type.FLOAT -> "F" - Type.DOUBLE -> "D" - Type.ARRAY -> "[".repeat(type.dimensions) + typeToName(type.elementType) - Type.OBJECT -> type.internalName - else -> type.internalName - } + private fun typeToName(type: Type): String = + when (type.sort) { + Type.VOID -> "V" + Type.BOOLEAN -> "Z" + Type.BYTE -> "B" + Type.CHAR -> "C" + Type.SHORT -> "S" + Type.INT -> "I" + Type.LONG -> "J" + Type.FLOAT -> "F" + Type.DOUBLE -> "D" + Type.ARRAY -> "[".repeat(type.dimensions) + typeToName(type.elementType) + Type.OBJECT -> type.internalName + else -> type.internalName + } - private fun typeToDisplayName(type: Type): String = when (type.sort) { - Type.BOOLEAN -> "boolean" - Type.BYTE -> "byte" - Type.CHAR -> "char" - Type.SHORT -> "short" - Type.INT -> "int" - Type.LONG -> "long" - Type.FLOAT -> "float" - Type.DOUBLE -> "double" - Type.VOID -> "void" - Type.ARRAY -> typeToDisplayName(type.elementType) + "[]".repeat(type.dimensions) - Type.OBJECT -> type.className.substringAfterLast('.') - else -> typeToName(type) - } + private fun typeToDisplayName(type: Type): String = + when (type.sort) { + Type.BOOLEAN -> "boolean" + Type.BYTE -> "byte" + Type.CHAR -> "char" + Type.SHORT -> "short" + Type.INT -> "int" + Type.LONG -> "long" + Type.FLOAT -> "float" + Type.DOUBLE -> "double" + Type.VOID -> "void" + Type.ARRAY -> typeToDisplayName(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className.substringAfterLast('.') + else -> typeToName(type) + } } } diff --git a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt index 8ce85dfe85..b3949cf909 100644 --- a/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt +++ b/lsp/jvm-symbol-index/src/main/kotlin/org/appdevforall/codeonthego/indexing/jvm/KotlinMetadataScanner.kt @@ -47,64 +47,76 @@ import kotlin.metadata.visibility * Skips non-Kotlin class files (no `@Metadata` annotation). */ object KotlinMetadataScanner { - private val log = LoggerFactory.getLogger(KotlinMetadataScanner::class.java) - fun scan(jarPath: Path, sourceId: String = jarPath.pathString): Flow = flow { - val jar = try { - JarFile(jarPath.toFile()) - } catch (e: Exception) { - log.warn("Failed to open JAR: {}", jarPath, e) - return@flow - } + fun scan( + jarPath: Path, + sourceId: String = jarPath.pathString, + ): Flow = + flow { + val jar = + try { + JarFile(jarPath.toFile()) + } catch (e: Exception) { + log.warn("Failed to open JAR: {}", jarPath, e) + return@flow + } - jar.use { - val entries = jar.entries() - while (entries.hasMoreElements()) { - val entry = entries.nextElement() - if (!entry.name.endsWith(".class")) continue - if (entry.name == "module-info.class") continue + jar.use { + val entries = jar.entries() + while (entries.hasMoreElements()) { + val entry = entries.nextElement() + if (!entry.name.endsWith(".class")) continue + if (entry.name == "module-info.class") continue - try { - jar.getInputStream(entry).use { input -> - parseKotlinClass(input, sourceId)?.forEach { emit(it) } + try { + jar.getInputStream(entry).use { input -> + parseKotlinClass(input, sourceId)?.forEach { emit(it) } + } + } catch (e: Exception) { + log.debug("Failed to parse {}: {}", entry.name, e.message) } - } catch (e: Exception) { - log.debug("Failed to parse {}: {}", entry.name, e.message) } } - } - } - .flowOn(Dispatchers.IO) + }.flowOn(Dispatchers.IO) - internal fun parseKotlinClass(input: InputStream, sourceId: String): List? { + internal fun parseKotlinClass( + input: InputStream, + sourceId: String, + ): List? { val reader = ClassReader(input) val collector = MetadataCollector() reader.accept( collector, - ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES + ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES, ) val header = collector.metadataHeader ?: return null - val metadata = try { - KotlinClassMetadata.readStrict(header) - } catch (e: Exception) { - log.debug("Failed to read Kotlin metadata: {}", e.message) - return null - } + val metadata = + try { + KotlinClassMetadata.readStrict(header) + } catch (e: Exception) { + log.debug("Failed to read Kotlin metadata: {}", e.message) + return null + } return when (metadata) { - is KotlinClassMetadata.Class -> + is KotlinClassMetadata.Class -> { extractFromClass(metadata.kmClass, sourceId) + } - is KotlinClassMetadata.FileFacade -> + is KotlinClassMetadata.FileFacade -> { extractFromPackage(metadata.kmPackage, collector.packageName, sourceId) + } - is KotlinClassMetadata.MultiFileClassPart -> + is KotlinClassMetadata.MultiFileClassPart -> { extractFromPackage(metadata.kmPackage, collector.packageName, sourceId) + } - else -> null + else -> { + null + } } } @@ -114,27 +126,33 @@ object KotlinMetadataScanner { ): List { val symbols = mutableListOf() val className = klass.name - val packageName = className.substringBeforeLast('/') - .replace('/', '.') - val shortName = className.substringAfterLast('/') - .substringAfterLast('$') - - val kind = when (klass.kind) { - ClassKind.INTERFACE -> JvmSymbolKind.INTERFACE - ClassKind.ENUM_CLASS -> JvmSymbolKind.ENUM - ClassKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS - ClassKind.OBJECT -> JvmSymbolKind.OBJECT - ClassKind.COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT - ClassKind.CLASS -> JvmSymbolKind.CLASS - else -> JvmSymbolKind.CLASS - } + val packageName = + className + .substringBeforeLast('/') + .replace('/', '.') + val shortName = + className + .substringAfterLast('/') + .substringAfterLast('$') + + val kind = + when (klass.kind) { + ClassKind.INTERFACE -> JvmSymbolKind.INTERFACE + ClassKind.ENUM_CLASS -> JvmSymbolKind.ENUM + ClassKind.ANNOTATION_CLASS -> JvmSymbolKind.ANNOTATION_CLASS + ClassKind.OBJECT -> JvmSymbolKind.OBJECT + ClassKind.COMPANION_OBJECT -> JvmSymbolKind.COMPANION_OBJECT + ClassKind.CLASS -> JvmSymbolKind.CLASS + else -> JvmSymbolKind.CLASS + } - val supertypes = klass.supertypes.mapNotNull { supertype -> - when (val c = supertype.classifier) { - is KmClassifier.Class -> c.name - else -> null + val supertypes = + klass.supertypes.mapNotNull { supertype -> + when (val c = supertype.classifier) { + is KmClassifier.Class -> c.name + else -> null + } } - } symbols.add( JvmSymbol( @@ -146,17 +164,19 @@ object KotlinMetadataScanner { kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = kmVisibility(klass.visibility), - data = JvmClassInfo( - supertypeNames = supertypes, - typeParameters = klass.typeParameters.map { it.name }, - isAbstract = klass.modality == Modality.ABSTRACT, - isFinal = klass.modality == Modality.FINAL, - kotlin = KotlinClassInfo( - isSealed = klass.modality == Modality.SEALED, - sealedSubclasses = klass.sealedSubclasses.map { it.replace('/', '.') }, + data = + JvmClassInfo( + supertypeNames = supertypes, + typeParameters = klass.typeParameters.map { it.name }, + isAbstract = klass.modality == Modality.ABSTRACT, + isFinal = klass.modality == Modality.FINAL, + kotlin = + KotlinClassInfo( + isSealed = klass.modality == Modality.SEALED, + sealedSubclasses = klass.sealedSubclasses.map { it.replace('/', '.') }, + ), ), - ), - ) + ), ) for (fn in klass.functions) { @@ -178,11 +198,12 @@ object KotlinMetadataScanner { packageName = packageName, kind = JvmSymbolKind.ENUM_ENTRY, language = JvmSourceLanguage.KOTLIN, - data = JvmEnumEntryInfo( - containingClassName = className, - ordinal = ordinal, - ), - ) + data = + JvmEnumEntryInfo( + containingClassName = className, + ordinal = ordinal, + ), + ), ) } } @@ -217,12 +238,13 @@ object KotlinMetadataScanner { kind = JvmSymbolKind.TYPE_ALIAS, language = JvmSourceLanguage.KOTLIN, visibility = kmVisibility(alias.visibility), - data = JvmTypeAliasInfo( - expandedTypeName = kmTypeToName(alias.expandedType), - expandedTypeDisplayName = kmTypeToDisplayName(alias.expandedType), - typeParameters = alias.typeParameters.map { it.name }, - ), - ) + data = + JvmTypeAliasInfo( + expandedTypeName = kmTypeToName(alias.expandedType), + expandedTypeDisplayName = kmTypeToDisplayName(alias.expandedType), + typeParameters = alias.typeParameters.map { it.name }, + ), + ), ) } @@ -243,31 +265,37 @@ object KotlinMetadataScanner { val receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "" val kind = if (isExtension) JvmSymbolKind.EXTENSION_FUNCTION else JvmSymbolKind.FUNCTION - val parameters = fn.valueParameters.map { param -> - JvmParameterInfo( - name = param.name, - typeName = kmTypeToName(param.type), - typeDisplayName = kmTypeToDisplayName(param.type), - hasDefaultValue = param.declaresDefaultValue, - isVararg = param.varargElementType != null, - ) - } + val parameters = + fn.valueParameters.map { param -> + JvmParameterInfo( + name = param.name, + typeName = kmTypeToName(param.type), + typeDisplayName = kmTypeToDisplayName(param.type), + hasDefaultValue = param.declaresDefaultValue, + isVararg = param.varargElementType != null, + ) + } - val name = if (containingClass.isNotEmpty()) - "$containingClass#${fn.name}" else "$packageName#${fn.name}" + val name = + if (containingClass.isNotEmpty()) { + "$containingClass#${fn.name}" + } else { + "$packageName#${fn.name}" + } val key = "$name(${parameters.joinToString(",") { it.typeFqName }})" - val signatureDisplay = buildString { - if (isExtension) { - append(receiverTypeDisplayName) - append('.') - } + val signatureDisplay = + buildString { + if (isExtension) { + append(receiverTypeDisplayName) + append('.') + } - append("(") - append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) - append("): ") - append(kmTypeToDisplayName(fn.returnType)) - } + append("(") + append(parameters.joinToString(", ") { "${it.name}: ${it.typeDisplayName}" }) + append("): ") + append(kmTypeToDisplayName(fn.returnType)) + } return JvmSymbol( key = key, @@ -278,27 +306,29 @@ object KotlinMetadataScanner { kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = vis, - data = JvmFunctionInfo( - containingClassName = containingClass, - returnTypeName = kmTypeToName(fn.returnType), - returnTypeDisplayName = kmTypeToDisplayName(fn.returnType), - parameterCount = parameters.size, - parameters = parameters, - signatureDisplay = signatureDisplay, - typeParameters = fn.typeParameters.map { it.name }, - kotlin = KotlinFunctionInfo( - receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", - receiverTypeDisplayName = receiverTypeDisplayName, - isSuspend = fn.isSuspend, - isInline = fn.isInline, - isInfix = fn.isInfix, - isOperator = fn.isOperator, - isTailrec = fn.isTailrec, - isExternal = fn.isExternal, - isExpect = fn.isExpect, - isReturnTypeNullable = fn.returnType.isNullable, + data = + JvmFunctionInfo( + containingClassName = containingClass, + returnTypeName = kmTypeToName(fn.returnType), + returnTypeDisplayName = kmTypeToDisplayName(fn.returnType), + parameterCount = parameters.size, + parameters = parameters, + signatureDisplay = signatureDisplay, + typeParameters = fn.typeParameters.map { it.name }, + kotlin = + KotlinFunctionInfo( + receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", + receiverTypeDisplayName = receiverTypeDisplayName, + isSuspend = fn.isSuspend, + isInline = fn.isInline, + isInfix = fn.isInfix, + isOperator = fn.isOperator, + isTailrec = fn.isTailrec, + isExternal = fn.isExternal, + isExpect = fn.isExpect, + isReturnTypeNullable = fn.returnType.isNullable, + ), ), - ), ) } @@ -315,8 +345,12 @@ object KotlinMetadataScanner { val isExtension = receiverType != null val kind = if (isExtension) JvmSymbolKind.EXTENSION_PROPERTY else JvmSymbolKind.PROPERTY - val name = if (containingClass.isNotEmpty()) - "$containingClass#${prop.name}" else "$packageName#${prop.name}" + val name = + if (containingClass.isNotEmpty()) { + "$containingClass#${prop.name}" + } else { + "$packageName#${prop.name}" + } return JvmSymbol( key = name, @@ -327,33 +361,38 @@ object KotlinMetadataScanner { kind = kind, language = JvmSourceLanguage.KOTLIN, visibility = vis, - data = JvmFieldInfo( - containingClassName = containingClass, - typeName = kmTypeToName(prop.returnType), - typeDisplayName = kmTypeToDisplayName(prop.returnType), - kotlin = KotlinPropertyInfo( - receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", - receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "", - isConst = prop.isConst, - isLateinit = prop.isLateinit, - hasGetter = prop.getter != null, - hasSetter = prop.setter != null, - isDelegated = prop.isDelegated, - isTypeNullable = prop.returnType.isNullable, + data = + JvmFieldInfo( + containingClassName = containingClass, + typeName = kmTypeToName(prop.returnType), + typeDisplayName = kmTypeToDisplayName(prop.returnType), + kotlin = + KotlinPropertyInfo( + receiverTypeName = receiverType?.let { kmTypeToName(it) } ?: "", + receiverTypeDisplayName = receiverType?.let { kmTypeToDisplayName(it) } ?: "", + isConst = prop.isConst, + isLateinit = prop.isLateinit, + hasGetter = prop.getter != null, + hasSetter = prop.setter != null, + isDelegated = prop.isDelegated, + isTypeNullable = prop.returnType.isNullable, + ), ), - ), ) } - private fun kmTypeToName(type: KmType): String = when (val c = type.classifier) { - is KmClassifier.Class -> c.name - is KmClassifier.TypeAlias -> c.name - is KmClassifier.TypeParameter -> "T${c.id}" - } + private fun kmTypeToName(type: KmType): String = + when (val c = type.classifier) { + is KmClassifier.Class -> c.name + is KmClassifier.TypeAlias -> c.name + is KmClassifier.TypeParameter -> "T${c.id}" + } private fun kmTypeToDisplayName(type: KmType): String { - val base = kmTypeToName(type).substringAfterLast('/') - .substringAfterLast('$') + val base = + kmTypeToName(type) + .substringAfterLast('/') + .substringAfterLast('$') val args = type.arguments.mapNotNull { it.type?.let { t -> kmTypeToDisplayName(t) } } return buildString { append(base) @@ -362,12 +401,13 @@ object KotlinMetadataScanner { } } - private fun kmVisibility(vis: Visibility) = when (vis) { - Visibility.PUBLIC -> JvmVisibility.PUBLIC - Visibility.PROTECTED -> JvmVisibility.PROTECTED - Visibility.INTERNAL -> JvmVisibility.INTERNAL - Visibility.PRIVATE, Visibility.PRIVATE_TO_THIS, Visibility.LOCAL -> JvmVisibility.PRIVATE - } + private fun kmVisibility(vis: Visibility) = + when (vis) { + Visibility.PUBLIC -> JvmVisibility.PUBLIC + Visibility.PROTECTED -> JvmVisibility.PROTECTED + Visibility.INTERNAL -> JvmVisibility.INTERNAL + Visibility.PRIVATE, Visibility.PRIVATE_TO_THIS, Visibility.LOCAL -> JvmVisibility.PRIVATE + } private class MetadataCollector : ClassVisitor(Opcodes.ASM9) { var metadataHeader: Metadata? = null @@ -382,18 +422,28 @@ object KotlinMetadataScanner { private var extraInt: Int? = null override fun visit( - version: Int, access: Int, name: String, - signature: String?, superName: String?, interfaces: Array?, + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, ) { val lastSlash = name.lastIndexOf('/') packageName = if (lastSlash >= 0) name.substring(0, lastSlash).replace('/', '.') else "" } - override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + override fun visitAnnotation( + descriptor: String?, + visible: Boolean, + ): AnnotationVisitor? { if (descriptor != "Lkotlin/Metadata;") return null return object : AnnotationVisitor(Opcodes.ASM9) { - override fun visit(name: String?, value: Any?) { + override fun visit( + name: String?, + value: Any?, + ) { when (name) { "mv" -> { if (value is IntArray) { @@ -401,42 +451,65 @@ object KotlinMetadataScanner { } } - "k" -> metadataKind = value as? Int - "xi" -> extraInt = value as? Int - "xs" -> extraString = value as? String - "pn" -> pn = value as? String + "k" -> { + metadataKind = value as? Int + } + + "xi" -> { + extraInt = value as? Int + } + + "xs" -> { + extraString = value as? String + } + + "pn" -> { + pn = value as? String + } } } override fun visitArray(name: String?): AnnotationVisitor = object : AnnotationVisitor(Opcodes.ASM9) { private val values = mutableListOf() - override fun visit(n: String?, value: Any?) { + + override fun visit( + n: String?, + value: Any?, + ) { value?.let { values.add(it) } } override fun visitEnd() { when (name) { - "mv" -> metadataVersion = - values.filterIsInstance().toIntArray() - - "d1" -> data1 = values.filterIsInstance().toTypedArray() - "d2" -> data2 = values.filterIsInstance().toTypedArray() + "mv" -> { + metadataVersion = + values.filterIsInstance().toIntArray() + } + + "d1" -> { + data1 = values.filterIsInstance().toTypedArray() + } + + "d2" -> { + data2 = values.filterIsInstance().toTypedArray() + } } } } override fun visitEnd() { val kind = metadataKind ?: return - metadataHeader = Metadata( - kind = kind, - metadataVersion = metadataVersion ?: intArrayOf(), - data1 = data1 ?: emptyArray(), - data2 = data2 ?: emptyArray(), - extraString = extraString ?: "", - packageName = pn ?: "", - extraInt = extraInt ?: 0, - ) + metadataHeader = + Metadata( + kind = kind, + metadataVersion = metadataVersion ?: intArrayOf(), + data1 = data1 ?: emptyArray(), + data2 = data2 ?: emptyArray(), + extraString = extraString ?: "", + packageName = pn ?: "", + extraInt = extraInt ?: 0, + ) } } } diff --git a/lsp/kotlin-api/build.gradle.kts b/lsp/kotlin-api/build.gradle.kts new file mode 100644 index 0000000000..4aa59af7cf --- /dev/null +++ b/lsp/kotlin-api/build.gradle.kts @@ -0,0 +1,27 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.lsp.kotlin.api" + + kotlin.compilerOptions { + freeCompilerArgs.addAll("-Xcontext-parameters") + } +} + +dependencies { + api(projects.lsp.api) + api(projects.lsp.indexing) + api(projects.lsp.jvmSymbolIndex) + api(projects.lsp.models) + api(projects.shared) + api(projects.subprojects.projects) + + implementation(projects.common) + implementation(libs.common.kotlin) + implementation(libs.common.kotlin.coroutines.core) +} diff --git a/lsp/kotlin-api/src/main/AndroidManifest.xml b/lsp/kotlin-api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b50865785a --- /dev/null +++ b/lsp/kotlin-api/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/DiagnosticAction.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/DiagnosticAction.kt new file mode 100644 index 0000000000..03f5bd2e92 --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/DiagnosticAction.kt @@ -0,0 +1,27 @@ +package com.itsaky.androidide.lsp.kotlin.api + +/** + * The fix-it action (if any) a Kotlin diagnostic carries, attached to a + * `DiagnosticItem.extra` as a [KotlinDiagnosticExtra]. Resident (used by + * `BaseKotlinCodeAction`'s subclasses), unlike the [KotlinDiagnosticExtra.compilationEnv] + * it travels with, which is only ever a real `CompilationEnvironment` once you're + * inside the isolated `lsp:kotlin-compiler-impl` module. + */ +sealed interface DiagnosticAction { + data object None : DiagnosticAction + + data class ResolveReference( + val referenceName: String, + ) : DiagnosticAction + + data object NullSafetyFix : DiagnosticAction +} + +data class KotlinDiagnosticExtra( + val compilationEnv: IKotlinCompilationEnvironment, + val action: ActionT, +) + +@Suppress("UNCHECKED_CAST") +inline fun KotlinDiagnosticExtra<*>?.asAction(): KotlinDiagnosticExtra? = + if (this?.action is T) this as KotlinDiagnosticExtra else null diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilationEnvironment.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilationEnvironment.kt new file mode 100644 index 0000000000..c17b2078ab --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilationEnvironment.kt @@ -0,0 +1,49 @@ +package com.itsaky.androidide.lsp.kotlin.api + +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.DiagnosticResult +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.progress.ICancelChecker +import java.nio.file.Path + +/** + * Bridge to a single Kotlin compilation environment (one per [IKotlinCompilerSession] + * today), implemented on the far side of the [IKotlinCompilerSession]'s DexClassLoader + * boundary. All operations here are what were previously called directly on + * `CompilationEnvironment`/the `compiler.*` free functions from `KotlinLanguageServer`. + */ +interface IKotlinCompilationEnvironment { + fun openFileIfNeeded(path: Path) + + fun onFileOpen(path: Path) + + fun onFileContentChanged(path: Path) + + fun onFileClosed(path: Path) + + fun onFileSaved(path: Path) + + suspend fun onFileCreated(path: Path) + + suspend fun onFileRemoved(path: Path) + + suspend fun onFileMoved( + fromPath: Path, + toPath: Path, + ) + + fun complete(params: CompletionParams): CompletionResult + + suspend fun findDefinition(params: DefinitionParams): DefinitionResult + + suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp + + fun collectDiagnostics( + file: Path, + cancelChecker: ICancelChecker, + ): DiagnosticResult +} diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt new file mode 100644 index 0000000000..2b0f172613 --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt @@ -0,0 +1,30 @@ +package com.itsaky.androidide.lsp.kotlin.api + +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.projects.api.Workspace +import java.nio.file.Path + +/** + * Bridge to the isolated Kotlin Analysis API session, loaded lazily via `DexClassLoader` + * on first Kotlin-file-gated request (see `KotlinCompilerLoader`) instead of being + * always resident in the main app dex. + */ +interface IKotlinCompilerSession : AutoCloseable { + fun updateLanguageClient(client: ILanguageClient?) + + fun updateProjectModel(workspace: Workspace) + + fun refreshSources() + + /** + * The environment for [file], or null when there is none (e.g. a `.kts` script, + * which has no environment yet) or the file is not a Kotlin file at all. + */ + fun compilationEnvironmentFor(file: Path): IKotlinCompilationEnvironment? + + /** Registers the Kotlin code-actions menu. Deferred here (off the eager setup path) since it needs classes from the isolated dex. */ + fun registerCodeActions() + + /** Schedules the already-open documents in [activeDocumentFiles] for analysis, mirroring what setupWithProject used to do eagerly for every open file. */ + fun openFileIfNeeded(activeDocumentFiles: List) +} diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSessionFactory.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSessionFactory.kt new file mode 100644 index 0000000000..c818cb9e30 --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSessionFactory.kt @@ -0,0 +1,22 @@ +package com.itsaky.androidide.lsp.kotlin.api + +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.projects.api.Workspace +import java.nio.file.Path + +/** + * Entry point loaded by name (reflection) from the carrier APK's `DexClassLoader` -- + * see `KotlinCompilerLoader`. Implemented by `KotlinCompilerSessionFactoryImpl` in the + * isolated `lsp:kotlin-compiler-impl` module, which must expose a public no-arg + * constructor for this to work. + */ +interface IKotlinCompilerSessionFactory { + fun create( + workspace: Workspace, + intellijPluginRoot: Path, + jdkHome: Path, + jdkRelease: Int, + jdkVersionString: String, + languageClient: ILanguageClient?, + ): IKotlinCompilerSession +} diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/KtSourceFileIndexKeys.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/KtSourceFileIndexKeys.kt new file mode 100644 index 0000000000..0a345e7ae1 --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/KtSourceFileIndexKeys.kt @@ -0,0 +1,15 @@ +package com.itsaky.androidide.lsp.kotlin.api + +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex +import org.appdevforall.codeonthego.indexing.service.IndexKey + +/** + * Resident-safe: registered by `KotlinLanguageServer.setupWithProject` (which must stay + * resident) but keyed against index implementations that live in the isolated + * `lsp:kotlin-compiler-impl` module. Deliberately not defined alongside `KtSymbolIndex` in + * that module, even though it originally was -- these two constants themselves have zero + * Analysis API dependencies and are needed on both sides of the DexClassLoader boundary. + */ +val KT_SOURCE_FILE_INDEX_KEY = IndexKey("kt-source-file-index") +val KT_SOURCE_FILE_META_INDEX_KEY = IndexKey("kt-source-file-meta-index") diff --git a/lsp/kotlin-compiler-impl/build.gradle.kts b/lsp/kotlin-compiler-impl/build.gradle.kts new file mode 100644 index 0000000000..20e5299b9e --- /dev/null +++ b/lsp/kotlin-compiler-impl/build.gradle.kts @@ -0,0 +1,43 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.library") + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.lsp.kotlin.compilerimpl" + + kotlin.compilerOptions { + freeCompilerArgs.addAll("-Xcontext-parameters") + } +} + +dependencies { + // The one dependency this module actually bundles -- removing it from every resident + // module's classpath is the whole point of ADFA-5010. Everything else below is + // compileOnly: those types are already loaded by the parent (main app) classloader by + // the time this module's carrier dex runs, so DexClassLoader's normal parent-first + // delegation resolves them there -- compileOnly just avoids also bundling dead-weight + // duplicate copies into the carrier dex. + implementation(projects.subprojects.kotlinAnalysisApi) + + compileOnly(projects.lsp.api) + compileOnly(projects.lsp.kotlin) + compileOnly(projects.lsp.kotlinApi) + compileOnly(projects.lsp.models) + compileOnly(projects.lsp.jvmSymbolIndex) + compileOnly(projects.editorApi) + compileOnly(projects.actions) + compileOnly(projects.shared) + compileOnly(projects.common) + compileOnly(projects.subprojects.projects) + compileOnly(projects.subprojects.projectModels) + compileOnly(projects.resources) + compileOnly(projects.idetooltips) + + implementation(libs.common.kotlin) + implementation(libs.common.kotlin.coroutines.core) + implementation(libs.common.kotlin.coroutines.android) + implementation(libs.sentry.android.core) +} diff --git a/lsp/kotlin-compiler-impl/src/main/AndroidManifest.xml b/lsp/kotlin-compiler-impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..b50865785a --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt similarity index 96% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt index ca820ac7ac..a99b7c36a4 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt @@ -6,9 +6,9 @@ import com.itsaky.androidide.actions.markInvisible import com.itsaky.androidide.actions.newDialogBuilder import com.itsaky.androidide.actions.requireFile import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.kotlin.api.DiagnosticAction import com.itsaky.androidide.lsp.kotlin.compiler.AbstractCompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.index.findSymbolBySimpleName -import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction import com.itsaky.androidide.lsp.kotlin.utils.insertImport import com.itsaky.androidide.lsp.models.CodeActionItem import com.itsaky.androidide.lsp.models.CodeActionKind @@ -60,7 +60,7 @@ class AddImportAction : BaseKotlinCodeAction() { val (env, action) = extra val nioPath = data.requireFile().toPath() return withContext(Dispatchers.IO) { - computeImportCandidates(env, nioPath, action.referenceName) + computeImportCandidates(env as AbstractCompilationEnvironment, nioPath, action.referenceName) } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt similarity index 98% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt index 69a5673035..4ecedba042 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt @@ -50,7 +50,7 @@ class ImplementMembersAction : BaseKotlinCodeAction() { val server = data.get() ?: return emptyList() val nioPath = data.requireFile().toPath() val offset = data.requireEditor().cursor.left - val env = server.compilationEnvironmentFor(nioPath) ?: return emptyList() + val env = server.compilationEnvironmentFor(nioPath) as? AbstractCompilationEnvironment ?: return emptyList() // Ties the analysis to this action's coroutine: cancelling the action aborts the queued analysis. return computeImplementMembersEdit(env, nioPath, offset, ScheduledCancelChecker(createJobCancelChecker())) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt similarity index 94% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt index 7327aa9d2d..23d67d39da 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt @@ -6,8 +6,9 @@ import com.itsaky.androidide.actions.newDialogBuilder import com.itsaky.androidide.actions.requireContext import com.itsaky.androidide.actions.requireFile import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.kotlin.api.DiagnosticAction +import com.itsaky.androidide.lsp.kotlin.compiler.AbstractCompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.read -import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction import com.itsaky.androidide.lsp.kotlin.utils.NullSafetyKind import com.itsaky.androidide.lsp.kotlin.utils.NullSafetyVariant import com.itsaky.androidide.lsp.kotlin.utils.findNullableMemberAccess @@ -60,16 +61,17 @@ class NullSafetyAction : BaseKotlinCodeAction() { ?: return emptyList() val nioPath = data.requireFile().toPath() + val env = extra.compilationEnv as AbstractCompilationEnvironment // Fetch the live KtFile BEFORE entering `read` (deadlock rule: its refresh needs write access). val ktFile = withContext(Dispatchers.IO) { - extra.compilationEnv.ktSymbolIndex + env.ktSymbolIndex .getCurrentKtFile(nioPath) .get() } ?: return emptyList() - extra.compilationEnv.project.read { + env.project.read { val qe = findNullableMemberAccess( ktFile, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt similarity index 97% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt index 1294b3259d..aee1333cb9 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt @@ -39,7 +39,7 @@ class OrganizeImportsAction : BaseKotlinCodeAction() { override suspend fun execAction(data: ActionData): List { val server = data.get() ?: return emptyList() val nioPath = data.requireFile().toPath() - val env = server.compilationEnvironmentFor(nioPath) ?: return emptyList() + val env = server.compilationEnvironmentFor(nioPath) as? AbstractCompilationEnvironment ?: return emptyList() // Ties the analysis to this action's coroutine: cancelling the action aborts the queued analysis. return computeOrganizeEdit(env, nioPath, ScheduledCancelChecker(createJobCancelChecker())) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/AbstractCompilationEnvironment.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt similarity index 87% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt index 84079924fa..72fb7d93f0 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationEnvironment.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.index.KtSymbolIndex import com.itsaky.androidide.lsp.kotlin.compiler.modules.AbstractKtModule import com.itsaky.androidide.lsp.kotlin.compiler.modules.AnalysisPreemptedException @@ -11,9 +12,20 @@ import com.itsaky.androidide.lsp.kotlin.compiler.registrar.AnalysisApiServicePro import com.itsaky.androidide.lsp.kotlin.compiler.registrar.LspAnalysisApiServiceRegistrar import com.itsaky.androidide.lsp.kotlin.compiler.services.ProjectStructureProvider import com.itsaky.androidide.lsp.kotlin.compiler.services.ResolutionScopeProvider +import com.itsaky.androidide.lsp.kotlin.completion.codeComplete import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor +import com.itsaky.androidide.lsp.kotlin.navigation.findDefinitionAt +import com.itsaky.androidide.lsp.kotlin.signaturehelp.doSignatureHelp import com.itsaky.androidide.lsp.kotlin.utils.SymbolVisibilityChecker import com.itsaky.androidide.lsp.kotlin.utils.toVirtualFileOrNull +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.lsp.models.CompletionResult +import com.itsaky.androidide.lsp.models.DefinitionParams +import com.itsaky.androidide.lsp.models.DefinitionResult +import com.itsaky.androidide.lsp.models.DiagnosticResult +import com.itsaky.androidide.lsp.models.SignatureHelp +import com.itsaky.androidide.lsp.models.SignatureHelpParams +import com.itsaky.androidide.progress.ICancelChecker import com.itsaky.androidide.projects.FileManager import com.itsaky.androidide.projects.api.Workspace import com.itsaky.androidide.utils.KeyedDebouncingAction @@ -97,7 +109,8 @@ internal class CompilationEnvironment( applicationEnvironmentMode = KotlinCoreApplicationEnvironmentMode.Production, enableParserEventSystem = enableParserEventSystem, ), - KotlinProjectModel.ProjectModelListener { + KotlinProjectModel.ProjectModelListener, + IKotlinCompilationEnvironment { companion object { val DEFAULT_FILE_MOD_EVENT_DEBOUNCE_DURATION = 400.milliseconds private val logger = LoggerFactory.getLogger(CompilationEnvironment::class.java) @@ -227,19 +240,19 @@ internal class CompilationEnvironment( ktSymbolIndex.refreshSources() } - fun openFileIfNeeded(path: Path) { + override fun openFileIfNeeded(path: Path) { fileAnalyzer.schedule(path) } - fun onFileOpen(path: Path) { + override fun onFileOpen(path: Path) { fileAnalyzer.schedule(path) } - fun onFileSaved(path: Path) { + override fun onFileSaved(path: Path) { fileAnalyzer.schedule(path) } - fun onFileClosed(path: Path) { + override fun onFileClosed(path: Path) { fileAnalyzer.cancelPending(path) refreshScheduler.cancelPending(path) ktSymbolIndex.invalidateCurrent(path) @@ -299,12 +312,12 @@ internal class CompilationEnvironment( } } - suspend fun onFileCreated(path: Path) { + override suspend fun onFileCreated(path: Path) { notifyElementModifiedForPath(path) { KaElementModificationType.ElementAdded } ktSymbolIndex.submitForIndexing(path) } - suspend fun onFileRemoved(path: Path) { + override suspend fun onFileRemoved(path: Path) { notifyElementModifiedForPath(path) { ktFile -> KaElementModificationType.ElementRemoved(ktFile) } @@ -312,7 +325,7 @@ internal class CompilationEnvironment( ktSymbolIndex.removeFromIndex(path) } - suspend fun onFileMoved( + override suspend fun onFileMoved( fromPath: Path, toPath: Path, ) { @@ -325,11 +338,22 @@ internal class CompilationEnvironment( } } - fun onFileContentChanged(path: Path) { + override fun onFileContentChanged(path: Path) { refreshScheduler.schedule(path) fileAnalyzer.schedule(path) } + override fun complete(params: CompletionParams): CompletionResult = context(this) { codeComplete(params) } + + override suspend fun findDefinition(params: DefinitionParams): DefinitionResult = context(this) { findDefinitionAt(params) } + + override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp = context(this) { doSignatureHelp(params) } + + override fun collectDiagnostics( + file: Path, + cancelChecker: ICancelChecker, + ): DiagnosticResult = context(this) { collectDiagnosticsFor(file, cancelChecker) } + override fun close() { ktProject.removeListener(this) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt similarity index 89% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt index 59777e4712..1ac943da5d 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilationKind.kt @@ -7,7 +7,6 @@ import kotlin.io.path.extension * The kind of compilation being performed in a [Compiler]. */ sealed interface CompilationKind { - /** * The types of files this compilation kind accepts. */ @@ -17,13 +16,11 @@ sealed interface CompilationKind { * Whether this compilation kind accepts the given file path. The default * implementation simply checks the accepted [fileExtensions]. */ - fun acceptsFile(path: Path): Boolean { - return path.extension in fileExtensions - } + fun acceptsFile(path: Path): Boolean = path.extension in fileExtensions /** * The default compilation kind. Mostly used for normal Kotlin source files. - */ + */ data object Default : CompilationKind { override val fileExtensions = listOf("kt") } @@ -34,4 +31,4 @@ sealed interface CompilationKind { data object Script : CompilationKind { override val fileExtensions = listOf("kts") } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/Compiler.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/CompilerExts.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt similarity index 71% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt index 032341591d..ae322a91a1 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/IncrementalModificationTracker.kt @@ -4,19 +4,17 @@ import org.jetbrains.kotlin.com.intellij.openapi.util.ModificationTracker import java.util.concurrent.atomic.AtomicLong class IncrementalModificationTracker : ModificationTracker { - private val myCounter = AtomicLong(0) /** * Increment the modification count. */ - fun incModificationCount() = apply { - myCounter.incrementAndGet() - } + fun incModificationCount() = + apply { + myCounter.incrementAndGet() + } operator fun inc() = incModificationCount() - override fun getModificationCount(): Long { - return myCounter.get() - } -} \ No newline at end of file + override fun getModificationCount(): Long = myCounter.get() +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt index ea0039e34b..206a48ec4a 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinApplicationEnvironmentPin.kt @@ -1,13 +1,12 @@ package com.itsaky.androidide.lsp.kotlin.compiler import org.jetbrains.kotlin.K1Deprecation -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.Companion.getOrCreateApplicationEnvironmentForProduction as acquireApplicationEnvironment import org.jetbrains.kotlin.com.intellij.openapi.util.Disposer import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.Companion.getOrCreateApplicationEnvironmentForProduction as acquireApplicationEnvironment @OptIn(K1Deprecation::class) internal object KotlinApplicationEnvironmentPin { - private var pinned = false private val pin = Disposer.newDisposable("kotlin-lsp-application-environment-pin") diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionFactoryImpl.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionFactoryImpl.kt new file mode 100644 index 0000000000..aef1490633 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionFactoryImpl.kt @@ -0,0 +1,44 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSession +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSessionFactory +import com.itsaky.androidide.projects.api.Workspace +import org.jetbrains.kotlin.config.JvmTarget +import org.jetbrains.kotlin.config.LanguageVersion +import org.jetbrains.kotlin.platform.jvm.JvmPlatforms +import java.nio.file.Path + +/** + * Reflection entry point loaded by name from the carrier APK's DexClassLoader (see + * `KotlinCompilerLoader` in `lsp:kotlin`). Requires a public no-arg constructor. + */ +class KotlinCompilerSessionFactoryImpl : IKotlinCompilerSessionFactory { + override fun create( + workspace: Workspace, + intellijPluginRoot: Path, + jdkHome: Path, + jdkRelease: Int, + jdkVersionString: String, + languageClient: ILanguageClient?, + ): IKotlinCompilerSession { + val jvmTarget = JvmTarget.fromString(jdkVersionString) ?: JvmTarget.JVM_21 + val jvmPlatform = JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) + + val projectModel = KotlinProjectModel() + projectModel.update(workspace, jvmPlatform) + + val compiler = + Compiler( + workspace = workspace, + projectModel = projectModel, + intellijPluginRoot = intellijPluginRoot, + jdkHome = jdkHome, + jdkRelease = jdkRelease, + languageVersion = LanguageVersion.LATEST_STABLE, + ) + compiler.updateLanguageClient(languageClient) + + return KotlinCompilerSessionImpl(compiler, projectModel, jvmPlatform) + } +} diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt new file mode 100644 index 0000000000..a775b3ade1 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt @@ -0,0 +1,46 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.kotlin.KotlinCodeActionsMenu +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSession +import com.itsaky.androidide.lsp.util.LSPEditorActions +import com.itsaky.androidide.projects.api.Workspace +import org.jetbrains.kotlin.platform.TargetPlatform +import org.jetbrains.kotlin.platform.jvm.JvmPlatform +import java.nio.file.Path + +/** Wraps [Compiler] behind [IKotlinCompilerSession] for the resident side to call across the DexClassLoader boundary. */ +internal class KotlinCompilerSessionImpl( + private val compiler: Compiler, + private val projectModel: KotlinProjectModel, + private val jvmPlatform: TargetPlatform, +) : IKotlinCompilerSession { + override fun updateLanguageClient(client: ILanguageClient?) { + compiler.updateLanguageClient(client) + } + + override fun updateProjectModel(workspace: Workspace) { + projectModel.update(workspace, jvmPlatform) + } + + override fun refreshSources() { + compiler.refreshSources() + } + + override fun compilationEnvironmentFor(file: Path): IKotlinCompilationEnvironment? = compiler.compilationEnvironmentFor(file) + + override fun registerCodeActions() { + LSPEditorActions.ensureActionsMenuRegistered(KotlinCodeActionsMenu) + } + + override fun openFileIfNeeded(activeDocumentFiles: List) { + activeDocumentFiles.forEach { file -> + compiler.compilationEnvironmentFor(file)?.openFileIfNeeded(file) + } + } + + override fun close() { + compiler.close() + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt similarity index 76% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt index f109360c3e..ed9ff56a81 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinProjectModel.kt @@ -1,7 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.compiler -import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_INDEX_KEY -import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_META_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_META_INDEX_KEY import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.api.Workspace import org.appdevforall.codeonthego.indexing.jvm.JVM_GENERATED_SYMBOL_INDEX @@ -24,7 +24,6 @@ import org.slf4j.LoggerFactory * refresh their sessions. */ internal class KotlinProjectModel { - private val logger = LoggerFactory.getLogger(KotlinProjectModel::class.java) private var workspace: Workspace? = null @@ -33,31 +32,36 @@ internal class KotlinProjectModel { private val listeners = mutableListOf() val libraryIndex: JvmSymbolIndex? - get() = ProjectManagerImpl.getInstance() - .indexingServiceManager - .registry - .get(JVM_LIBRARY_SYMBOL_INDEX) + get() = + ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(JVM_LIBRARY_SYMBOL_INDEX) val sourceIndex: JvmSymbolIndex? - get() = ProjectManagerImpl - .getInstance() - .indexingServiceManager - .registry - .get(KT_SOURCE_FILE_INDEX_KEY) + get() = + ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(KT_SOURCE_FILE_INDEX_KEY) val fileIndex: KtFileMetadataIndex? - get() = ProjectManagerImpl - .getInstance() - .indexingServiceManager - .registry - .get(KT_SOURCE_FILE_META_INDEX_KEY) + get() = + ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(KT_SOURCE_FILE_META_INDEX_KEY) val generatedIndex: JvmSymbolIndex? - get() = ProjectManagerImpl - .getInstance() - .indexingServiceManager - .registry - .get(JVM_GENERATED_SYMBOL_INDEX) + get() = + ProjectManagerImpl + .getInstance() + .indexingServiceManager + .registry + .get(JVM_GENERATED_SYMBOL_INDEX) /** * The kind of change that occurred. @@ -71,7 +75,10 @@ internal class KotlinProjectModel { } fun interface ProjectModelListener { - fun onProjectModelChanged(model: KotlinProjectModel, changeKind: ChangeKind) + fun onProjectModelChanged( + model: KotlinProjectModel, + changeKind: ChangeKind, + ) } fun addListener(listener: ProjectModelListener) { @@ -86,7 +93,10 @@ internal class KotlinProjectModel { * Called when the project is synced (setupWithProject). * This replaces the entire project structure. */ - fun update(workspace: Workspace, platform: TargetPlatform) { + fun update( + workspace: Workspace, + platform: TargetPlatform, + ) { this.workspace = workspace this.platform = platform notifyListeners(ChangeKind.STRUCTURE) @@ -108,4 +118,4 @@ internal class KotlinProjectModel { logger.info("Notifying project listeners for change: {}", changeKind) listeners.forEach { it.onProjectModelChanged(this, changeKind) } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt index bd9c000672..6e3213e410 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/ReadWriteLock.kt @@ -21,4 +21,4 @@ fun Project.read(fn: () -> T): T { fun Project.write(fn: () -> T): T { val lock = getUserData(key)!! return lock.write(fn) -} \ No newline at end of file +} diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt new file mode 100644 index 0000000000..f1c5c8bb78 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt @@ -0,0 +1,100 @@ +package com.itsaky.androidide.lsp.kotlin.compiler + +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtLibraryModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtSourceModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtLibraryModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtSourceModule +import com.itsaky.androidide.projects.api.AndroidModule +import com.itsaky.androidide.projects.api.ModuleProject +import com.itsaky.androidide.projects.api.Workspace +import com.itsaky.androidide.projects.models.bootClassPaths +import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.slf4j.LoggerFactory +import java.nio.file.Path +import kotlin.io.path.pathString + +private val logger = LoggerFactory.getLogger("WorkspaceExts") + +internal fun Workspace.collectKtModules( + project: Project, + appEnv: CoreApplicationEnvironment, +): List = + buildList { + fun addModule(module: KtModule) = add(module) + + val moduleProjects = + subProjects + .asSequence() + .filterIsInstance() + .filter { it.path != rootProject.path } + + val jarToModMap = mutableMapOf() + + fun addLibrary(path: Path): KtLibraryModule { + val module = + buildKtLibraryModule(project, appEnv) { + id = path.pathString + addContentRoot(path) + } + jarToModMap[path] = module + return module + } + + val bootClassPaths = + moduleProjects + .filterIsInstance() + .flatMap { project -> + project.bootClassPaths + .asSequence() + .filter { it.exists() } + .map { it.toPath() } + .map(::addLibrary) + } + + val libraryDependencies = + moduleProjects + .flatMap { it.getCompileClasspaths() } + .filter { it.exists() } + .map { it.toPath() } + .associateWith(::addLibrary) + + val subprojectsAsModules = mutableMapOf() + val sourceRootToModuleMap = mutableMapOf() + + fun getOrCreateModule(moduleProject: ModuleProject): KtSourceModule { + subprojectsAsModules[moduleProject]?.let { return it } + + val module = + buildKtSourceModule(project) { + this.module = moduleProject + + bootClassPaths.forEach { addDependency(it) } + + moduleProject + .getCompileClasspaths(excludeSourceGeneratedClassPath = true) + .forEach { classpath -> + val libDep = libraryDependencies[classpath.toPath()] + if (libDep == null) { + logger.error( + "Skipping non-existent classpath classpath: {}", + classpath, + ) + return@forEach + } + addDependency(libDep) + } + + moduleProject.getCompileModuleProjects().forEach { dep -> + addDependency(getOrCreateModule(dep)) + } + } + + subprojectsAsModules[moduleProject] = module + module.contentRoots.forEach { root -> sourceRootToModuleMap[root] = module } + return module + } + + moduleProjects.forEach { addModule(getOrCreateModule(it)) } + } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt new file mode 100644 index 0000000000..fa7fc510ad --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt @@ -0,0 +1,29 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.psi.KtFile +import java.nio.file.Path + +internal sealed interface IndexCommand { + data object Stop : IndexCommand + + data object SourceScanningComplete : IndexCommand + + data object IndexingComplete : IndexCommand + + data class ScanSourceFile( + val vf: VirtualFile, + ) : IndexCommand + + data class IndexModifiedFile( + val ktFile: KtFile, + ) : IndexCommand + + data class IndexSourceFile( + val vf: VirtualFile, + ) : IndexCommand + + data class RemoveFromIndex( + val path: Path, + ) : IndexCommand +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexWorker.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt similarity index 98% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt index 2b36a45cd7..cf7c04b32f 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/KtSymbolIndex.kt @@ -1,6 +1,8 @@ package com.itsaky.androidide.lsp.kotlin.compiler.index import com.github.benmanes.caffeine.cache.Caffeine +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_META_INDEX_KEY import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule import com.itsaky.androidide.lsp.kotlin.compiler.modules.backingFilePath @@ -20,7 +22,6 @@ import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex -import org.appdevforall.codeonthego.indexing.service.IndexKey import org.checkerframework.checker.index.qual.NonNegative import org.jetbrains.kotlin.analysis.api.platform.modification.KaElementModificationType import org.jetbrains.kotlin.analysis.api.platform.modification.KaSourceModificationService @@ -39,9 +40,6 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import kotlin.io.path.pathString -val KT_SOURCE_FILE_INDEX_KEY = IndexKey("kt-source-file-index") -val KT_SOURCE_FILE_META_INDEX_KEY = IndexKey("kt-source-file-meta-index") - /** * An index of symbols from Kotlin source files and JARs. * diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt new file mode 100644 index 0000000000..9c21290945 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt @@ -0,0 +1,62 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.index + +import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind +import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule +import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence +import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule +import com.itsaky.androidide.lsp.kotlin.utils.toNioPathOrNull +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.isActive +import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex +import org.slf4j.LoggerFactory + +internal class ScanningWorker( + private val kind: CompilationKind, + private val sourceIndex: JvmSymbolIndex, + private val indexWorker: IndexWorker, + private val modules: List, +) { + companion object { + private val logger = LoggerFactory.getLogger(ScanningWorker::class.java) + } + + suspend fun scan() = + coroutineScope { + val sourceFiles = + modules + .asFlatSequence() + .filter { it.isSourceModule } + .flatMap { it.computeFiles(extended = true) } + .filter { + it.toNioPathOrNull()?.let { path -> kind.acceptsFile(path) } ?: run { + logger.warn("rejecting {} from kt source index", it) + false + } + }.takeWhile { isActive } + .toList() + + sourceIndex.setActiveSources( + sourceFiles + .asSequence() + .map { it.path } + .takeWhile { isActive } + .toSet(), + ) + + for (sourceFile in sourceFiles) { + if (!isActive) return@coroutineScope + indexWorker.submitCommand(IndexCommand.ScanSourceFile(sourceFile)) + } + + indexWorker.submitCommand(IndexCommand.SourceScanningComplete) + + sourceFiles + .asSequence() + .takeWhile { isActive } + .forEach { sourceFile -> + indexWorker.submitCommand(IndexCommand.IndexSourceFile(sourceFile)) + } + + indexWorker.submitCommand(IndexCommand.IndexingComplete) + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/SourceFileIndexer.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt similarity index 96% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt index 186a0cf6fa..9cd84c0b2a 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/WorkerQueue.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.selects.select internal class WorkerQueue { - private val scanChannel = Channel(capacity = 100) private val editChannel = Channel(capacity = 20) private val indexChannel = Channel(capacity = 100) @@ -15,7 +14,9 @@ internal class WorkerQueue { private var pushedBack: T? = null suspend fun putScanQueue(item: T) = scanChannel.send(item) + suspend fun putEditQueue(item: T) = editChannel.send(item) + suspend fun putIndexQueue(item: T) = indexChannel.send(item) /** @@ -40,7 +41,10 @@ internal class WorkerQueue { } suspend fun take(): T { - pushedBack?.let { pushedBack = null; return it } + pushedBack?.let { + pushedBack = null + return it + } scanChannel.tryReceive().getOrNull()?.let { return it } editChannel.tryReceive().getOrNull()?.let { return it } @@ -52,4 +56,4 @@ internal class WorkerQueue { indexChannel.onReceive { it } } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt index 9c1f603ef0..9bd2f128cb 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt @@ -75,4 +75,4 @@ internal abstract class AbstractKtModule( override fun toString(): String { return id } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt similarity index 98% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt index 7ffef26605..431875e66a 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractSourceModule.kt @@ -13,9 +13,8 @@ import kotlin.io.path.walk internal abstract class AbstractSourceModule( project: Project, - directRegularDependencies: List + directRegularDependencies: List, ) : AbstractKtModule(project, directRegularDependencies) { - override fun computeFiles(extended: Boolean): Sequence = contentRoots .asSequence() @@ -55,4 +54,4 @@ internal abstract class AbstractSourceModule( override fun isSearchInLibraries(): Boolean = false } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisScheduler.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AnalysisThreadContext.java diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/CancelCheckerProgressIndicator.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtFileExts.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt similarity index 84% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt index aaca94149e..1d9456e375 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtLibraryModule.kt @@ -36,12 +36,11 @@ internal class KtLibraryModule( override val isSdk: Boolean = false, private val jvmTarget: JvmTarget = DEFAULT_JVM_TARGET, override val librarySources: KaLibrarySourceModule? = null, -) : KaLibraryModule, - AbstractKtModule( +) : AbstractKtModule( project, - dependencies - ) { - + dependencies, + ), + KaLibraryModule { class Builder( private val project: Project, private val applicationEnvironment: CoreApplicationEnvironment, @@ -61,31 +60,37 @@ internal class KtLibraryModule( dependencies.add(dep) } - fun build(): KtLibraryModule = KtLibraryModule( - project = project, - id = id, - contentRoots = contentRoots.toSet(), - dependencies = dependencies.toList(), - applicationEnvironment = applicationEnvironment, - isSdk = isSdk, - jvmTarget = jvmTarget, - librarySources = librarySources, - ) + fun build(): KtLibraryModule = + KtLibraryModule( + project = project, + id = id, + contentRoots = contentRoots.toSet(), + dependencies = dependencies.toList(), + applicationEnvironment = applicationEnvironment, + isSdk = isSdk, + jvmTarget = jvmTarget, + librarySources = librarySources, + ) } @OptIn(KaImplementationDetail::class) override fun computeFiles(extended: Boolean): Sequence { - val roots = if (isSdk) project.read { - LibraryUtils.findClassesFromJdkHome( - contentRoots.first(), - isJre = false - ) - } - else contentRoots + val roots = + if (isSdk) { + project.read { + LibraryUtils.findClassesFromJdkHome( + contentRoots.first(), + isJre = false, + ) + } + } else { + contentRoots + } - val notExtendedFiles = roots - .asSequence() - .mapNotNull { getVirtualFileForLibraryRoot(it, applicationEnvironment, project) } + val notExtendedFiles = + roots + .asSequence() + .mapNotNull { getVirtualFileForLibraryRoot(it, applicationEnvironment, project) } if (!extended) return notExtendedFiles @@ -97,17 +102,11 @@ internal class KtLibraryModule( override val baseContentScope: GlobalSearchScope by lazy { val virtualFileUrls = computeFiles(extended = true).map { it.url }.toSet() object : GlobalSearchScope(project) { - override fun contains(vf: VirtualFile): Boolean { - return vf.url in virtualFileUrls - } + override fun contains(vf: VirtualFile): Boolean = vf.url in virtualFileUrls - override fun isSearchInModuleContent(module: Module): Boolean { - return false - } + override fun isSearchInModuleContent(module: Module): Boolean = false - override fun isSearchInLibraries(): Boolean { - return true - } + override fun isSearchInLibraries(): Boolean = true } } @@ -136,6 +135,7 @@ internal fun buildKtLibraryModule( ): KtLibraryModule = KtLibraryModule.Builder(project, applicationEnvironment).apply(init).build() private const val JAR_SEPARATOR = "!/" + private fun getVirtualFileForLibraryRoot( root: Path, environment: CoreApplicationEnvironment, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtModule.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt similarity index 75% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt index cb62f1cf90..0cb3869bf2 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/KtSourceModule.kt @@ -21,13 +21,15 @@ internal class KtSourceModule( project: Project, val module: ModuleProject, directRegularDependencies: List, -) : KaSourceModule, AbstractSourceModule(project, directRegularDependencies) { - +) : AbstractSourceModule(project, directRegularDependencies), + KaSourceModule { companion object { private val logger = LoggerFactory.getLogger(KtSourceModule::class.java) } - class Builder(private val project: Project) { + class Builder( + private val project: Project, + ) { lateinit var module: ModuleProject private val dependencies = mutableListOf() @@ -42,29 +44,38 @@ internal class KtSourceModule( get() = module.path override val contentRoots by lazy { - module.getSourceDirectories() + module + .getSourceDirectories() .asSequence() .map { it.toPath() } .toSet() } private val versions by lazy { - val kotlinCompilerSettings = when { - module.hasJavaProject() -> module.javaProject - .kotlinCompilerSettings - - module.hasAndroidProject() -> module.androidProject - .kotlinCompilerSettings - - else -> null - } + val kotlinCompilerSettings = + when { + module.hasJavaProject() -> { + module.javaProject + .kotlinCompilerSettings + } + + module.hasAndroidProject() -> { + module.androidProject + .kotlinCompilerSettings + } + + else -> { + null + } + } if (kotlinCompilerSettings == null) { return@lazy DEFAULT_LANGUAGE_VERSION to DEFAULT_JVM_TARGET } - val apiVersion = LanguageVersion.fromVersionString(kotlinCompilerSettings.apiVersion) - ?: LanguageVersion.fromFullVersionString(kotlinCompilerSettings.apiVersion) + val apiVersion = + LanguageVersion.fromVersionString(kotlinCompilerSettings.apiVersion) + ?: LanguageVersion.fromFullVersionString(kotlinCompilerSettings.apiVersion) val jvmTarget = JvmTarget.fromString(kotlinCompilerSettings.jvmTarget) @@ -79,18 +90,17 @@ internal class KtSourceModule( get() = super.moduleDescription override val languageVersionSettings: LanguageVersionSettings - get() = LanguageVersionSettingsImpl( - languageVersion = versions.first, - apiVersion = ApiVersion.createByLanguageVersion(versions.first), - ) + get() = + LanguageVersionSettingsImpl( + languageVersion = versions.first, + apiVersion = ApiVersion.createByLanguageVersion(versions.first), + ) override val targetPlatform: TargetPlatform get() = JvmPlatforms.jvmPlatformByTargetVersion(versions.second) - - } internal fun buildKtSourceModule( project: Project, init: KtSourceModule.Builder.() -> Unit, -): KtSourceModule = KtSourceModule.Builder(project).apply(init).build() \ No newline at end of file +): KtSourceModule = KtSourceModule.Builder(project).apply(init).build() diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt similarity index 87% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt index 0de0b4282c..6fea4fbdf7 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/NotUnderContentRootModule.kt @@ -19,9 +19,11 @@ internal class NotUnderContentRootModule( directRegularDependencies: List = emptyList(), override val targetPlatform: TargetPlatform = JvmPlatforms.defaultJvmPlatform, override val file: PsiFile? = null, -) : KaNotUnderContentRootModule, AbstractKtModule( - project, directRegularDependencies -) { +) : AbstractKtModule( + project, + directRegularDependencies, + ), + KaNotUnderContentRootModule { override val name: String get() = id @@ -31,10 +33,11 @@ internal class NotUnderContentRootModule( override val contentRoots: Set get() = file?.virtualFile?.toNioPath()?.let(::setOf) ?: emptySet() - override fun computeFiles(extended: Boolean): Sequence = sequence { - val vf = file?.virtualFile - if (vf != null) { - yield(vf) + override fun computeFiles(extended: Boolean): Sequence = + sequence { + val vf = file?.virtualFile + if (vf != null) { + yield(vf) + } } - } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt similarity index 57% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt index b3f7c0a841..d0af144b89 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProvider.kt @@ -7,38 +7,49 @@ internal typealias ServiceMap = Map, ServiceRegistration<*>> internal typealias MutableServiceMap = MutableMap, ServiceRegistration<*>> internal sealed class ServiceRegistration { - companion object { - operator fun invoke(klass: KClass, type: KClass) = - Typed(klass, type) - - operator fun invoke(klass: KClass, instance: T) = - Instance(klass, instance) - - operator fun invoke(klass: KClass, factory: () -> T) = - InstanceFactory(klass, factory) + operator fun invoke( + klass: KClass, + type: KClass, + ) = Typed(klass, type) + + operator fun invoke( + klass: KClass, + instance: T, + ) = Instance(klass, instance) + + operator fun invoke( + klass: KClass, + factory: () -> T, + ) = InstanceFactory(klass, factory) } abstract val klass: KClass abstract fun register(to: MockComponentManager) - data class Typed(override val klass: KClass, val type: KClass) : - ServiceRegistration() { + data class Typed( + override val klass: KClass, + val type: KClass, + ) : ServiceRegistration() { override fun register(to: MockComponentManager) { to.registerService(klass.java, type.java) } } - data class Instance(override val klass: KClass, val instance: T) : - ServiceRegistration() { + data class Instance( + override val klass: KClass, + val instance: T, + ) : ServiceRegistration() { override fun register(to: MockComponentManager) { to.registerService(klass.java, instance) } } - data class InstanceFactory(override val klass: KClass, val factory: () -> T) : - ServiceRegistration() { + data class InstanceFactory( + override val klass: KClass, + val factory: () -> T, + ) : ServiceRegistration() { override fun register(to: MockComponentManager) { val instance = factory() to.registerService(klass.java, instance) @@ -52,11 +63,12 @@ internal interface AnalysisApiServiceProvider { val applicationServices: ServiceMap val projectServices: ServiceMap - fun toBuilder(): Builder = Builder( - pluginRelativePath = pluginRelativePath, - appServices = applicationServices, - projectServices = projectServices, - ) + fun toBuilder(): Builder = + Builder( + pluginRelativePath = pluginRelativePath, + appServices = applicationServices, + projectServices = projectServices, + ) private data class SimpleAnalysisApiServiceProvider( override val pluginRelativePath: String?, @@ -81,7 +93,7 @@ internal interface AnalysisApiServiceProvider { store: MutableServiceMap, key: KClass<*>, value: ServiceRegistration<*>, - replace: Boolean + replace: Boolean, ) { if (!replace) { check(store.putIfAbsent(key, value) == null) { @@ -95,11 +107,15 @@ internal interface AnalysisApiServiceProvider { } private fun appSvc( - key: KClass<*>, value: ServiceRegistration<*>, replace: Boolean = false + key: KClass<*>, + value: ServiceRegistration<*>, + replace: Boolean = false, ) = put(applicationServices, key, value, replace) private fun projSvc( - key: KClass<*>, value: ServiceRegistration<*>, replace: Boolean = false + key: KClass<*>, + value: ServiceRegistration<*>, + replace: Boolean = false, ) = put(projectServices, key, value, replace) fun appService( @@ -108,11 +124,17 @@ internal interface AnalysisApiServiceProvider { replace: Boolean = false, ) = appSvc(key, ServiceRegistration(key, type), replace) - fun appService(key: KClass, value: T, replace: Boolean = false) = - appSvc(key, ServiceRegistration(key, value), replace) + fun appService( + key: KClass, + value: T, + replace: Boolean = false, + ) = appSvc(key, ServiceRegistration(key, value), replace) - fun appService(key: KClass, replace: Boolean = false, value: () -> T) = - appSvc(key, ServiceRegistration(key, value), replace) + fun appService( + key: KClass, + replace: Boolean = false, + value: () -> T, + ) = appSvc(key, ServiceRegistration(key, value), replace) fun projectService( key: KClass, @@ -120,16 +142,23 @@ internal interface AnalysisApiServiceProvider { replace: Boolean = false, ) = projSvc(key, ServiceRegistration(key, type), replace) - fun projectService(key: KClass, value: T, replace: Boolean = false) = - projSvc(key, ServiceRegistration(key, value), replace) - - fun projectService(key: KClass, replace: Boolean = false, value: () -> T) = - projSvc(key, ServiceRegistration(key, value), replace) + fun projectService( + key: KClass, + value: T, + replace: Boolean = false, + ) = projSvc(key, ServiceRegistration(key, value), replace) - fun build(): AnalysisApiServiceProvider = SimpleAnalysisApiServiceProvider( - pluginRelativePath = pluginRelativePath, - applicationServices = applicationServices.toMap(), - projectServices = projectServices.toMap(), - ) + fun projectService( + key: KClass, + replace: Boolean = false, + value: () -> T, + ) = projSvc(key, ServiceRegistration(key, value), replace) + + fun build(): AnalysisApiServiceProvider = + SimpleAnalysisApiServiceProvider( + pluginRelativePath = pluginRelativePath, + applicationServices = applicationServices.toMap(), + projectServices = projectServices.toMap(), + ) } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt similarity index 65% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt index c115caf114..809889a275 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/AnalysisApiServiceProviders.kt @@ -33,30 +33,32 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.smartPointers.SmartTypePointer @Suppress("UnstableApiUsage") @OptIn(KaImplementationDetail::class) internal object AnalysisApiServiceProviders { + val BaseProvider = + AnalysisApiServiceProvider + .Builder() + .apply { + pluginRelativePath = "/META-INF/kt-lsp/kt-lsp.xml" - val BaseProvider = AnalysisApiServiceProvider.Builder().apply { - pluginRelativePath = "/META-INF/kt-lsp/kt-lsp.xml" + appService(KotlinAnalysisPermissionOptions::class, AnalysisPermissionOptions::class) + appService(AsyncExecutionService::class, NoOpAsyncExecutionService::class) - appService(KotlinAnalysisPermissionOptions::class, AnalysisPermissionOptions::class) - appService(AsyncExecutionService::class, NoOpAsyncExecutionService::class) + projectService( + KotlinLifetimeTokenFactory::class, + KotlinReadActionConfinementLifetimeTokenFactory::class, + ) - projectService( - KotlinLifetimeTokenFactory::class, - KotlinReadActionConfinementLifetimeTokenFactory::class - ) - - projectService(KotlinPlatformSettings::class, PlatformSettings::class) - projectService(SmartTypePointerManager::class, SmartTypePointerManagerImpl::class) - projectService(KotlinProjectStructureProvider::class, ProjectStructureProvider::class) - projectService(KotlinModuleDependentsProvider::class, ModuleDependentsProvider::class) - projectService(KotlinModificationTrackerFactory::class, ModificationTrackerFactory::class) - projectService(KotlinAnnotationsResolverFactory::class, AnnotationsResolverFactory::class) - projectService(KotlinDeclarationProviderMerger::class, DeclarationProviderMerger::class) - projectService(KotlinPackageProviderMerger::class, PackageProviderMerger::class) - projectService(KotlinPackagePartProviderFactory::class, PackagePartProviderFactory::class) - projectService(KotlinPackageProviderFactory::class, PackageProviderFactory::class) - projectService(KotlinDeclarationProviderFactory::class, DeclarationProviderFactory::class) - }.build() + projectService(KotlinPlatformSettings::class, PlatformSettings::class) + projectService(SmartTypePointerManager::class, SmartTypePointerManagerImpl::class) + projectService(KotlinProjectStructureProvider::class, ProjectStructureProvider::class) + projectService(KotlinModuleDependentsProvider::class, ModuleDependentsProvider::class) + projectService(KotlinModificationTrackerFactory::class, ModificationTrackerFactory::class) + projectService(KotlinAnnotationsResolverFactory::class, AnnotationsResolverFactory::class) + projectService(KotlinDeclarationProviderMerger::class, DeclarationProviderMerger::class) + projectService(KotlinPackageProviderMerger::class, PackageProviderMerger::class) + projectService(KotlinPackagePartProviderFactory::class, PackagePartProviderFactory::class) + projectService(KotlinPackageProviderFactory::class, PackageProviderFactory::class) + projectService(KotlinDeclarationProviderFactory::class, DeclarationProviderFactory::class) + }.build() val Production = BaseProvider -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt similarity index 92% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt index 92454ac27d..4b3bdaccab 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/registrar/LspAnalysisApiServiceRegistrar.kt @@ -14,8 +14,7 @@ import org.jetbrains.kotlin.com.intellij.psi.impl.PsiElementFinderImpl @OptIn(KaImplementationDetail::class) internal class LspAnalysisApiServiceRegistrar( private val provider: AnalysisApiServiceProvider, -): AnalysisApiSimpleServiceRegistrar() { - +) : AnalysisApiSimpleServiceRegistrar() { @Suppress("UNCHECKED_CAST") private fun MockComponentManager.servAll(services: ServiceMap) { services.forEach { (_, registration) -> @@ -39,10 +38,13 @@ internal class LspAnalysisApiServiceRegistrar( project.servAll(provider.projectServices) } - override fun registerProjectModelServices(project: MockProject, disposable: Disposable) { + override fun registerProjectModelServices( + project: MockProject, + disposable: Disposable, + ) { with(PsiElementFinder.EP.getPoint(project)) { registerExtension(JavaElementFinder(project), disposable) registerExtension(PsiElementFinderImpl(project), disposable) } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt similarity index 89% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt index 6dbe0e491e..1608618e03 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnalysisPermissionOptions.kt @@ -5,4 +5,4 @@ import org.jetbrains.kotlin.analysis.api.platform.permissions.KotlinAnalysisPerm class AnalysisPermissionOptions( override val defaultIsAnalysisAllowedOnEdt: Boolean = false, override val defaultIsAnalysisAllowedInWriteAction: Boolean = true, -) : KotlinAnalysisPermissionOptions \ No newline at end of file +) : KotlinAnalysisPermissionOptions diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt similarity index 84% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt index 0d04ab5888..93a48be988 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/AnnotationsResolver.kt @@ -25,8 +25,9 @@ import org.jetbrains.kotlin.psi.KtUserType import org.jetbrains.kotlin.psi.declarationRecursiveVisitor import org.jetbrains.kotlin.util.collectionUtils.filterIsInstanceAnd -internal class AnnotationsResolverFactory : KtLspService, KotlinAnnotationsResolverFactory { - +internal class AnnotationsResolverFactory : + KtLspService, + KotlinAnnotationsResolverFactory { private lateinit var project: Project private lateinit var index: KtSymbolIndex @@ -34,15 +35,14 @@ internal class AnnotationsResolverFactory : KtLspService, KotlinAnnotationsResol project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.project = project this.index = index } - override fun createAnnotationResolver(searchScope: GlobalSearchScope): KotlinAnnotationsResolver { - return AnnotationsResolver(project, searchScope, index) - } + override fun createAnnotationResolver(searchScope: GlobalSearchScope): KotlinAnnotationsResolver = + AnnotationsResolver(project, searchScope, index) } @Suppress("UnstableApiUsage") @@ -51,7 +51,6 @@ internal class AnnotationsResolver( private val scope: GlobalSearchScope, private val index: KtSymbolIndex, ) : KotlinAnnotationsResolver { - private val declarationProvider by lazy { project.createDeclarationProvider(scope, contextualModule = null) } @@ -59,50 +58,51 @@ internal class AnnotationsResolver( private fun allDeclarations(): List { val virtualFiles = VirtualFileEnumeration.extract(scope) ?: return emptyList() - val filesInScope = virtualFiles - .filesIfCollection - .orEmpty() - .asSequence() - .filter { it in scope } - .mapNotNull { index.getKtFile(it) } + val filesInScope = + virtualFiles + .filesIfCollection + .orEmpty() + .asSequence() + .filter { it in scope } + .mapNotNull { index.getKtFile(it) } return buildList { - val visitor = declarationRecursiveVisitor visit@{ - val isLocal = when (it) { - is KtClassOrObject -> it.isLocal - is KtFunction -> it.isLocal - is KtProperty -> it.isLocal - else -> return@visit - } - - if (!isLocal) { - add(it) + val visitor = + declarationRecursiveVisitor visit@{ + val isLocal = + when (it) { + is KtClassOrObject -> it.isLocal + is KtFunction -> it.isLocal + is KtProperty -> it.isLocal + else -> return@visit + } + + if (!isLocal) { + add(it) + } } - } filesInScope.forEach { it.accept(visitor) } } } - override fun declarationsByAnnotation(annotationClassId: ClassId): Set { - return allDeclarations() + override fun declarationsByAnnotation(annotationClassId: ClassId): Set = + allDeclarations() .asSequence() .filter { annotationClassId in annotationsOnDeclaration(it) } .toSet() - } - override fun annotationsOnDeclaration(declaration: KtAnnotated): Set { - return declaration + override fun annotationsOnDeclaration(declaration: KtAnnotated): Set = + declaration .annotationEntries .asSequence() .flatMap { it.typeReference?.resolveAnnotationClassIds(declarationProvider).orEmpty() } .toSet() - } } private fun KtTypeReference.resolveAnnotationClassIds( declarationProvider: KotlinDeclarationProvider, - candidates: MutableSet = mutableSetOf() + candidates: MutableSet = mutableSetOf(), ): Set { val annotationTypeElement = typeElement as? KtUserType val referencedName = annotationTypeElement?.referencedFqName ?: return emptySet() @@ -132,8 +132,10 @@ private val KtUserType.referencedFqName: FqName? return FqName.fromSegments(allQualifiers) } - -private fun FqName.resolveToClassIds(to: MutableSet, declarationProvider: KotlinDeclarationProvider) { +private fun FqName.resolveToClassIds( + to: MutableSet, + declarationProvider: KotlinDeclarationProvider, +) { toClassIdSequence().mapNotNullTo(to) { classId -> val classes = declarationProvider.getAllClassesByClassId(classId) val typeAliases = declarationProvider.getAllTypeAliasesByClassId(classId) @@ -162,4 +164,3 @@ private fun FqName.toClassIdSequence(): Sequence { } } } - diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt similarity index 84% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt index b8e3498a21..239f24d492 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DeclarationsProvider.kt @@ -33,8 +33,9 @@ import org.jetbrains.kotlin.psi.KtTypeAlias import org.jetbrains.kotlin.psi.psiUtil.isTopLevelKtOrJavaMember import java.nio.file.Paths -internal class DeclarationProviderFactory : KtLspService, KotlinDeclarationProviderFactory { - +internal class DeclarationProviderFactory : + KtLspService, + KotlinDeclarationProviderFactory { private lateinit var project: Project private lateinit var index: KtSymbolIndex @@ -42,7 +43,7 @@ internal class DeclarationProviderFactory : KtLspService, KotlinDeclarationProvi project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.project = project this.index = index @@ -50,13 +51,13 @@ internal class DeclarationProviderFactory : KtLspService, KotlinDeclarationProvi override fun createDeclarationProvider( scope: GlobalSearchScope, - contextualModule: KaModule? - ): KotlinDeclarationProvider { - return DeclarationProvider(scope, project, index) - } + contextualModule: KaModule?, + ): KotlinDeclarationProvider = DeclarationProvider(scope, project, index) } -class DeclarationProviderMerger(private val project: Project) : KotlinDeclarationProviderMerger { +class DeclarationProviderMerger( + private val project: Project, +) : KotlinDeclarationProviderMerger { override fun merge(providers: List): KotlinDeclarationProvider = providers.mergeSpecificProviders<_, DeclarationProvider>(KotlinCompositeDeclarationProvider.factory) { targetProviders -> val combinedScope = GlobalSearchScope.union(targetProviders.map { it.scope }) @@ -81,13 +82,12 @@ internal abstract class AbstractDeclarationProvider( } override fun findInternalFilesForFacade(facadeFqName: FqName): Collection = - // We don't deserialize libraries from stubs so we can return empty here safely - // We don't take the KaBuiltinsModule into account for simplicity, + // We don't deserialize libraries from stubs so we can return empty here safely + // We don't take the KaBuiltinsModule into account for simplicity, // that means we expect the kotlin stdlib to be included on the project emptyList() - override fun findFilesForFacadeByPackage(packageFqName: FqName): Collection = - ktFilesForPackage(packageFqName).toList() + override fun findFilesForFacadeByPackage(packageFqName: FqName): Collection = ktFilesForPackage(packageFqName).toList() override fun findFilesForScript(scriptFqName: FqName): Collection = ktFilesForPackage(scriptFqName).mapNotNull { it.script }.toList() @@ -98,8 +98,7 @@ internal abstract class AbstractDeclarationProvider( project.read { PsiTreeUtil.collectElementsOfType(it, KtClassOrObject::class.java).asSequence() } - } - .filter { it.getClassId() == classId } + }.filter { it.getClassId() == classId } .toList() override fun getAllTypeAliasesByClassId(classId: ClassId): Collection = @@ -108,8 +107,7 @@ internal abstract class AbstractDeclarationProvider( project.read { PsiTreeUtil.collectElementsOfType(it, KtTypeAlias::class.java).asSequence() } - } - .filter { it.getClassId() == classId } + }.filter { it.getClassId() == classId } .toList() override fun getClassLikeDeclarationByClassId(classId: ClassId): KtClassLikeDeclaration? = @@ -126,11 +124,11 @@ internal abstract class AbstractDeclarationProvider( ktFilesForPackage(callableId.packageName) .flatMap { project.read { - PsiTreeUtil.collectElementsOfType(it, KtNamedFunction::class.java) + PsiTreeUtil + .collectElementsOfType(it, KtNamedFunction::class.java) .asSequence() } - } - .filter { it.isTopLevel } + }.filter { it.isTopLevel } .filter { it.nameAsName == callableId.callableName } .toList() @@ -138,11 +136,11 @@ internal abstract class AbstractDeclarationProvider( ktFilesForPackage(packageFqName) .flatMap { project.read { - PsiTreeUtil.collectElementsOfType(it, KtClassLikeDeclaration::class.java) + PsiTreeUtil + .collectElementsOfType(it, KtClassLikeDeclaration::class.java) .asSequence() } - } - .filter { it.isTopLevelKtOrJavaMember() } + }.filter { it.isTopLevelKtOrJavaMember() } .mapNotNull { it.nameAsName } .toSet() @@ -150,11 +148,11 @@ internal abstract class AbstractDeclarationProvider( ktFilesForPackage(packageFqName) .flatMap { project.read { - PsiTreeUtil.collectElementsOfType(it, KtCallableDeclaration::class.java) + PsiTreeUtil + .collectElementsOfType(it, KtCallableDeclaration::class.java) .asSequence() } - } - .filter { it.isTopLevelKtOrJavaMember() } + }.filter { it.isTopLevelKtOrJavaMember() } .mapNotNull { it.nameAsName } .toSet() @@ -164,8 +162,7 @@ internal abstract class AbstractDeclarationProvider( project.read { PsiTreeUtil.collectElementsOfType(it, KtProperty::class.java).asSequence() } - } - .filter { it.isTopLevel } + }.filter { it.isTopLevel } .filter { it.nameAsName == callableId.callableName } .toList() } @@ -173,16 +170,15 @@ internal abstract class AbstractDeclarationProvider( internal class DeclarationProvider( val scope: GlobalSearchScope, project: Project, - private val index: KtSymbolIndex + private val index: KtSymbolIndex, ) : AbstractDeclarationProvider(project) { - override val hasSpecificCallablePackageNamesComputation = false override val hasSpecificClassifierPackageNamesComputation = false - override fun ktFilesForPackage(fqName: FqName): Sequence { - return index.filesForPackage(fqName.asString()) + override fun ktFilesForPackage(fqName: FqName): Sequence = + index + .filesForPackage(fqName.asString()) .mapNotNull { VirtualFileManager.getInstance().findFileByNioPath(Paths.get(it.filePath)) } .filter { it in scope } .mapNotNull { index.getKtFile(it) } - } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt similarity index 81% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt index df24ee2918..c4c811a531 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/DirectInheritorsProvider.kt @@ -32,7 +32,9 @@ import org.jetbrains.kotlin.psi.psiUtil.contains import org.jetbrains.kotlin.psi.psiUtil.getImportedSimpleNameByImportAlias import org.jetbrains.kotlin.psi.psiUtil.getSuperNames -internal class DirectInheritorsProvider: KtLspService, KotlinDirectInheritorsProvider { +internal class DirectInheritorsProvider : + KtLspService, + KotlinDirectInheritorsProvider { private lateinit var index: KtSymbolIndex private lateinit var modules: List private lateinit var project: Project @@ -44,7 +46,7 @@ internal class DirectInheritorsProvider: KtLspService, KotlinDirectInheritorsPro project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.project = project this.index = index @@ -55,7 +57,7 @@ internal class DirectInheritorsProvider: KtLspService, KotlinDirectInheritorsPro override fun getDirectKotlinInheritors( ktClass: KtClass, scope: GlobalSearchScope, - includeLocalInheritors: Boolean + includeLocalInheritors: Boolean, ): Iterable { computeIndex() @@ -81,35 +83,41 @@ internal class DirectInheritorsProvider: KtLspService, KotlinDirectInheritorsPro modules .asFlatSequence() - .filter { it.isSourceModule }.flatMap { it.computeFiles(extended = true) } + .filter { it.isSourceModule } + .flatMap { it.computeFiles(extended = true) } .mapNotNull { index.getKtFile(it) } .forEach { ktFile -> - ktFile.accept(object : KtTreeVisitorVoid() { - override fun visitClassOrObject(classOrObject: KtClassOrObject) { - classOrObject.getSuperNames().forEach { superName -> - classesBySupertypeName - .computeIfAbsent(Name.identifier(superName)) { mutableSetOf() } - .add(classOrObject) + ktFile.accept( + object : KtTreeVisitorVoid() { + override fun visitClassOrObject(classOrObject: KtClassOrObject) { + classOrObject.getSuperNames().forEach { superName -> + classesBySupertypeName + .computeIfAbsent(Name.identifier(superName)) { mutableSetOf() } + .add(classOrObject) + } + super.visitClassOrObject(classOrObject) } - super.visitClassOrObject(classOrObject) - } - override fun visitTypeAlias(typeAlias: KtTypeAlias) { - val typeElement = typeAlias.getTypeReference()?.typeElement ?: return + override fun visitTypeAlias(typeAlias: KtTypeAlias) { + val typeElement = typeAlias.getTypeReference()?.typeElement ?: return - findInheritableSimpleNames(typeElement).forEach { expandedName -> - inheritableTypeAliasesByAliasedName - .computeIfAbsent(Name.identifier(expandedName)) { mutableSetOf() } - .add(typeAlias) - } + findInheritableSimpleNames(typeElement).forEach { expandedName -> + inheritableTypeAliasesByAliasedName + .computeIfAbsent(Name.identifier(expandedName)) { mutableSetOf() } + .add(typeAlias) + } - super.visitTypeAlias(typeAlias) - } - }) + super.visitTypeAlias(typeAlias) + } + }, + ) } } - private fun calculateAliases(aliasedName: Name, aliases: MutableSet) { + private fun calculateAliases( + aliasedName: Name, + aliases: MutableSet, + ) { inheritableTypeAliasesByAliasedName[aliasedName].orEmpty().forEach { alias -> val aliasName = alias.nameAsSafeName val isNewAliasName = aliases.add(aliasName) @@ -166,7 +174,13 @@ private fun findInheritableSimpleNames(typeElement: KtTypeElement): List } } } - is KtNullableType -> typeElement.innerType?.let(::findInheritableSimpleNames) ?: emptyList() - else -> emptyList() + + is KtNullableType -> { + typeElement.innerType?.let(::findInheritableSimpleNames) ?: emptyList() + } + + else -> { + emptyList() + } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt similarity index 89% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt index c6dff05a08..40bfb90032 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAccessibilityChecker.kt @@ -9,11 +9,11 @@ import org.jetbrains.kotlin.resolve.jvm.modules.JavaModuleResolver class JavaModuleAccessibilityChecker( private val javaModuleResolver: CliJavaModuleResolver, -): KotlinJavaModuleAccessibilityChecker { +) : KotlinJavaModuleAccessibilityChecker { override fun checkAccessibility( useSiteFile: VirtualFile?, referencedFile: VirtualFile, - referencedPackage: FqName? + referencedPackage: FqName?, ): KotlinJavaModuleAccessibilityError? { val accessError = javaModuleResolver.checkAccessibility(useSiteFile, referencedFile, referencedPackage) return accessError?.let(::convertAccessError) @@ -21,13 +21,16 @@ class JavaModuleAccessibilityChecker( private fun convertAccessError(accessError: JavaModuleResolver.AccessError): KotlinJavaModuleAccessibilityError = when (accessError) { - is JavaModuleResolver.AccessError.ModuleDoesNotReadUnnamedModule -> + is JavaModuleResolver.AccessError.ModuleDoesNotReadUnnamedModule -> { KotlinJavaModuleAccessibilityError.ModuleDoesNotReadUnnamedModule + } - is JavaModuleResolver.AccessError.ModuleDoesNotReadModule -> + is JavaModuleResolver.AccessError.ModuleDoesNotReadModule -> { KotlinJavaModuleAccessibilityError.ModuleDoesNotReadModule(accessError.dependencyModuleName) + } - is JavaModuleResolver.AccessError.ModuleDoesNotExportPackage -> + is JavaModuleResolver.AccessError.ModuleDoesNotExportPackage -> { KotlinJavaModuleAccessibilityError.ModuleDoesNotExportPackage(accessError.dependencyModuleName) + } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt similarity index 79% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt index c1a05127e3..c951cd7f83 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/JavaModuleAnnotationsProvider.kt @@ -9,8 +9,7 @@ import org.jetbrains.kotlin.name.ClassId @OptIn(KaNonPublicApi::class) class JavaModuleAnnotationsProvider( private val javaModuleResolver: CliJavaModuleResolver, -): KotlinJavaModuleJavaAnnotationsProvider { - override fun getAnnotationsForModuleOwnerOfClass(classId: ClassId): List? { - return javaModuleResolver.getAnnotationsForModuleOwnerOfClass(classId) - } -} \ No newline at end of file +) : KotlinJavaModuleJavaAnnotationsProvider { + override fun getAnnotationsForModuleOwnerOfClass(classId: ClassId): List? = + javaModuleResolver.getAnnotationsForModuleOwnerOfClass(classId) +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt index 2ead27bcdb..895b55bc98 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/KtLspService.kt @@ -6,11 +6,10 @@ import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.com.intellij.mock.MockProject internal interface KtLspService { - fun setupWith( project: MockProject, index: KtSymbolIndex, modules: List, libraryRoots: List, ) -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt similarity index 96% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt index d661755914..d8e22f8c8e 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/LanguageSettings.kt @@ -6,4 +6,4 @@ import org.jetbrains.kotlin.config.LanguageVersionSettings import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl val latestLanguageVersionSettings: LanguageVersionSettings = - LanguageVersionSettingsImpl(LanguageVersion.LATEST_STABLE, ApiVersion.LATEST) \ No newline at end of file + LanguageVersionSettingsImpl(LanguageVersion.LATEST_STABLE, ApiVersion.LATEST) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt similarity index 67% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt index 167b968eb0..dc1be4a267 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModificationTrackerFactory.kt @@ -3,4 +3,6 @@ package com.itsaky.androidide.lsp.kotlin.compiler.services import org.jetbrains.kotlin.analysis.api.platform.modification.KotlinModificationTrackerByEventFactoryBase import org.jetbrains.kotlin.com.intellij.openapi.project.Project -class ModificationTrackerFactory(project: Project): KotlinModificationTrackerByEventFactoryBase(project) \ No newline at end of file +class ModificationTrackerFactory( + project: Project, +) : KotlinModificationTrackerByEventFactoryBase(project) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt similarity index 71% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt index 5b057064c8..2d07c86f23 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ModuleDependentsProvider.kt @@ -9,25 +9,26 @@ import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.com.intellij.mock.MockProject import org.jetbrains.kotlin.com.intellij.util.containers.ContainerUtil.createConcurrentSoftMap -internal class ModuleDependentsProvider : KtLspService, KotlinModuleDependentsProviderBase() { - +internal class ModuleDependentsProvider : + KotlinModuleDependentsProviderBase(), + KtLspService { private lateinit var modules: List override fun setupWith( project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.modules = modules } private val directDependentsByKtModule by lazy { - modules.asSequence() + modules + .asSequence() .map { module -> buildDependentsMap(module, module.allDirectDependencies()) - } - .reduce { acc, value -> acc + value } + }.reduce { acc, value -> acc + value } } private val transitiveDependentsByKtModule = createConcurrentSoftMap>() @@ -38,30 +39,26 @@ internal class ModuleDependentsProvider : KtLspService, KotlinModuleDependentsPr .reduce { acc, map -> acc + map } } - override fun getDirectDependents(module: KaModule): Set { - return directDependentsByKtModule[module].orEmpty() - } + override fun getDirectDependents(module: KaModule): Set = directDependentsByKtModule[module].orEmpty() - override fun getRefinementDependents(module: KaModule): Set { - return refinementDependentsByKtModule[module].orEmpty() - } + override fun getRefinementDependents(module: KaModule): Set = refinementDependentsByKtModule[module].orEmpty() - override fun getTransitiveDependents(module: KaModule): Set { - return transitiveDependentsByKtModule.computeIfAbsent(module) { key -> + override fun getTransitiveDependents(module: KaModule): Set = + transitiveDependentsByKtModule.computeIfAbsent(module) { key -> computeTransitiveDependents( - key + key, ) } - } } private fun buildDependentsMap( module: KaModule, dependencies: Sequence, -): Map> = buildMap { - dependencies.forEach { dependency -> - if (dependency == module) return@forEach - val dependents = computeIfAbsent(dependency) { mutableSetOf() } - dependents.add(module) +): Map> = + buildMap { + dependencies.forEach { dependency -> + if (dependency == module) return@forEach + val dependents = computeIfAbsent(dependency) { mutableSetOf() } + dependents.add(module) + } } -} diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt new file mode 100644 index 0000000000..965f462451 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt @@ -0,0 +1,192 @@ +@file:Suppress("UnstableApiUsage") + +package com.itsaky.androidide.lsp.kotlin.compiler.services + +import org.jetbrains.concurrency.CancellablePromise +import org.jetbrains.concurrency.Promise +import org.jetbrains.kotlin.com.intellij.openapi.Disposable +import org.jetbrains.kotlin.com.intellij.openapi.application.AppUIExecutor +import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager +import org.jetbrains.kotlin.com.intellij.openapi.application.AsyncExecutionService +import org.jetbrains.kotlin.com.intellij.openapi.application.ExpirableExecutor +import org.jetbrains.kotlin.com.intellij.openapi.application.ModalityState +import org.jetbrains.kotlin.com.intellij.openapi.application.NonBlockingReadAction +import org.jetbrains.kotlin.com.intellij.openapi.progress.ProgressIndicator +import org.jetbrains.kotlin.com.intellij.openapi.project.Project +import org.jetbrains.kotlin.com.intellij.util.Function +import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.TimeUnit +import java.util.function.BooleanSupplier +import java.util.function.Consumer + +/** + * No-op [AsyncExecutionService] for standalone (non-IDE) environments. + * + * The real implementation requires an IDE event-dispatch / write-thread infrastructure that does + * not exist in our standalone setup. Submitted tasks are run asynchronously on the common + * ForkJoin pool so that stub rebuilds triggered by structural PSI changes don't block the + * analysis thread or deadlock against the project read/write lock. + */ +internal class NoOpAsyncExecutionService : AsyncExecutionService() { + private val executor: AppUIExecutor = NoOpAppUIExecutor() + + override fun createExecutor(backgroundExecutor: Executor): ExpirableExecutor = NoOpExpirableExecutor(backgroundExecutor) + + override fun createUIExecutor(modalityState: ModalityState): AppUIExecutor = executor + + override fun createWriteThreadExecutor(modalityState: ModalityState): AppUIExecutor = executor + + override fun buildNonBlockingReadAction(callable: Callable): NonBlockingReadAction = NoOpNonBlockingReadAction(callable) +} + +private class NoOpAppUIExecutor : AppUIExecutor { + override fun later(): AppUIExecutor = this + + override fun withDocumentsCommitted(project: Project): AppUIExecutor = this + + override fun inSmartMode(project: Project): AppUIExecutor = this + + override fun expireWith(disposable: Disposable): AppUIExecutor = this + + override fun execute(runnable: Runnable) { + ApplicationManager.getApplication().invokeLater(runnable) + } + + override fun submit(callable: Callable): CancellablePromise { + val future = CompletableFuture() + ApplicationManager.getApplication().invokeLater { + try { + future.complete(callable.call()) + } catch (e: Throwable) { + future.completeExceptionally(e) + } + } + return future.asCancellablePromise() + } + + override fun submit(runnable: Runnable): CancellablePromise<*> { + val future = CompletableFuture() + ApplicationManager.getApplication().invokeLater { + try { + runnable.run() + future.complete(null) + } catch (e: Throwable) { + future.completeExceptionally(e) + } + } + return future.asCancellablePromise() + } +} + +private class NoOpExpirableExecutor( + private val exec: Executor, +) : ExpirableExecutor { + override fun expireWith(disposable: Disposable): ExpirableExecutor = this + + override fun execute(runnable: Runnable) = exec.execute(runnable) + + override fun submit(callable: Callable): CancellablePromise = + CompletableFuture.supplyAsync({ callable.call() }, exec).asCancellablePromise() + + override fun submit(runnable: Runnable): CancellablePromise<*> = + CompletableFuture.runAsync(runnable, exec).thenApply { null }.asCancellablePromise() +} + +private class NoOpNonBlockingReadAction( + private val callable: Callable, +) : NonBlockingReadAction { + override fun inSmartMode(project: Project): NonBlockingReadAction = this + + override fun withDocumentsCommitted(project: Project): NonBlockingReadAction = this + + override fun expireWhen(condition: BooleanSupplier): NonBlockingReadAction = this + + override fun wrapProgress(indicator: ProgressIndicator): NonBlockingReadAction = this + + override fun expireWith(disposable: Disposable): NonBlockingReadAction = this + + override fun finishOnUiThread( + modalityState: ModalityState, + uiThreadAction: Consumer, + ): NonBlockingReadAction = this + + override fun coalesceBy(vararg equality: Any): NonBlockingReadAction = this + + override fun submit(backgroundThreadExecutor: Executor): CancellablePromise = + CompletableFuture + .supplyAsync({ callable.call() }, backgroundThreadExecutor) + .asCancellablePromise() + + override fun executeSynchronously(): T = callable.call() +} + +private fun CompletableFuture.asCancellablePromise(): CancellablePromise = CompletableFutureCancellablePromise(this) + +private class CompletableFutureCancellablePromise( + private val future: CompletableFuture, +) : CancellablePromise { + // Future + override fun cancel(mayInterruptIfRunning: Boolean): Boolean = future.cancel(mayInterruptIfRunning) + + override fun isCancelled(): Boolean = future.isCancelled + + override fun isDone(): Boolean = future.isDone + + override fun get(): T = future.get() + + override fun get( + timeout: Long, + unit: TimeUnit, + ): T = future.get(timeout, unit) + + // CancellablePromise + override fun cancel() { + future.cancel(true) + } + + override fun onSuccess(handler: Consumer): CancellablePromise { + future.thenAccept(handler) + return this + } + + override fun onError(handler: Consumer): CancellablePromise { + future.exceptionally { e -> + handler.accept(e) + null + } + return this + } + + override fun onProcessed(handler: Consumer): CancellablePromise { + future.whenComplete { value, _ -> handler.accept(value) } + return this + } + + // Promise + override fun getState(): Promise.State = + when { + future.isCancelled || future.isCompletedExceptionally -> Promise.State.REJECTED + future.isDone -> Promise.State.SUCCEEDED + else -> Promise.State.PENDING + } + + override fun processed(child: Promise): Promise = this + + override fun blockingGet( + timeout: Int, + unit: TimeUnit, + ): T = future.get(timeout.toLong(), unit) + + override fun then(handler: Function): Promise = + future.thenApply { handler.`fun`(it) }.asCancellablePromise() + + override fun thenAsync(handler: Function>): Promise = + future + .thenCompose { value -> + @Suppress("UNCHECKED_CAST") + (handler.`fun`(value) as CompletableFutureCancellablePromise).future + }.asCancellablePromise() +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt similarity index 79% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt index 0a65305983..088dca0d04 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PackageProvider.kt @@ -22,7 +22,9 @@ import org.jetbrains.kotlin.load.kotlin.PackagePartProvider import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.name.Name -internal class PackageProviderFactory: KtLspService, KotlinPackageProviderFactory { +internal class PackageProviderFactory : + KtLspService, + KotlinPackageProviderFactory { private lateinit var project: Project private lateinit var index: KtSymbolIndex @@ -30,7 +32,7 @@ internal class PackageProviderFactory: KtLspService, KotlinPackageProviderFactor project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.project = project this.index = index @@ -42,18 +44,22 @@ internal class PackageProviderFactory: KtLspService, KotlinPackageProviderFactor private class PackageProvider( project: Project, searchScope: GlobalSearchScope, - private val index: KtSymbolIndex -): KotlinPackageProviderBase(project, searchScope) { - override fun doesKotlinOnlyPackageExist(packageFqName: FqName): Boolean { - return packageFqName.isRoot || index.packageExistsInSource(packageFqName.asString()) - } + private val index: KtSymbolIndex, +) : KotlinPackageProviderBase(project, searchScope) { + override fun doesKotlinOnlyPackageExist(packageFqName: FqName): Boolean = + packageFqName.isRoot || index.packageExistsInSource(packageFqName.asString()) - override fun getKotlinOnlySubpackageNames(packageFqName: FqName): Set { - return index.subpackageNames(packageFqName.asString()).map { Name.identifier(it) }.toSet() - } + override fun getKotlinOnlySubpackageNames(packageFqName: FqName): Set = + index + .subpackageNames(packageFqName.asString()) + .map { + Name.identifier(it) + }.toSet() } -internal class PackageProviderMerger(private val project: Project) : KotlinPackageProviderMerger { +internal class PackageProviderMerger( + private val project: Project, +) : KotlinPackageProviderMerger { override fun merge(providers: List): KotlinPackageProvider = providers.mergeSpecificProviders<_, PackageProvider>(KotlinCompositePackageProvider.factory) { targetProviders -> val combinedScope = GlobalSearchScope.union(targetProviders.map { it.searchScope }) @@ -65,21 +71,22 @@ internal class PackageProviderMerger(private val project: Project) : KotlinPacka } } -internal class PackagePartProviderFactory: KtLspService, KotlinPackagePartProviderFactory { +internal class PackagePartProviderFactory : + KtLspService, + KotlinPackagePartProviderFactory { private lateinit var allLibraryRoots: List override fun setupWith( project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.allLibraryRoots = libraryRoots } - override fun createPackagePartProvider(scope: GlobalSearchScope): PackagePartProvider { - return JvmPackagePartProvider(latestLanguageVersionSettings, scope).apply { + override fun createPackagePartProvider(scope: GlobalSearchScope): PackagePartProvider = + JvmPackagePartProvider(latestLanguageVersionSettings, scope).apply { addRoots(allLibraryRoots, MessageCollector.NONE) } - } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt index d297e756a1..5466c97631 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/PlatformSettings.kt @@ -6,4 +6,4 @@ import org.jetbrains.kotlin.analysis.api.platform.KotlinPlatformSettings class PlatformSettings : KotlinPlatformSettings { override val deserializedDeclarationsOrigin: KotlinDeserializedDeclarationsOrigin get() = KotlinDeserializedDeclarationsOrigin.BINARIES -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt similarity index 88% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt index e9b5704529..d8aca5d964 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ProjectStructureProvider.kt @@ -21,12 +21,12 @@ import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap import kotlin.io.path.pathString -internal class ProjectStructureProvider : KtLspService, KotlinProjectStructureProviderBase() { - +internal class ProjectStructureProvider : + KotlinProjectStructureProviderBase(), + KtLspService { companion object { - fun getInstance(project: Project): ProjectStructureProvider { - return KotlinProjectStructureProvider.getInstance(project) as ProjectStructureProvider - } + fun getInstance(project: Project): ProjectStructureProvider = + KotlinProjectStructureProvider.getInstance(project) as ProjectStructureProvider } private lateinit var modules: List @@ -35,7 +35,10 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr private val inMemoryVfToModule = ConcurrentHashMap() private val pathToInMemoryVf = ConcurrentHashMap() - fun registerInMemoryFile(sourcePath: String, vf: VirtualFile) { + fun registerInMemoryFile( + sourcePath: String, + vf: VirtualFile, + ) { pathToInMemoryVf.remove(sourcePath)?.let { inMemoryVfToModule.remove(it) } val module = findModuleForSourceId(sourcePath) ?: return @@ -61,7 +64,7 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr project: MockProject, index: KtSymbolIndex, modules: List, - libraryRoots: List + libraryRoots: List, ) { this.modules = modules this.project = project @@ -69,22 +72,24 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr override fun getModule( element: PsiElement, - useSiteModule: KaModule? + useSiteModule: KaModule?, ): KaModule = getModuleImpl(element, useSiteModule) private fun getModuleImpl( element: PsiElement, - useSiteModule: KaModule? + useSiteModule: KaModule?, ): KaModule { - val virtualFile = element.containingFile?.virtualFile - ?: return notUnderContentRootModuleWithoutPsiFile + val virtualFile = + element.containingFile?.virtualFile + ?: return notUnderContentRootModuleWithoutPsiFile // Fast path: in-memory file registered by onFileContentChanged. inMemoryVfToModule[virtualFile]?.let { return it } - val backingFilePath = (element.containingFile as? KtFile)?.let { - it.backingFilePath ?: it.originalKtFile?.backingFilePath - } + val backingFilePath = + (element.containingFile as? KtFile)?.let { + it.backingFilePath ?: it.originalKtFile?.backingFilePath + } if (backingFilePath != null) { findModuleForSourceId(backingFilePath.pathString)?.let { return it } @@ -148,14 +153,12 @@ internal class ProjectStructureProvider : KtLspService, KotlinProjectStructurePr } @OptIn(KaPlatformInterface::class) - override fun getNotUnderContentRootModule(project: Project): KaNotUnderContentRootModule { - return notUnderContentRootModuleWithoutPsiFile - } + override fun getNotUnderContentRootModule(project: Project): KaNotUnderContentRootModule = notUnderContentRootModuleWithoutPsiFile private fun searchVirtualFileInModule( vf: VirtualFile, module: KaModule, - visited: MutableSet + visited: MutableSet, ): KaModule? { if (visited.contains(module)) return null if (module.contentScope.contains(vf)) return module diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt index f68031b742..489ac5137b 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt @@ -37,4 +37,4 @@ class ResolutionScopeProvider : KaBaseResolutionScopeProvider() { fun invalidateAll() { resolutionScopeCache.invalidateAll() } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt similarity index 65% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt index bc9bfd9a48..391250edde 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/WriteAccessGuard.kt @@ -4,8 +4,6 @@ import org.jetbrains.kotlin.com.intellij.openapi.editor.Document import org.jetbrains.kotlin.com.intellij.openapi.editor.impl.DocumentWriteAccessGuard @Suppress("UnstableApiUsage") -class WriteAccessGuard: DocumentWriteAccessGuard() { - override fun isWritable(p0: Document): Result { - return success() - } -} \ No newline at end of file +class WriteAccessGuard : DocumentWriteAccessGuard() { + override fun isWritable(p0: Document): Result = success() +} diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt new file mode 100644 index 0000000000..52c4088710 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt @@ -0,0 +1,72 @@ +package com.itsaky.androidide.lsp.kotlin.compiler.util + +import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger +import org.slf4j.LoggerFactory + +class SLF4JLogger( + name: String, +) : Logger() { + private val logger = LoggerFactory.getLogger(name) + + override fun isDebugEnabled(): Boolean = logger.isDebugEnabled + + override fun debug( + message: String?, + t: Throwable?, + ) { + if (t != null) { + logger.debug(message ?: "", t) + } else { + logger.debug(message ?: "") + } + } + + override fun info( + message: String?, + t: Throwable?, + ) { + if (t != null) { + logger.info(message ?: "", t) + } else { + logger.info(message ?: "") + } + } + + override fun warn( + message: String?, + t: Throwable?, + ) { + if (t != null) { + logger.warn(message ?: "", t) + } else { + logger.warn(message ?: "") + } + } + + override fun error( + message: String?, + t: Throwable?, + vararg details: String?, + ) { + val msg = message ?: "" + val detailStr = + if (details.isNotEmpty()) { + details.filterNotNull().joinToString(System.lineSeparator()) + } else { + "" + } + + val fullMessage = + if (detailStr.isNotEmpty()) { + if (msg.isNotEmpty()) "$msg: $detailStr" else detailStr + } else { + msg + } + + if (t != null) { + logger.error(fullMessage, t) + } else { + logger.error(fullMessage) + } + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt similarity index 96% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt index 2fd8342f52..69785bdf43 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/AdvancedKotlinEditHandler.kt @@ -10,7 +10,6 @@ import org.slf4j.LoggerFactory internal abstract class AdvancedKotlinEditHandler( protected val analysisContext: AnalysisContext, ) : BaseKotlinEditHandler() { - companion object { private val logger = LoggerFactory.getLogger(AdvancedKotlinEditHandler::class.java) } @@ -21,7 +20,7 @@ internal abstract class AdvancedKotlinEditHandler( text: Content, line: Int, column: Int, - index: Int + index: Int, ) { val managedFile = analysisContext.env.ktSymbolIndex.getCurrentKtFileIfPresent(analysisContext.file) if (managedFile == null) { @@ -42,6 +41,6 @@ internal abstract class AdvancedKotlinEditHandler( abstract fun performEdits( ktFile: KtFile, editor: CodeEditor, - item: CompletionItem + item: CompletionItem, ) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt similarity index 88% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt index 81479a265e..04e5ea2cce 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/BaseKotlinEditHandler.kt @@ -12,8 +12,10 @@ import io.github.rosemoe.sora.widget.CodeEditor * @author Akash Yadav */ open class BaseKotlinEditHandler : DefaultEditHandler() { - - override fun executeCommand(editor: CodeEditor, command: Command?) { + override fun executeCommand( + editor: CodeEditor, + command: Command?, + ) { if (editor is ILspEditor) { editor.executeCommand(command) return diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt index a87b6891a9..0a210fbd2e 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/CompletionContext.kt @@ -4,7 +4,6 @@ package com.itsaky.androidide.lsp.kotlin.completion * The context for the providing code completions in a file. */ enum class CompletionContext { - /** * Scope completions (local variables, parameters, etc.) */ @@ -14,4 +13,4 @@ enum class CompletionContext { * Member completions (properties, member functions, extension functions, etc.) */ Member, -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt index 433963f83e..345a3e904c 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt @@ -50,4 +50,4 @@ object ContextKeywords { DeclarationContext.FUNCTION_BODY -> STATEMENT_KEYWORDS } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt similarity index 73% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt index 3878cb57ac..f94d04fcde 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextResolver.kt @@ -43,13 +43,15 @@ data class CursorContext( val isInsideModifierList: Boolean, ) - private val logger = LoggerFactory.getLogger("ContextResolver") /** * Resolves [CursorContext] at the given offset in the given [KtFile]. */ -fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int): CursorContext? { +fun KaSession.resolveCursorContext( + ktFile: KtFile, + offset: Int, +): CursorContext? { val psiElement = ktFile.findElementAt(offset) if (psiElement == null) { logger.error("Unable to find PSI element at offset {} in file {}", offset, ktFile) @@ -69,11 +71,13 @@ fun KaSession.resolveCursorContext(ktFile: KtFile, offset: Int): CursorContext? // The element is typically a KtModifierList, an error node, // or the incomplete declaration itself. val modifierList = ktElement.getParentOfType(strict = false) - val existingModifiers = modifierList - ?.node?.getChildren(MODIFIER_KEYWORDS) - ?.mapNotNull { it.elementType as? KtModifierKeywordToken } - ?.toSet() - ?: emptySet() + val existingModifiers = + modifierList + ?.node + ?.getChildren(MODIFIER_KEYWORDS) + ?.mapNotNull { it.elementType as? KtModifierKeywordToken } + ?.toSet() + ?: emptySet() val declarationKind = resolveDeclarationKind(ktElement) val declarationContext = resolveDeclarationContext(ktElement) @@ -121,23 +125,36 @@ private fun resolveDeclarationContext(element: KtElement): DeclarationContext { when (ancestor) { is KtClassBody -> { return when (val owner = ancestor.parent) { - is KtClass -> when { - owner.isInterface() -> DeclarationContext.INTERFACE_BODY - owner.isEnum() -> DeclarationContext.ENUM_BODY - owner.isAnnotation() -> DeclarationContext.ANNOTATION_BODY - else -> DeclarationContext.CLASS_BODY + is KtClass -> { + when { + owner.isInterface() -> DeclarationContext.INTERFACE_BODY + owner.isEnum() -> DeclarationContext.ENUM_BODY + owner.isAnnotation() -> DeclarationContext.ANNOTATION_BODY + else -> DeclarationContext.CLASS_BODY + } + } + + is KtObjectDeclaration -> { + DeclarationContext.OBJECT_BODY } - is KtObjectDeclaration -> DeclarationContext.OBJECT_BODY - else -> DeclarationContext.CLASS_BODY + else -> { + DeclarationContext.CLASS_BODY + } } } - is KtBlockExpression -> return DeclarationContext.FUNCTION_BODY - is KtFile -> return if (ancestor.isScript()) - DeclarationContext.SCRIPT_TOP_LEVEL - else - DeclarationContext.TOP_LEVEL + is KtBlockExpression -> { + return DeclarationContext.FUNCTION_BODY + } + + is KtFile -> { + return if (ancestor.isScript()) { + DeclarationContext.SCRIPT_TOP_LEVEL + } else { + DeclarationContext.TOP_LEVEL + } + } } } return DeclarationContext.TOP_LEVEL @@ -146,21 +163,46 @@ private fun resolveDeclarationContext(element: KtElement): DeclarationContext { private fun resolveDeclarationKind(element: KtElement): DeclarationKind { // Walk up to the nearest declaration owning this modifier list / position return when (val declaration = element.getNonStrictParentOfType()) { - is KtClass -> when { - declaration.isInterface() -> DeclarationKind.INTERFACE - declaration.isEnum() -> DeclarationKind.ENUM_CLASS - declaration.isAnnotation() -> DeclarationKind.ANNOTATION_CLASS - else -> DeclarationKind.CLASS + is KtClass -> { + when { + declaration.isInterface() -> DeclarationKind.INTERFACE + declaration.isEnum() -> DeclarationKind.ENUM_CLASS + declaration.isAnnotation() -> DeclarationKind.ANNOTATION_CLASS + else -> DeclarationKind.CLASS + } + } + + is KtObjectDeclaration -> { + DeclarationKind.OBJECT } - is KtObjectDeclaration -> DeclarationKind.OBJECT - is KtNamedFunction -> DeclarationKind.FUN - is KtProperty -> if (declaration.isVar) DeclarationKind.PROPERTY_VAR - else DeclarationKind.PROPERTY_VAL + is KtNamedFunction -> { + DeclarationKind.FUN + } + + is KtProperty -> { + if (declaration.isVar) { + DeclarationKind.PROPERTY_VAR + } else { + DeclarationKind.PROPERTY_VAL + } + } + + is KtTypeAlias -> { + DeclarationKind.TYPEALIAS + } - is KtTypeAlias -> DeclarationKind.TYPEALIAS - is KtConstructor<*> -> DeclarationKind.CONSTRUCTOR - null -> DeclarationKind.UNKNOWN // pure modifier list, no keyword yet - else -> DeclarationKind.UNKNOWN + is KtConstructor<*> -> { + DeclarationKind.CONSTRUCTOR + } + + null -> { + DeclarationKind.UNKNOWN + } + + // pure modifier list, no keyword yet + else -> { + DeclarationKind.UNKNOWN + } } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt similarity index 55% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt index 27f3de4703..6b00651fb0 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/DeclarationContext.kt @@ -7,9 +7,9 @@ enum class DeclarationContext { TOP_LEVEL, CLASS_BODY, INTERFACE_BODY, - OBJECT_BODY, // includes companion object + OBJECT_BODY, // includes companion object ENUM_BODY, - FUNCTION_BODY, // local declarations & statements + FUNCTION_BODY, // local declarations & statements SCRIPT_TOP_LEVEL, ANNOTATION_BODY, } @@ -18,8 +18,15 @@ enum class DeclarationContext { * Defines declaration kinds for element at cursor. */ enum class DeclarationKind { - CLASS, INTERFACE, OBJECT, ENUM_CLASS, ANNOTATION_CLASS, - FUN, PROPERTY_VAL, PROPERTY_VAR, - TYPEALIAS, CONSTRUCTOR, - UNKNOWN // e.g. modifier typed before any keyword yet + CLASS, + INTERFACE, + OBJECT, + ENUM_CLASS, + ANNOTATION_CLASS, + FUN, + PROPERTY_VAL, + PROPERTY_VAR, + TYPEALIAS, + CONSTRUCTOR, + UNKNOWN, // e.g. modifier typed before any keyword yet } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt similarity index 97% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt index 0b568fc04b..24178e0036 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinAutoImportEditHandler.kt @@ -13,12 +13,11 @@ internal class KotlinAutoImportEditHandler( analysisContext: AnalysisContext, private val symbolToImport: JvmSymbol? = null, ) : AdvancedKotlinEditHandler(analysisContext) { - context(ctx: AnalysisContext) override fun performEdits( ktFile: KtFile, editor: CodeEditor, - item: CompletionItem + item: CompletionItem, ) { val fqnToImport = symbolToImport?.fqName @@ -30,4 +29,4 @@ internal class KotlinAutoImportEditHandler( RewriteHelper.performEdits(edits, editor) } } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt similarity index 81% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt index 9847201e1b..3cb9a638e1 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletionItem.kt @@ -20,21 +20,20 @@ class KotlinCompletionItem( matchLevel: MatchLevel, additionalTextEdits: List?, data: ICompletionData?, - editHandler: IEditHandler = BaseKotlinEditHandler() + editHandler: IEditHandler = BaseKotlinEditHandler(), ) : CompletionItem( - ideLabel, - detail, - insertText, - insertTextFormat, - sortText, - command, - completionKind, - matchLevel, - additionalTextEdits, - data, - editHandler -) { - + ideLabel, + detail, + insertText, + insertTextFormat, + sortText, + command, + completionKind, + matchLevel, + additionalTextEdits, + data, + editHandler, + ) { constructor() : this( "", // label "", // detail @@ -45,6 +44,6 @@ class KotlinCompletionItem( CompletionItemKind.NONE, // kind MatchLevel.NO_MATCH, // match level ArrayList(), // additionalEdits - null // data + null, // data ) -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinCompletions.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt similarity index 75% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt index 33986ca3c2..da93ce0900 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetRepository.kt @@ -6,11 +6,12 @@ import com.itsaky.androidide.lsp.snippets.SnippetRegistry object KotlinSnippetRepository { val snippets: Map> - get() = KotlinSnippetScope.entries.associateWith { scope -> - SnippetRegistry.getSnippets("kt", scope.filename) - } + get() = + KotlinSnippetScope.entries.associateWith { scope -> + SnippetRegistry.getSnippets("kt", scope.filename) + } fun init() { SnippetRegistry.initBuiltIn("kt", KotlinSnippetScope.entries) } -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt similarity index 88% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt index a3d5a9d47b..634cd09d2e 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/KotlinSnippetScope.kt @@ -7,8 +7,9 @@ import com.itsaky.androidide.lsp.snippets.ISnippetScope * * @author Akash Yadav */ -enum class KotlinSnippetScope(override val filename: String) : ISnippetScope { - +enum class KotlinSnippetScope( + override val filename: String, +) : ISnippetScope { /** * Snippets that can be used at the top level. This includes snippets such as class, interface, * enum templates. @@ -23,4 +24,4 @@ enum class KotlinSnippetScope(override val filename: String) : ISnippetScope { /** Snippets that can be used anywhere in the code, irrespective of the current scope. */ GLOBAL("global"), -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt index 5f0710e1d8..eeb98519cb 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt @@ -171,4 +171,4 @@ object ModifierFilter { private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt similarity index 89% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt index 901b5c0c04..015eba7c60 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/diagnostic/KotlinDiagnosticProvider.kt @@ -1,5 +1,7 @@ package com.itsaky.androidide.lsp.kotlin.diagnostic +import com.itsaky.androidide.lsp.kotlin.api.DiagnosticAction +import com.itsaky.androidide.lsp.kotlin.api.KotlinDiagnosticExtra import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.modules.AnalysisPriority import com.itsaky.androidide.lsp.kotlin.compiler.modules.ScheduledCancelChecker @@ -25,25 +27,6 @@ import java.nio.file.Path private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") -internal data class KotlinDiagnosticExtra( - val compilationEnv: CompilationEnvironment, - val action: ActionT, -) - -@Suppress("UNCHECKED_CAST") -internal inline fun KotlinDiagnosticExtra<*>?.asAction(): KotlinDiagnosticExtra? = - if (this?.action is T) this as KotlinDiagnosticExtra else null - -internal sealed interface DiagnosticAction { - data object None : DiagnosticAction - - data class ResolveReference( - val referenceName: String, - ) : DiagnosticAction - - data object NullSafetyFix : DiagnosticAction -} - context(env: CompilationEnvironment) internal fun collectDiagnosticsFor( file: Path, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/GoToDefinition.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/navigation/ReferenceAtCaret.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/ActiveParameterResolver.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/ActiveParameterResolver.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/ActiveParameterResolver.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/ActiveParameterResolver.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/CallAtCursorFinder.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/CallAtCursorFinder.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/CallAtCursorFinder.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/CallAtCursorFinder.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/KotlinSignatureHelp.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/KotlinSignatureHelp.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/KotlinSignatureHelp.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/KotlinSignatureHelp.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/SignatureInfoBuilder.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/SignatureInfoBuilder.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/SignatureInfoBuilder.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/signaturehelp/SignatureInfoBuilder.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/AbstractMemberStubs.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/AbstractMemberStubs.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/AbstractMemberStubs.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/AbstractMemberStubs.kt diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt new file mode 100644 index 0000000000..7ff029f22c --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt @@ -0,0 +1,92 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext +import org.jetbrains.kotlin.lexer.KtKeywordToken +import org.jetbrains.kotlin.lexer.KtTokens + +object ContextKeywords { + /** Hard keywords valid as *statement starters* inside a function body */ + val STATEMENT_KEYWORDS = + setOf( + KtTokens.IF_KEYWORD, + KtTokens.ELSE_KEYWORD, + KtTokens.WHEN_KEYWORD, + KtTokens.WHILE_KEYWORD, + KtTokens.DO_KEYWORD, + KtTokens.FOR_KEYWORD, + KtTokens.TRY_KEYWORD, + KtTokens.RETURN_KEYWORD, + KtTokens.THROW_KEYWORD, + KtTokens.BREAK_KEYWORD, + KtTokens.CONTEXT_KEYWORD, + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, // local declarations + KtTokens.OBJECT_KEYWORD, // anonymous / local object + KtTokens.CLASS_KEYWORD, // local class (rare but legal) + ) + + /** Declaration starters at top-level / class body */ + val DECLARATION_KEYWORDS = + setOf( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + KtTokens.CLASS_KEYWORD, + KtTokens.INTERFACE_KEYWORD, + KtTokens.OBJECT_KEYWORD, + KtTokens.TYPE_ALIAS_KEYWORD, + KtTokens.CONSTRUCTOR_KEYWORD, + KtTokens.INIT_KEYWORD, + ) + + val TOP_LEVEL_ONLY = setOf(KtTokens.PACKAGE_KEYWORD, KtTokens.IMPORT_KEYWORD) + + /** + * Resolve valid keywords for the given declaration context. + * + * @param ctx The declaration context. + * @return The keyword tokens for the declaration context. + */ + fun keywordsFor(ctx: DeclarationContext): Set = + when (ctx) { + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + TOP_LEVEL_ONLY + DECLARATION_KEYWORDS + } + + DeclarationContext.CLASS_BODY -> { + DECLARATION_KEYWORDS + + setOf(KtTokens.INIT_KEYWORD, KtTokens.CONSTRUCTOR_KEYWORD) + } + + DeclarationContext.INTERFACE_BODY -> { + setOf( + KtTokens.VAL_KEYWORD, + KtTokens.VAR_KEYWORD, + KtTokens.FUN_KEYWORD, + KtTokens.CLASS_KEYWORD, + KtTokens.INTERFACE_KEYWORD, + KtTokens.OBJECT_KEYWORD, + KtTokens.TYPE_ALIAS_KEYWORD, + ) + } + + DeclarationContext.OBJECT_BODY, + DeclarationContext.ENUM_BODY, + -> { + DECLARATION_KEYWORDS - setOf(KtTokens.CONSTRUCTOR_KEYWORD) + } + + DeclarationContext.ANNOTATION_BODY -> { + setOf(KtTokens.VAL_KEYWORD) + } + + // annotation params only + + DeclarationContext.FUNCTION_BODY -> { + STATEMENT_KEYWORDS + } + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt similarity index 72% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt index 51318536ab..7e3fa7de76 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextResolver.kt @@ -64,7 +64,7 @@ internal fun KaSession.resolveAnalysisContext( file: Path, ktFile: KtFile, offset: Int, - partial: String + partial: String, ): AnalysisContext? { val psiElement = ktFile.findElementAt(offset) if (psiElement == null) { @@ -84,11 +84,13 @@ internal fun KaSession.resolveAnalysisContext( // The element is typically a KtModifierList, an error node, // or the incomplete declaration itself. val modifierList = ktElement.getParentOfType(strict = false) - val existingModifiers = modifierList - ?.node?.getChildren(MODIFIER_KEYWORDS) - ?.mapNotNull { it.elementType as? KtModifierKeywordToken } - ?.toSet() - ?: emptySet() + val existingModifiers = + modifierList + ?.node + ?.getChildren(MODIFIER_KEYWORDS) + ?.mapNotNull { it.elementType as? KtModifierKeywordToken } + ?.toSet() + ?: emptySet() val declarationKind = resolveDeclarationKind(ktElement) val declarationContext = resolveDeclarationContext(ktElement) @@ -114,23 +116,36 @@ private fun resolveDeclarationContext(element: KtElement): DeclarationContext { when (ancestor) { is KtClassBody -> { return when (val owner = ancestor.parent) { - is KtClass -> when { - owner.isInterface() -> DeclarationContext.INTERFACE_BODY - owner.isEnum() -> DeclarationContext.ENUM_BODY - owner.isAnnotation() -> DeclarationContext.ANNOTATION_BODY - else -> DeclarationContext.CLASS_BODY + is KtClass -> { + when { + owner.isInterface() -> DeclarationContext.INTERFACE_BODY + owner.isEnum() -> DeclarationContext.ENUM_BODY + owner.isAnnotation() -> DeclarationContext.ANNOTATION_BODY + else -> DeclarationContext.CLASS_BODY + } } - is KtObjectDeclaration -> DeclarationContext.OBJECT_BODY - else -> DeclarationContext.CLASS_BODY + is KtObjectDeclaration -> { + DeclarationContext.OBJECT_BODY + } + + else -> { + DeclarationContext.CLASS_BODY + } } } - is KtBlockExpression -> return DeclarationContext.FUNCTION_BODY - is KtFile -> return if (ancestor.isScript()) - DeclarationContext.SCRIPT_TOP_LEVEL - else - DeclarationContext.TOP_LEVEL + is KtBlockExpression -> { + return DeclarationContext.FUNCTION_BODY + } + + is KtFile -> { + return if (ancestor.isScript()) { + DeclarationContext.SCRIPT_TOP_LEVEL + } else { + DeclarationContext.TOP_LEVEL + } + } } } return DeclarationContext.TOP_LEVEL @@ -139,21 +154,46 @@ private fun resolveDeclarationContext(element: KtElement): DeclarationContext { private fun resolveDeclarationKind(element: KtElement): DeclarationKind { // Walk up to the nearest declaration owning this modifier list / position return when (val declaration = element.getNonStrictParentOfType()) { - is KtClass -> when { - declaration.isInterface() -> DeclarationKind.INTERFACE - declaration.isEnum() -> DeclarationKind.ENUM_CLASS - declaration.isAnnotation() -> DeclarationKind.ANNOTATION_CLASS - else -> DeclarationKind.CLASS + is KtClass -> { + when { + declaration.isInterface() -> DeclarationKind.INTERFACE + declaration.isEnum() -> DeclarationKind.ENUM_CLASS + declaration.isAnnotation() -> DeclarationKind.ANNOTATION_CLASS + else -> DeclarationKind.CLASS + } } - is KtObjectDeclaration -> DeclarationKind.OBJECT - is KtNamedFunction -> DeclarationKind.FUN - is KtProperty -> if (declaration.isVar) DeclarationKind.PROPERTY_VAR - else DeclarationKind.PROPERTY_VAL + is KtObjectDeclaration -> { + DeclarationKind.OBJECT + } + + is KtNamedFunction -> { + DeclarationKind.FUN + } - is KtTypeAlias -> DeclarationKind.TYPEALIAS - is KtConstructor<*> -> DeclarationKind.CONSTRUCTOR - null -> DeclarationKind.UNKNOWN // pure modifier list, no keyword yet - else -> DeclarationKind.UNKNOWN + is KtProperty -> { + if (declaration.isVar) { + DeclarationKind.PROPERTY_VAR + } else { + DeclarationKind.PROPERTY_VAL + } + } + + is KtTypeAlias -> { + DeclarationKind.TYPEALIAS + } + + is KtConstructor<*> -> { + DeclarationKind.CONSTRUCTOR + } + + null -> { + DeclarationKind.UNKNOWN + } + + // pure modifier list, no keyword yet + else -> { + DeclarationKind.UNKNOWN + } } } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt similarity index 69% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt index 0b86603991..ce88bee7bd 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/EditExts.kt @@ -13,30 +13,36 @@ import org.slf4j.LoggerFactory private val logger = LoggerFactory.getLogger("EditExts") fun TextRange.toRange(containingFile: PsiFile): Range { - val doc = PsiDocumentManager.getInstance(containingFile.project).getDocument(containingFile) - ?: return Range.NONE + val doc = + PsiDocumentManager.getInstance(containingFile.project).getDocument(containingFile) + ?: return Range.NONE val startLine = doc.getLineNumber(startOffset) val startCol = startOffset - doc.getLineStartOffset(startLine) val endLine = doc.getLineNumber(endOffset) val endCol = endOffset - doc.getLineStartOffset(endLine) return Range( - start = Position( - line = startLine, - column = startCol, - index = startOffset, - ), end = Position( - line = endLine, - column = endCol, - index = endOffset, - ) + start = + Position( + line = startLine, + column = startCol, + index = startOffset, + ), + end = + Position( + line = endLine, + column = endCol, + index = endOffset, + ), ) } context(ctx: AnalysisContext) -internal fun insertImport(fqn: String): List = - insertImport(ctx.ktFile, fqn) +internal fun insertImport(fqn: String): List = insertImport(ctx.ktFile, fqn) -internal fun insertImport(ktFile: KtFile, fqn: String): List { +internal fun insertImport( + ktFile: KtFile, + fqn: String, +): List { val imports = ktFile.importDirectives val importText = "import $fqn" for (import in imports) { @@ -66,31 +72,38 @@ internal fun insertImport(ktFile: KtFile, fqn: String): List { return listOf( TextEdit( range = Range(start, start), - newText = importText + System.lineSeparator() - ) + newText = importText + System.lineSeparator(), + ), ) } -internal fun insertBefore(element: PsiElement, text: String): List { +internal fun insertBefore( + element: PsiElement, + text: String, +): List { val range = rangeOf(element) return listOf( TextEdit( range = Range(range.start, range.start), - newText = text - ) + newText = text, + ), ) } -internal fun insertAfter(element: PsiElement, text: String): List { +internal fun insertAfter( + element: PsiElement, + text: String, +): List { val range = rangeOf(element) return listOf( TextEdit( range = Range(range.end, range.end), - newText = text - ) + newText = text, + ), ) } -internal fun rangeOf(element: PsiElement, containingFile: PsiFile = element.containingFile): Range { - return element.textRange.toRange(containingFile) -} \ No newline at end of file +internal fun rangeOf( + element: PsiElement, + containingFile: PsiFile = element.containingFile, +): Range = element.textRange.toRange(containingFile) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportOrganizer.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportOrganizer.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportOrganizer.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportOrganizer.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt index cacdb775fe..550afa9d78 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ImportUsageCollector.kt @@ -99,7 +99,10 @@ private fun KaSymbol.importableFqNameString(): String? = // that's actually imported. Covers `Foo()` calls and `@Foo` annotations (both resolve to the // constructor). Must precede the KaCallableSymbol branch, which a constructor also matches. is KaConstructorSymbol -> containingClassId?.asSingleFqName()?.asString() + is KaClassLikeSymbol -> classId?.asSingleFqName()?.asString() + is KaCallableSymbol -> callableId?.asSingleFqName()?.asString() + else -> null } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt index 406f9f0ac8..6284f3ddd3 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt @@ -175,4 +175,4 @@ internal object ModifierFilter { private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) -} \ No newline at end of file +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/NullSafetyFix.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/NullSafetyFix.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/NullSafetyFix.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/NullSafetyFix.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt similarity index 99% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt index 502d5c3aea..cb7edab0fd 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolExts.kt @@ -22,4 +22,4 @@ val KaSymbol.containingTopLevelClassDeclaration: KaClassSymbol? } return lastClass - } \ No newline at end of file + } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt similarity index 91% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt index 69c4c612c2..46168b24b7 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SymbolVisibilityChecker.kt @@ -24,8 +24,9 @@ internal class SymbolVisibilityChecker( useSiteModule: KaModule, useSitePackage: String? = null, ): Boolean { - val declaringModule = structureProvider.findModuleForSourceId(symbol.sourceId) - ?: return false + val declaringModule = + structureProvider.findModuleForSourceId(symbol.sourceId) + ?: return false if (!isReachable(useSiteModule, declaringModule)) return false if (!arePlatformCompatible(useSiteModule, declaringModule)) return false @@ -34,7 +35,10 @@ internal class SymbolVisibilityChecker( return true } - fun isReachable(useSiteModule: KaModule, declaringModule: KaModule): Boolean { + fun isReachable( + useSiteModule: KaModule, + declaringModule: KaModule, + ): Boolean { if (useSiteModule == declaringModule) return true if (moduleVisibilityCache[useSiteModule]?.contains(declaringModule) == true) return true @@ -54,7 +58,10 @@ internal class SymbolVisibilityChecker( return false } - fun arePlatformCompatible(useSiteModule: KaModule, declaringModule: KaModule): Boolean { + fun arePlatformCompatible( + useSiteModule: KaModule, + declaringModule: KaModule, + ): Boolean { val usePlatform = useSiteModule.targetPlatform val declPlatform = declaringModule.targetPlatform diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/TypeRendering.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/TypeRendering.kt similarity index 100% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/TypeRendering.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/TypeRendering.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt similarity index 53% rename from lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt rename to lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt index a5bc54fc80..e41d42ac8a 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/VirtualFileExts.kt @@ -4,8 +4,6 @@ import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFileManager import java.nio.file.Path -fun VirtualFile.toNioPathOrNull(): Path? = - runCatching { toNioPath() }.getOrNull() +fun VirtualFile.toNioPathOrNull(): Path? = runCatching { toNioPath() }.getOrNull() -fun Path.toVirtualFileOrNull(): VirtualFile? = - VirtualFileManager.getInstance().findFileByNioPath(this) \ No newline at end of file +fun Path.toVirtualFileOrNull(): VirtualFile? = VirtualFileManager.getInstance().findFileByNioPath(this) diff --git a/lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml b/lsp/kotlin-compiler-impl/src/main/resources/META-INF/kt-lsp/kt-lsp.xml similarity index 100% rename from lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml rename to lsp/kotlin-compiler-impl/src/main/resources/META-INF/kt-lsp/kt-lsp.xml diff --git a/lsp/kotlin/build.gradle.kts b/lsp/kotlin/build.gradle.kts index 9b16f87796..f7bb441c01 100644 --- a/lsp/kotlin/build.gradle.kts +++ b/lsp/kotlin/build.gradle.kts @@ -43,10 +43,10 @@ dependencies { implementation(projects.actions) implementation(projects.lsp.api) implementation(projects.lsp.jvmSymbolIndex) + implementation(projects.lsp.kotlinApi) implementation(projects.lsp.models) implementation(projects.editorApi) implementation(projects.eventbusEvents) - implementation(projects.subprojects.kotlinAnalysisApi) implementation(projects.shared) implementation(projects.subprojects.projects) implementation(projects.subprojects.projectModels) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt index ae9f0d903c..7370cb3763 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt @@ -30,15 +30,11 @@ import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.IServerSettings -import com.itsaky.androidide.lsp.kotlin.compiler.CompilationEnvironment -import com.itsaky.androidide.lsp.kotlin.compiler.Compiler -import com.itsaky.androidide.lsp.kotlin.compiler.KotlinProjectModel -import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_INDEX_KEY -import com.itsaky.androidide.lsp.kotlin.compiler.index.KT_SOURCE_FILE_META_INDEX_KEY -import com.itsaky.androidide.lsp.kotlin.completion.codeComplete -import com.itsaky.androidide.lsp.kotlin.diagnostic.collectDiagnosticsFor -import com.itsaky.androidide.lsp.kotlin.navigation.findDefinitionAt -import com.itsaky.androidide.lsp.kotlin.signaturehelp.doSignatureHelp +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilationEnvironment +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSession +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.api.KT_SOURCE_FILE_META_INDEX_KEY +import com.itsaky.androidide.lsp.kotlin.loader.KotlinCompilerLoader import com.itsaky.androidide.lsp.models.CompletionParams import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.DefinitionParams @@ -49,7 +45,6 @@ import com.itsaky.androidide.lsp.models.ReferenceParams import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams -import com.itsaky.androidide.lsp.util.LSPEditorActions import com.itsaky.androidide.models.Range import com.itsaky.androidide.projects.FileManager import com.itsaky.androidide.projects.ProjectManagerImpl @@ -70,22 +65,19 @@ import org.appdevforall.codeonthego.indexing.jvm.KtFileMetadataIndex import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import org.jetbrains.kotlin.config.JvmTarget -import org.jetbrains.kotlin.config.LanguageVersion -import org.jetbrains.kotlin.platform.jvm.JvmPlatforms import org.slf4j.LoggerFactory import java.nio.file.Path -import java.nio.file.Paths class KotlinLanguageServer : ILanguageServer { private var _client: ILanguageClient? = null private var _settings: IServerSettings? = null private var initialized = false + private var workspace: Workspace? = null private val scope = CoroutineScope(SupervisorJob() + CoroutineName(KotlinLanguageServer::class.simpleName!!)) - private var projectModel: KotlinProjectModel? = null - private var compiler: Compiler? = null + + private val loader = KotlinCompilerLoader(BaseApplication.baseInstance) override val serverId: String = SERVER_ID @@ -111,27 +103,54 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) scope.cancel("LSP is being shut down") - compiler?.close() + loader.close() initialized = false } override fun connectClient(client: ILanguageClient?) { this._client = client - this.compiler?.updateLanguageClient(client) + loader.currentSession()?.updateLanguageClient(client) } - /** Returns the [CompilationEnvironment] responsible for [file], or null if the compiler is not ready. */ - internal fun compilationEnvironmentFor(file: Path): CompilationEnvironment? = compiler?.compilationEnvironmentFor(file) - override fun applySettings(settings: IServerSettings?) { this._settings = settings } + /** + * The session, creating it (extracting and DexClassLoader-loading the carrier APK) on + * the first call for a Kotlin file -- not eagerly in [setupWithProject], which used to + * pay this cost for every project open, Kotlin or not. + */ + private fun ensureSession(): IKotlinCompilerSession? { + val ws = workspace ?: return null + return loader + .getOrCreateSession( + workspace = ws, + jdkHome = Environment.JAVA_HOME.toPath(), + jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE, + jdkVersionString = IJdkDistributionProvider.DEFAULT_JAVA_VERSION, + languageClient = client, + ).also { session -> + if (!codeActionsRegistered) { + session.registerCodeActions() + codeActionsRegistered = true + } + } + } + + private var codeActionsRegistered = false + + /** + * The environment for [file], loading the compiler on first Kotlin-file-gated call if + * needed. Public: the code actions that move with the isolated compiler module (e.g. + * `OrganizeImportsAction`, `ImplementMembersAction`) reach back through this to get the + * concrete environment they need for advanced operations, same as before this refactor. + */ + fun compilationEnvironmentFor(file: Path): IKotlinCompilationEnvironment? = ensureSession()?.compilationEnvironmentFor(file) + override fun setupWithProject(workspace: Workspace) { logger.info("setupWithProject called, initialized={}", initialized) - LSPEditorActions.ensureActionsMenuRegistered(KotlinCodeActionsMenu) - val context = BaseApplication.baseInstance val indexingServiceManager = ProjectManagerImpl @@ -163,47 +182,17 @@ class KotlinLanguageServer : ILanguageServer { jvmLibraryIndexingService?.refresh() - val jdkHome = Environment.JAVA_HOME.toPath() - val jdkRelease = IJdkDistributionProvider.DEFAULT_JAVA_RELEASE - val intellijPluginRoot = Paths.get(context.applicationInfo.sourceDir) - - val jvmTarget = - JvmTarget.fromString(IJdkDistributionProvider.DEFAULT_JAVA_VERSION) - ?: JvmTarget.JVM_21 - - val jvmPlatform = JvmPlatforms.jvmPlatformByTargetVersion(jvmTarget) - - if (!initialized) { - logger.info("Creating initial analysis session") - - val model = KotlinProjectModel() - model.update(workspace, jvmPlatform) - this.projectModel = model - - val compiler = - Compiler( - workspace = workspace, - projectModel = model, - intellijPluginRoot = intellijPluginRoot, - jdkHome = jdkHome, - jdkRelease = jdkRelease, - languageVersion = LanguageVersion.LATEST_STABLE, - ) - - compiler.updateLanguageClient(client) - this.compiler = compiler - } else { - logger.info("Updating project model") - projectModel?.update(workspace, jvmPlatform) - } + this.workspace = workspace + + val session = loader.currentSession() + if (session != null) { + // A session already exists (a Kotlin file was opened before this re-setup, e.g. on + // project reload) -- refresh its project model and pick up files that are already open. + session.updateProjectModel(workspace) - // Open already open files - // we won't get an event for these - FileManager.activeDocuments.ifNotEmpty { - forEach { document -> - compiler - ?.compilationEnvironmentFor(document.file) - ?.openFileIfNeeded(document.file) + FileManager.activeDocuments.ifNotEmpty { + val activeFiles = map { it.file } + session.openFileIfNeeded(activeFiles) } } @@ -218,10 +207,7 @@ class KotlinLanguageServer : ILanguageServer { } logger.debug("complete(position={}, file={})", params.position, params.file) - return compiler - ?.compilationEnvironmentFor(params.file) - ?.let { context(it) { codeComplete(params) } } - ?: CompletionResult.EMPTY + return compilationEnvironmentFor(params.file)?.complete(params) ?: CompletionResult.EMPTY } override suspend fun findReferences(params: ReferenceParams): ReferenceResult { @@ -246,10 +232,7 @@ class KotlinLanguageServer : ILanguageServer { } logger.debug("findDefinition(position={}, file={})", params.position, params.file) - return compiler - ?.compilationEnvironmentFor(params.file) - ?.let { context(it) { findDefinitionAt(params) } } - ?: DefinitionResult.empty() + return compilationEnvironmentFor(params.file)?.findDefinition(params) ?: DefinitionResult.empty() } override suspend fun expandSelection(params: ExpandSelectionParams): Range = params.selection @@ -264,10 +247,7 @@ class KotlinLanguageServer : ILanguageServer { } logger.debug("signatureHelp(position={}, file={})", params.position, params.file) - return compiler - ?.compilationEnvironmentFor(params.file) - ?.let { context(it) { doSignatureHelp(params) } } - ?: SignatureHelp.empty() + return compilationEnvironmentFor(params.file)?.signatureHelp(params) ?: SignatureHelp.empty() } override suspend fun analyze(file: Path): DiagnosticResult { @@ -287,10 +267,7 @@ class KotlinLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } - return compiler - ?.compilationEnvironmentFor(file) - ?.let { context(it) { collectDiagnosticsFor(file, createJobCancelChecker()) } } - ?: DiagnosticResult.NO_UPDATE + return compilationEnvironmentFor(file)?.collectDiagnostics(file, createJobCancelChecker()) ?: DiagnosticResult.NO_UPDATE } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -300,9 +277,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.openedFile) - ?.onFileOpen(event.openedFile) + compilationEnvironmentFor(event.openedFile)?.onFileOpen(event.openedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -312,9 +287,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.changedFile) - ?.onFileContentChanged(event.changedFile) + compilationEnvironmentFor(event.changedFile)?.onFileContentChanged(event.changedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -324,9 +297,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.closedFile) - ?.onFileClosed(event.closedFile) + compilationEnvironmentFor(event.closedFile)?.onFileClosed(event.closedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -336,16 +307,14 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.savedFile) - ?.onFileSaved(event.savedFile) + compilationEnvironmentFor(event.savedFile)?.onFileSaved(event.savedFile) } @Subscribe @Suppress("unused") fun onBuildCompleted(event: BuildCompletedEvent) { Sentry.addBreadcrumb("onBuildCompleted: result=${event.result}") - compiler?.refreshSources() + loader.currentSession()?.refreshSources() } @Subscribe @@ -357,7 +326,7 @@ class KotlinLanguageServer : ILanguageServer { } scope.launch { - runCatching { compiler?.compilationEnvironmentFor(path) } + runCatching { compilationEnvironmentFor(path) } .getOrNull() ?.onFileCreated(path) } @@ -372,7 +341,7 @@ class KotlinLanguageServer : ILanguageServer { } scope.launch { - runCatching { compiler?.compilationEnvironmentFor(path) } + runCatching { compilationEnvironmentFor(path) } .getOrNull() ?.onFileRemoved(path) } @@ -391,27 +360,21 @@ class KotlinLanguageServer : ILanguageServer { if (!oldIsKotlinFile && newIsKotlinFile) { // only the new file is a Kotlin file // so just submit it for indexing - compiler - ?.compilationEnvironmentFor(toPath) - ?.onFileCreated(toPath) + compilationEnvironmentFor(toPath)?.onFileCreated(toPath) return@launch } if (oldIsKotlinFile && !newIsKotlinFile) { // only the old file was a Kotlin file // so just remove it from the index - compiler - ?.compilationEnvironmentFor(fromPath) - ?.onFileRemoved(fromPath) + compilationEnvironmentFor(fromPath)?.onFileRemoved(fromPath) return@launch } - val fromKind = runCatching { compiler?.compilationKindFor(fromPath) }.getOrNull() - val toKind = runCatching { compiler?.compilationKindFor(toPath) }.getOrNull() - val fromEnv = fromKind?.let { compiler?.compilationEnvironmentFor(it) } - val toEnv = toKind?.let { compiler?.compilationEnvironmentFor(it) } + val fromEnv = runCatching { compilationEnvironmentFor(fromPath) }.getOrNull() + val toEnv = runCatching { compilationEnvironmentFor(toPath) }.getOrNull() - if (fromKind != null && fromEnv == toEnv && toEnv != null) { + if (fromEnv != null && fromEnv === toEnv) { // file was renamed within the same compilation environment toEnv.onFileMoved(fromPath, toPath) return@launch diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt index 191c665976..ef035da0c0 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/BaseKotlinCodeAction.kt @@ -13,9 +13,9 @@ import com.itsaky.androidide.actions.requireContext import com.itsaky.androidide.actions.requireFile import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer -import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction -import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticExtra -import com.itsaky.androidide.lsp.kotlin.diagnostic.asAction +import com.itsaky.androidide.lsp.kotlin.api.DiagnosticAction +import com.itsaky.androidide.lsp.kotlin.api.KotlinDiagnosticExtra +import com.itsaky.androidide.lsp.kotlin.api.asAction import com.itsaky.androidide.lsp.models.DiagnosticItem import com.itsaky.androidide.lsp.models.DiagnosticsInSelection import com.itsaky.androidide.utils.DocumentUtils @@ -64,7 +64,7 @@ abstract class BaseKotlinCodeAction : EditorActionItem { get() ?.client - internal inline fun DiagnosticItem.ktExtra(): KotlinDiagnosticExtra? = + inline fun DiagnosticItem.ktExtra(): KotlinDiagnosticExtra? = (extra as? KotlinDiagnosticExtra<*>)?.asAction() /** @@ -72,7 +72,7 @@ abstract class BaseKotlinCodeAction : EditorActionItem { * Prefers [DiagnosticsInSelection] (any matching diagnostic in the selected area); falls back * to the at-selection-start [DiagnosticItem] when no container is present. */ - protected inline fun ActionData.findFirstDiagnosticItem(predicate: (DiagnosticItem) -> Boolean): DiagnosticItem? = + inline fun ActionData.findFirstDiagnosticItem(predicate: (DiagnosticItem) -> Boolean): DiagnosticItem? = get() ?.let { return it.diagnostics.firstOrNull(predicate) } ?: get()?.takeIf(predicate) @@ -81,7 +81,7 @@ abstract class BaseKotlinCodeAction : EditorActionItem { * Find the first in-selection [DiagnosticItem] whose extra carries a [T] action, paired with the * typed [KotlinDiagnosticExtra]. Carries the type evidence through so callers don't re-extract. */ - internal inline fun ActionData.findDiagnosticExtra(): Pair>? { + inline fun ActionData.findDiagnosticExtra(): Pair>? { val item = findFirstDiagnosticItem { it.ktExtra() != null } ?: return null return item to item.ktExtra()!! } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt deleted file mode 100644 index 7d07076842..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/WorkspaceExts.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler - -import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtLibraryModule -import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule -import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtSourceModule -import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtLibraryModule -import com.itsaky.androidide.lsp.kotlin.compiler.modules.buildKtSourceModule -import com.itsaky.androidide.projects.api.AndroidModule -import com.itsaky.androidide.projects.api.ModuleProject -import com.itsaky.androidide.projects.api.Workspace -import com.itsaky.androidide.projects.models.bootClassPaths -import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment -import org.jetbrains.kotlin.com.intellij.openapi.project.Project -import org.slf4j.LoggerFactory -import java.nio.file.Path -import kotlin.io.path.pathString - -private val logger = LoggerFactory.getLogger("WorkspaceExts") - -internal fun Workspace.collectKtModules( - project: Project, - appEnv: CoreApplicationEnvironment -): List = buildList { - fun addModule(module: KtModule) = add(module) - - val moduleProjects = subProjects - .asSequence() - .filterIsInstance() - .filter { it.path != rootProject.path } - - val jarToModMap = mutableMapOf() - - fun addLibrary(path: Path): KtLibraryModule { - val module = buildKtLibraryModule(project, appEnv) { - id = path.pathString - addContentRoot(path) - } - jarToModMap[path] = module - return module - } - - val bootClassPaths = moduleProjects - .filterIsInstance() - .flatMap { project -> - project.bootClassPaths - .asSequence() - .filter { it.exists() } - .map { it.toPath() } - .map(::addLibrary) - } - - val libraryDependencies = moduleProjects - .flatMap { it.getCompileClasspaths() } - .filter { it.exists() } - .map { it.toPath() } - .associateWith(::addLibrary) - - val subprojectsAsModules = mutableMapOf() - val sourceRootToModuleMap = mutableMapOf() - - fun getOrCreateModule(moduleProject: ModuleProject): KtSourceModule { - subprojectsAsModules[moduleProject]?.let { return it } - - val module = buildKtSourceModule(project) { - this.module = moduleProject - - bootClassPaths.forEach { addDependency(it) } - - moduleProject.getCompileClasspaths(excludeSourceGeneratedClassPath = true) - .forEach { classpath -> - val libDep = libraryDependencies[classpath.toPath()] - if (libDep == null) { - logger.error( - "Skipping non-existent classpath classpath: {}", - classpath - ) - return@forEach - } - addDependency(libDep) - } - - moduleProject.getCompileModuleProjects().forEach { dep -> - addDependency(getOrCreateModule(dep)) - } - } - - subprojectsAsModules[moduleProject] = module - module.contentRoots.forEach { root -> sourceRootToModuleMap[root] = module } - return module - } - - moduleProjects.forEach { addModule(getOrCreateModule(it)) } -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt deleted file mode 100644 index 29e3d94a10..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/IndexCommand.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler.index - -import org.jetbrains.kotlin.com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.kotlin.psi.KtFile -import java.nio.file.Path - -internal sealed interface IndexCommand { - data object Stop : IndexCommand - data object SourceScanningComplete: IndexCommand - data object IndexingComplete: IndexCommand - data class ScanSourceFile(val vf: VirtualFile): IndexCommand - data class IndexModifiedFile(val ktFile: KtFile): IndexCommand - data class IndexSourceFile(val vf: VirtualFile): IndexCommand - data class RemoveFromIndex(val path: Path): IndexCommand -} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt deleted file mode 100644 index 3f2c06fdec..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/index/ScanningWorker.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler.index - -import com.itsaky.androidide.lsp.kotlin.compiler.CompilationKind -import com.itsaky.androidide.lsp.kotlin.compiler.modules.KtModule -import com.itsaky.androidide.lsp.kotlin.compiler.modules.asFlatSequence -import com.itsaky.androidide.lsp.kotlin.compiler.modules.isSourceModule -import com.itsaky.androidide.lsp.kotlin.utils.toNioPathOrNull -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.isActive -import org.appdevforall.codeonthego.indexing.jvm.JvmSymbolIndex -import org.slf4j.LoggerFactory - -internal class ScanningWorker( - private val kind: CompilationKind, - private val sourceIndex: JvmSymbolIndex, - private val indexWorker: IndexWorker, - private val modules: List, -) { - - companion object { - private val logger = LoggerFactory.getLogger(ScanningWorker::class.java) - } - - suspend fun scan() = coroutineScope { - val sourceFiles = modules.asFlatSequence() - .filter { it.isSourceModule } - .flatMap { it.computeFiles(extended = true) } - .filter { - it.toNioPathOrNull()?.let { path -> kind.acceptsFile(path) } ?: run { - logger.warn("rejecting {} from kt source index", it) - false - } - } - .takeWhile { isActive } - .toList() - - sourceIndex.setActiveSources( - sourceFiles - .asSequence() - .map { it.path } - .takeWhile { isActive } - .toSet() - ) - - for (sourceFile in sourceFiles) { - if (!isActive) return@coroutineScope - indexWorker.submitCommand(IndexCommand.ScanSourceFile(sourceFile)) - } - - indexWorker.submitCommand(IndexCommand.SourceScanningComplete) - - sourceFiles.asSequence() - .takeWhile { isActive } - .forEach { sourceFile -> - indexWorker.submitCommand(IndexCommand.IndexSourceFile(sourceFile)) - } - - indexWorker.submitCommand(IndexCommand.IndexingComplete) - } -} \ No newline at end of file diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt deleted file mode 100644 index bc88cd8739..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/NoOpAsyncExecutionService.kt +++ /dev/null @@ -1,157 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package com.itsaky.androidide.lsp.kotlin.compiler.services - -import org.jetbrains.concurrency.CancellablePromise -import org.jetbrains.concurrency.Promise -import org.jetbrains.kotlin.com.intellij.openapi.Disposable -import org.jetbrains.kotlin.com.intellij.openapi.application.AppUIExecutor -import org.jetbrains.kotlin.com.intellij.openapi.application.ApplicationManager -import org.jetbrains.kotlin.com.intellij.openapi.application.AsyncExecutionService -import org.jetbrains.kotlin.com.intellij.openapi.application.ExpirableExecutor -import org.jetbrains.kotlin.com.intellij.openapi.application.ModalityState -import org.jetbrains.kotlin.com.intellij.openapi.application.NonBlockingReadAction -import org.jetbrains.kotlin.com.intellij.openapi.progress.ProgressIndicator -import org.jetbrains.kotlin.com.intellij.openapi.project.Project -import org.jetbrains.kotlin.com.intellij.util.Function -import java.util.concurrent.Callable -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executor -import java.util.concurrent.ForkJoinPool -import java.util.concurrent.TimeUnit -import java.util.function.BooleanSupplier -import java.util.function.Consumer - -/** - * No-op [AsyncExecutionService] for standalone (non-IDE) environments. - * - * The real implementation requires an IDE event-dispatch / write-thread infrastructure that does - * not exist in our standalone setup. Submitted tasks are run asynchronously on the common - * ForkJoin pool so that stub rebuilds triggered by structural PSI changes don't block the - * analysis thread or deadlock against the project read/write lock. - */ -internal class NoOpAsyncExecutionService : AsyncExecutionService() { - - private val executor: AppUIExecutor = NoOpAppUIExecutor() - - override fun createExecutor(backgroundExecutor: Executor): ExpirableExecutor = - NoOpExpirableExecutor(backgroundExecutor) - - override fun createUIExecutor(modalityState: ModalityState): AppUIExecutor = executor - - override fun createWriteThreadExecutor(modalityState: ModalityState): AppUIExecutor = executor - - override fun buildNonBlockingReadAction(callable: Callable): NonBlockingReadAction = - NoOpNonBlockingReadAction(callable) -} - -private class NoOpAppUIExecutor : AppUIExecutor { - override fun later(): AppUIExecutor = this - override fun withDocumentsCommitted(project: Project): AppUIExecutor = this - override fun inSmartMode(project: Project): AppUIExecutor = this - override fun expireWith(disposable: Disposable): AppUIExecutor = this - - override fun execute(runnable: Runnable) { - ApplicationManager.getApplication().invokeLater(runnable) - } - - override fun submit(callable: Callable): CancellablePromise { - val future = CompletableFuture() - ApplicationManager.getApplication().invokeLater { - try { future.complete(callable.call()) } - catch (e: Throwable) { future.completeExceptionally(e) } - } - return future.asCancellablePromise() - } - - override fun submit(runnable: Runnable): CancellablePromise<*> { - val future = CompletableFuture() - ApplicationManager.getApplication().invokeLater { - try { runnable.run(); future.complete(null) } - catch (e: Throwable) { future.completeExceptionally(e) } - } - return future.asCancellablePromise() - } -} - -private class NoOpExpirableExecutor(private val exec: Executor) : ExpirableExecutor { - override fun expireWith(disposable: Disposable): ExpirableExecutor = this - - override fun execute(runnable: Runnable) = exec.execute(runnable) - - override fun submit(callable: Callable): CancellablePromise = - CompletableFuture.supplyAsync({ callable.call() }, exec).asCancellablePromise() - - override fun submit(runnable: Runnable): CancellablePromise<*> = - CompletableFuture.runAsync(runnable, exec).thenApply { null }.asCancellablePromise() -} - -private class NoOpNonBlockingReadAction(private val callable: Callable) : NonBlockingReadAction { - override fun inSmartMode(project: Project): NonBlockingReadAction = this - override fun withDocumentsCommitted(project: Project): NonBlockingReadAction = this - override fun expireWhen(condition: BooleanSupplier): NonBlockingReadAction = this - override fun wrapProgress(indicator: ProgressIndicator): NonBlockingReadAction = this - override fun expireWith(disposable: Disposable): NonBlockingReadAction = this - override fun finishOnUiThread(modalityState: ModalityState, uiThreadAction: Consumer): NonBlockingReadAction = this - override fun coalesceBy(vararg equality: Any): NonBlockingReadAction = this - - override fun submit(backgroundThreadExecutor: Executor): CancellablePromise = - CompletableFuture.supplyAsync({ callable.call() }, backgroundThreadExecutor) - .asCancellablePromise() - - override fun executeSynchronously(): T = callable.call() -} - -private fun CompletableFuture.asCancellablePromise(): CancellablePromise = - CompletableFutureCancellablePromise(this) - -private class CompletableFutureCancellablePromise( - private val future: CompletableFuture -) : CancellablePromise { - - // Future - override fun cancel(mayInterruptIfRunning: Boolean): Boolean = future.cancel(mayInterruptIfRunning) - override fun isCancelled(): Boolean = future.isCancelled - override fun isDone(): Boolean = future.isDone - override fun get(): T = future.get() - override fun get(timeout: Long, unit: TimeUnit): T = future.get(timeout, unit) - - // CancellablePromise - override fun cancel() { future.cancel(true) } - - override fun onSuccess(handler: Consumer): CancellablePromise { - future.thenAccept(handler) - return this - } - - override fun onError(handler: Consumer): CancellablePromise { - future.exceptionally { e -> handler.accept(e); null } - return this - } - - override fun onProcessed(handler: Consumer): CancellablePromise { - future.whenComplete { value, _ -> handler.accept(value) } - return this - } - - // Promise - override fun getState(): Promise.State = when { - future.isCancelled || future.isCompletedExceptionally -> Promise.State.REJECTED - future.isDone -> Promise.State.SUCCEEDED - else -> Promise.State.PENDING - } - - override fun processed(child: Promise): Promise = this - - override fun blockingGet(timeout: Int, unit: TimeUnit): T = future.get(timeout.toLong(), unit) - - override fun then(handler: Function): Promise = - future.thenApply { handler.`fun`(it) }.asCancellablePromise() - - override fun thenAsync( - handler: Function> - ): Promise = future.thenCompose { value -> - @Suppress("UNCHECKED_CAST") - (handler.`fun`(value) as CompletableFutureCancellablePromise).future - }.asCancellablePromise() -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt deleted file mode 100644 index dac66e7160..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/util/SLF4JLogger.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.compiler.util - -import org.jetbrains.kotlin.com.intellij.openapi.diagnostic.Logger -import org.slf4j.LoggerFactory - -class SLF4JLogger(name: String) : Logger() { - private val logger = LoggerFactory.getLogger(name) - - override fun isDebugEnabled(): Boolean { - return logger.isDebugEnabled - } - - override fun debug(message: String?, t: Throwable?) { - if (t != null) { - logger.debug(message ?: "", t) - } else { - logger.debug(message ?: "") - } - } - - override fun info(message: String?, t: Throwable?) { - if (t != null) { - logger.info(message ?: "", t) - } else { - logger.info(message ?: "") - } - } - - override fun warn(message: String?, t: Throwable?) { - if (t != null) { - logger.warn(message ?: "", t) - } else { - logger.warn(message ?: "") - } - } - - override fun error(message: String?, t: Throwable?, vararg details: String?) { - val msg = message ?: "" - val detailStr = if (details.isNotEmpty()) { - details.filterNotNull().joinToString(System.lineSeparator()) - } else { - "" - } - - val fullMessage = if (detailStr.isNotEmpty()) { - if (msg.isNotEmpty()) "$msg: $detailStr" else detailStr - } else { - msg - } - - if (t != null) { - logger.error(fullMessage, t) - } else { - logger.error(fullMessage) - } - } -} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt new file mode 100644 index 0000000000..bef762bc93 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt @@ -0,0 +1,120 @@ +package com.itsaky.androidide.lsp.kotlin.loader + +import android.content.Context +import com.itsaky.androidide.lsp.api.ILanguageClient +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSession +import com.itsaky.androidide.lsp.kotlin.api.IKotlinCompilerSessionFactory +import com.itsaky.androidide.projects.api.Workspace +import dalvik.system.DexClassLoader +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Path + +/** + * Extracts the Kotlin Analysis API carrier APK from assets and loads it via + * [DexClassLoader] on first use, so the ~28MB Analysis API dependency graph + * (ADFA-5010) is never resident in the main app dex or classloaded until a Kotlin + * file is actually opened. Mirrors `PluginLoader`'s DexClassLoader construction. + */ +class KotlinCompilerLoader( + private val context: Context, +) { + @Volatile + private var session: IKotlinCompilerSession? = null + + private val carrierApk: File by lazy { extractCarrierApk() } + + private fun extractCarrierApk(): File { + val dir = context.getDir(CARRIER_DIR_NAME, Context.MODE_PRIVATE) + val dest = File(dir, CARRIER_APK_ASSET_NAME) + val markerFile = File(dir, "$CARRIER_APK_ASSET_NAME.marker") + + // The main APK's own mtime as a cheap "has the app been updated/reinstalled since we + // last extracted" marker -- avoids re-extracting on every process start. + val currentMarker = File(context.applicationInfo.sourceDir).lastModified().toString() + val previousMarker = runCatching { markerFile.readText() }.getOrNull() + + if (!dest.exists() || previousMarker != currentMarker) { + logger.info("Extracting Kotlin compiler carrier APK to {}", dest) + context.assets.open(CARRIER_APK_ASSET_NAME).use { input -> + dest.outputStream().use { output -> input.copyTo(output) } + } + markerFile.writeText(currentMarker) + } + + return dest + } + + /** + * Returns the current session, creating one (extracting the carrier APK and + * `DexClassLoader`-loading it if needed) on first call. Blocking, like the + * `Compiler` construction it replaces was already blocking when it ran eagerly + * inside `setupWithProject`. + */ + fun getOrCreateSession( + workspace: Workspace, + jdkHome: Path, + jdkRelease: Int, + jdkVersionString: String, + languageClient: ILanguageClient?, + ): IKotlinCompilerSession { + session?.let { return it } + + synchronized(this) { + session?.let { return it } + + val optimizedDir = File(context.codeCacheDir, "kotlin_compiler_dex").apply { mkdirs() } + val classLoader = + DexClassLoader( + carrierApk.absolutePath, + optimizedDir.absolutePath, + null, + this::class.java.classLoader, + ) + + // The Analysis API/IntelliJ-platform machinery inside the carrier dex relies on + // context-classloader-default `ServiceLoader.load(...)` for its own internal + // extension-point wiring; without this, those lookups silently return empty + // (not an exception) against the wrong (parent) classloader. + val previousContextClassLoader = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = classLoader + try { + val factory = + classLoader + .loadClass(FACTORY_CLASS_NAME) + .getDeclaredConstructor() + .newInstance() as IKotlinCompilerSessionFactory + + val created = + factory.create( + workspace = workspace, + intellijPluginRoot = carrierApk.toPath(), + jdkHome = jdkHome, + jdkRelease = jdkRelease, + jdkVersionString = jdkVersionString, + languageClient = languageClient, + ) + + session = created + return created + } finally { + Thread.currentThread().contextClassLoader = previousContextClassLoader + } + } + } + + fun currentSession(): IKotlinCompilerSession? = session + + fun close() { + session?.close() + session = null + } + + companion object { + private const val CARRIER_DIR_NAME = "kotlin_compiler" + private const val CARRIER_APK_ASSET_NAME = "kotlin-compiler-carrier.apk" + private const val FACTORY_CLASS_NAME = + "com.itsaky.androidide.lsp.kotlin.compiler.KotlinCompilerSessionFactoryImpl" + private val logger = LoggerFactory.getLogger(KotlinCompilerLoader::class.java) + } +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt deleted file mode 100644 index c446c08c14..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ContextKeywords.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.utils - -import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext -import org.jetbrains.kotlin.lexer.KtKeywordToken -import org.jetbrains.kotlin.lexer.KtTokens - -object ContextKeywords { - - /** Hard keywords valid as *statement starters* inside a function body */ - val STATEMENT_KEYWORDS = setOf( - KtTokens.IF_KEYWORD, - KtTokens.ELSE_KEYWORD, - KtTokens.WHEN_KEYWORD, - KtTokens.WHILE_KEYWORD, - KtTokens.DO_KEYWORD, - KtTokens.FOR_KEYWORD, - KtTokens.TRY_KEYWORD, - KtTokens.RETURN_KEYWORD, - KtTokens.THROW_KEYWORD, - KtTokens.BREAK_KEYWORD, - KtTokens.CONTEXT_KEYWORD, - KtTokens.VAL_KEYWORD, - KtTokens.VAR_KEYWORD, - KtTokens.FUN_KEYWORD,// local declarations - KtTokens.OBJECT_KEYWORD,// anonymous / local object - KtTokens.CLASS_KEYWORD,// local class (rare but legal) - ) - - /** Declaration starters at top-level / class body */ - val DECLARATION_KEYWORDS = setOf( - KtTokens.VAL_KEYWORD, - KtTokens.VAR_KEYWORD, - KtTokens.FUN_KEYWORD, - KtTokens.CLASS_KEYWORD, - KtTokens.INTERFACE_KEYWORD, - KtTokens.OBJECT_KEYWORD, - KtTokens.TYPE_ALIAS_KEYWORD, - KtTokens.CONSTRUCTOR_KEYWORD, - KtTokens.INIT_KEYWORD, - ) - - val TOP_LEVEL_ONLY = setOf(KtTokens.PACKAGE_KEYWORD, KtTokens.IMPORT_KEYWORD) - - /** - * Resolve valid keywords for the given declaration context. - * - * @param ctx The declaration context. - * @return The keyword tokens for the declaration context. - */ - fun keywordsFor(ctx: DeclarationContext): Set = when (ctx) { - DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> TOP_LEVEL_ONLY + DECLARATION_KEYWORDS - - DeclarationContext.CLASS_BODY -> DECLARATION_KEYWORDS + - setOf(KtTokens.INIT_KEYWORD, KtTokens.CONSTRUCTOR_KEYWORD) - - DeclarationContext.INTERFACE_BODY -> setOf( - KtTokens.VAL_KEYWORD, - KtTokens.VAR_KEYWORD, - KtTokens.FUN_KEYWORD, - KtTokens.CLASS_KEYWORD, - KtTokens.INTERFACE_KEYWORD, - KtTokens.OBJECT_KEYWORD, - KtTokens.TYPE_ALIAS_KEYWORD - ) - - DeclarationContext.OBJECT_BODY, - DeclarationContext.ENUM_BODY -> DECLARATION_KEYWORDS - setOf(KtTokens.CONSTRUCTOR_KEYWORD) - - DeclarationContext.ANNOTATION_BODY -> setOf(KtTokens.VAL_KEYWORD) // annotation params only - - DeclarationContext.FUNCTION_BODY -> STATEMENT_KEYWORDS - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 29fb8afcd8..d1ead0d6e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -136,6 +136,8 @@ include( ":lsp:jvm-symbol-index", ":lsp:jvm-symbol-models", ":lsp:kotlin", + ":lsp:kotlin-api", + ":lsp:kotlin-compiler-impl", ":lsp:xml", ":profiler", ":subprojects:aapt2-proto", @@ -148,6 +150,7 @@ include( ":subprojects:hidden-apis-compat", ":subprojects:javac-services", ":subprojects:kotlin-analysis-api", + ":subprojects:kotlin-compiler-carrier", ":subprojects:libjdwp", ":subprojects:privileged-services", ":subprojects:projects", diff --git a/subprojects/kotlin-compiler-carrier/build.gradle.kts b/subprojects/kotlin-compiler-carrier/build.gradle.kts new file mode 100644 index 0000000000..fd4e35201f --- /dev/null +++ b/subprojects/kotlin-compiler-carrier/build.gradle.kts @@ -0,0 +1,25 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.application") + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.kotlincompilercarrier" + + // This APK is never installed -- only its classes.dex is read, via DexClassLoader, by + // KotlinCompilerLoader in lsp:kotlin. Shrinking/obfuscating it would require porting over + // kotlin-analysis-api's ~350 consumer-rules.pro keep rules for no benefit (see ADFA-3604 -- + // the same "ship intact rather than shrink" call already made for this dependency graph). + buildTypes { + release { + isMinifyEnabled = false + isShrinkResources = false + } + } +} + +dependencies { + implementation(projects.lsp.kotlinCompilerImpl) +} diff --git a/subprojects/kotlin-compiler-carrier/src/main/AndroidManifest.xml b/subprojects/kotlin-compiler-carrier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..27a1513334 --- /dev/null +++ b/subprojects/kotlin-compiler-carrier/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + From b17939e0620fb36bcbd6e8aa0301a80e5fc7f071 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Tue, 4 Aug 2026 18:42:15 -0700 Subject: [PATCH 03/11] ADFA-5010: Drop app's stray direct kotlin-analysis-api dep, clean up proguard, add ADR app/build.gradle.kts still had its own direct implementation(kotlinAnalysisApi) line independent of lsp/kotlin's (already removed) -- caught by dex-inspecting a real release build, which showed org.jetbrains.kotlin.analysis.** classes still resident in app's own classes*.dex despite the module split. Removing it drops app's total dex from 40.17MB to 31.24MB with zero Analysis API classes left in app's dex (confirmed moved intact into the carrier's dex instead). Also removes the now-pointless org.jetbrains.kotlin.**/Caffeine/kotlin.reflect/ kotlin.script/kotlinx.coroutines.internal/streamex/gnu.trove keep block and the compiler.services.** PicoContainer rule from app/proguard-rules.pro -- nothing on app's classpath needs them once the jar is gone; the carrier module doesn't run R8 at all, so it doesn't need them either. app:assembleV8Release verified green with these rules removed. Adds ADR 0011 documenting the lazy-load-via-DexClassLoader decision. Co-Authored-By: Claude Sonnet 5 --- app/build.gradle.kts | 1 - app/proguard-rules.pro | 36 ------------ ...-kotlin-analysis-api-via-dexclassloader.md | 58 +++++++++++++++++++ docs/adr/README.md | 1 + 4 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa67d0abad..f1223bdba5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -298,7 +298,6 @@ dependencies { implementation(projects.gradlePluginConfig) implementation(projects.subprojects.aaptcompiler) implementation(projects.subprojects.javacServices) - implementation(projects.subprojects.kotlinAnalysisApi) implementation(projects.subprojects.shizukuApi) implementation(projects.subprojects.shizukuManager) implementation(projects.subprojects.shizukuProvider) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7d9f1ad3da..930be5c990 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -29,42 +29,6 @@ # Builder model implementations -keep class com.itsaky.androidide.builder.model.** { *; } -# lsp/kotlin registers its own IntelliJ project/application services -- some -# by class name in lsp/kotlin/src/main/resources/META-INF/kt-lsp/kt-lsp.xml, -# the rest via ::class literals in -# lsp/kotlin/.../registrar/AnalysisApiServiceProviders.kt, which PicoContainer -# then instantiates reflectively via each class's no-arg constructor. A -# ::class literal doesn't count as an actual `new` call to R8, so it kept -# stripping "unused" no-arg constructors one class at a time as each was -# discovered on-device (ClassNotFoundException on DirectInheritorsProvider, -# then a PicoInitializationException on ModuleDependentsProvider's missing -# constructor). Keep the whole package rather than list every implementation -# class in AnalysisApiServiceProviders.kt individually. --keep class com.itsaky.androidide.lsp.kotlin.compiler.services.** { *; } - -# Kotlin Analysis API (bundled in subprojects/kotlin-analysis-api, used by the -# Kotlin LSP). subprojects/kotlin-analysis-api/consumer-rules.pro keeps every -# class this jar's own IntelliJ plugin XML descriptors and ServiceLoader -# entries reference by name, but on-device testing kept surfacing distinct -# reflection paths that narrow list didn't cover -- not just inside this jar, -# but in other lsp/kotlin runtime dependencies too (Caffeine picks a cache -# implementation from dozens of codegenned variant classes at runtime; a -# protobuf-lite message field is resolved by name string; lsp/kotlin's own -# kt-lsp.xml above; and a NullPointerException deep in IntelliJ's own -# JavaCoreApplicationEnvironment bootstrap, verified absent on an unshrunk -# debug build). Each fix was quick but the next gap kept appearing elsewhere -# in the same dependency graph, so rather than keep discovering them one -# on-device crash at a time, keep every runtime dependency lsp/kotlin pulls in -# whole. This gives up the dex-size reduction for this whole dependency graph; -# see ADFA-3604 for the size trade-off. --keep class org.jetbrains.kotlin.** { *; } --keep class com.github.benmanes.caffeine.** { *; } --keep class kotlin.reflect.** { *; } --keep class kotlin.script.** { *; } --keep class kotlinx.coroutines.internal.** { *; } --keep class one.util.streamex.** { *; } --keep class gnu.trove.** { *; } - # Eclipse -keep class org.eclipse.** { *; } diff --git a/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md b/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md new file mode 100644 index 0000000000..362eb29c84 --- /dev/null +++ b/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md @@ -0,0 +1,58 @@ +# 0011. Lazy-load the Kotlin Analysis API via a carrier APK + DexClassLoader + +- **Status:** Proposed +- **Date:** 2026-08-04 +- **Deciders:** Code On The Go team + +## Context + +Per ADFA-4549's release-build DEX analysis, the embedded Kotlin Analysis API accounted for the largest single share of the app's DEX: tens of MiB and tens of thousands of classes, more than everything else in the app combined. It was always resident: merged into `app`'s `classes*.dex` by D8 at build time, and loaded/verified on every app run whether or not the user ever opened a Kotlin file. + +What's actually embedded is not a build-time compiler. Full Gradle builds run real Gradle out-of-process via the Tooling API ([ADR 0002](0002-on-device-builds-via-gradle-tooling-api.md)) and never touch this dependency. What's embedded is the Kotlin Analysis API (FIR-based, `analysis-api-standalone-embeddable-for-ide`), used purely for in-process live-editing: completion, diagnostics, navigation ([ADR 0010](0010-navigation-resolves-via-analysis-api.md)), and signature help in the Kotlin language server. It was also constructed eagerly — `KotlinLanguageServer.setupWithProject()` built a full `Compiler`/`CompilationEnvironment` on every project open, even for pure-Java projects. + +**Three independently-versioned Kotlin artifacts exist in this codebase — do not conflate them:** +1. This repo's own Kotlin Gradle plugin (`2.3.0`, `gradle/libs.versions.toml`), used to compile CodeOnTheGo's own source. +2. The embedded Analysis API fork isolated by this decision (`2.3.255`, `analysis-api-standalone-embeddable-for-ide`, downloaded from `appdevforall/kotlin-android`). +3. The on-device Gradle build toolchain's own Kotlin compiler (`1.9.x`), used when the app builds a *user's* project on-device, out-of-process, per ADR 0002. Unrelated to both of the above; further confirmation that on-device builds never touch the Analysis API. + +Separately: `app/proguard-rules.pro`'s `-dontoptimize`/`-dontobfuscate` (ADFA-3604) is caused by the app's *own* first-party source compiled by the repo's 2.3.0 plugin (the cited corruption example, `utils.TestModeUtilsKt.isTestMode`, is app code, not a class from the Analysis API jar) — isolating the jar does not remove the need for that workaround, and it stays. + +## Decision + +**Isolate the Analysis API and everything that directly references it behind a `DexClassLoader` boundary, loaded lazily on first Kotlin-file interaction, mirroring the app's existing plugin-loading pattern.** + +Any class that directly imports Analysis API types must live in the *same* dex/classloader as the Analysis API itself — a resident class in the main dex cannot statically reference a type that only exists in a lazily-loaded child dex. So isolating just the downloaded jar isn't enough; the whole `lsp/kotlin` compiler-facing surface has to move with it, behind a bridge interface the always-resident editor code calls through. + +- **`lsp/kotlin-api`** — the bridge: `IKotlinCompilerSession`, `IKotlinCompilationEnvironment`, `IKotlinCompilerSessionFactory`, plus small shared value types (`DiagnosticAction`, `KotlinDiagnosticExtra`). Resident. Depended on by both sides. +- **`lsp/kotlin-compiler-impl`** — the isolated payload: `compiler/`, `completion/`, `diagnostic/`, `navigation/`, `signaturehelp/`, `utils/`, and the four diagnostic-driven code actions (Add Import, Organize Imports, Null Safety, Implement Members) moved out of `lsp/kotlin`. Implements the bridge interfaces against the real `Compiler`/`CompilationEnvironment`. Depends `implementation` on `subprojects/kotlin-analysis-api` (the only dependency actually bundled here); everything resident it needs (`lsp/kotlin`, `lsp/api`, `lsp/models`, `lsp/jvm-symbol-index`, etc.) is `compileOnly` — those types are already loaded by the parent (main app) classloader by the time the carrier dex runs, so `DexClassLoader`'s normal parent-first delegation resolves them there. +- **`subprojects/kotlin-compiler-carrier`** — a `com.android.application` module (`isMinifyEnabled = false`, matching the existing "ship intact, don't shrink" call for this dependency graph) whose only purpose is to produce a real, D8-dexed APK from `lsp/kotlin-compiler-impl`. Never installed or launched; `app`'s build copies its output straight into `app/src/main/assets/kotlin-compiler-carrier.apk` (`copyKotlinCompilerCarrierToAssets`, wired into `preBuild`) rather than through the brotli-compressed installer-zip pipeline used for the JDK/Android SDK — this is a few MB, not hundreds, and belongs in the base APK unconditionally. +- **`lsp/kotlin`** (resident) — `KotlinLanguageServer` becomes a thin wrapper. `KotlinCompilerLoader` extracts the carrier APK from assets to a stable private file on first use and loads it via `DexClassLoader(apkPath, optimizedDir, null, parent)`, the same construction `PluginLoader` already uses for optional plugin APKs. The trigger point moves from `setupWithProject()` (every project open, Kotlin or not) to the first call actually gated on a `.kt` file — `onDocumentOpen`, `complete()`, `analyze()`, `findDefinition()`, `signatureHelp()` — which also fixes the pre-existing bug where every project open paid the Analysis API bootstrap cost even for pure-Java projects. + +Two risks specific to this boundary, both mitigated in `KotlinCompilerLoader`: +- **`intellijPluginRoot`** (the path `Compiler`/`CompilationEnvironment` read `kt-lsp.xml`'s plugin descriptor from) must point at the carrier APK's own extracted path, not `context.applicationInfo.sourceDir` — the descriptor now lives inside the carrier, not the main app APK. +- **Context-classloader mismatch**: the Analysis API/IntelliJ-platform machinery uses context-classloader-default `ServiceLoader.load(...)` for its own internal extension-point wiring. `KotlinCompilerLoader` sets `Thread.currentThread().contextClassLoader` to the `DexClassLoader` around initialization and restores it after — without this, those lookups silently return empty against the wrong (parent) classloader rather than throwing, which is a much harder failure to diagnose than a crash. + +## Consequences + +**Positive** +- The Analysis API and its transitive graph (IntelliJ platform core, Guava, protobuf, aalto-xml shaded under it) are gone from `app`'s own `classes*.dex` entirely — confirmed via dex inspection of a real release build (zero `Lorg/jetbrains/kotlin/analysis/**` class descriptors remain in `app`'s dex; they moved intact into the carrier's dex). +- Projects that never open a Kotlin file never pay the classload, dex-verification, or `Compiler`/`CompilationEnvironment` construction cost. +- `app/proguard-rules.pro`'s Analysis-API-specific `-keep` rules (the `org.jetbrains.kotlin.**`/Caffeine/kotlin.reflect/kotlin.script/kotlinx.coroutines.internal/streamex/gnu.trove block, and the `compiler.services.**` PicoContainer rule) are removed — nothing in `app`'s own classpath needs them anymore. The carrier module needs no equivalent rules since it doesn't run R8 at all. + +**Negative / costs** +- First Kotlin-file interaction in a session now pays a one-time synchronous latency spike: asset extraction (first run only) + `DexClassLoader` construction + `Compiler` bootstrap. Previously this cost was paid unconditionally at project-open time instead. +- Three new Gradle modules and a real classloader boundary to reason about for anyone touching Kotlin LSP code going forward: a change to a bridge interface (`lsp/kotlin-api`) now requires updating both the resident caller and the isolated implementation, and any new resident type crossing the boundary must be added as `compileOnly` on the isolated side, not `implementation`/`api`, to avoid bundling dead-weight duplicate classes into the carrier dex. +- `subprojects/kotlin-analysis-api/consumer-rules.pro`'s ~350 keep rules become dead weight (never exercised, since the carrier never runs R8) but were left in place rather than deleted, since removing them isn't required for correctness and wasn't in this decision's scope. + +## Alternatives considered + +- **Reflectively patch the app's own `PathClassLoader.pathList`** (the technique the old `androidx.multidex.MultiDex.install()` support library used) — rejected: relies on non-SDK-API internals that Android has progressively locked down across releases, and the ticket's own wording specifies `DexClassLoader` as the mechanism. +- **Android Dynamic Feature modules / Play Feature Delivery** — rejected: this app must install offline/side-loaded (F-Droid, direct APK, GitHub releases), not only via Play Store, and `SplitInstallManager` is a Play-Store-install-only mechanism in production. +- **Ship the carrier jar through the existing brotli-compressed installer-zip pipeline** (`AssetsInstaller`/`BundledAssetsInstaller`, used for the JDK/Android SDK/Gradle distribution) — rejected: that framework exists for hundreds-of-MB optional first-run downloads with progress UI; this is a few MB that belongs in the base APK unconditionally, closer in spirit to how `plugin-api.jar` is already copied straight into assets at build time. + +## Related + +- [ADR 0002](0002-on-device-builds-via-gradle-tooling-api.md) — confirms on-device builds never touch this dependency. +- [ADR 0003](0003-vendored-forked-desktop-toolchain.md) — the analogous `javac`/`jaxp` in-process, always-resident toolchain; a similar lazy-load treatment has been proposed for it but is out of scope here. +- [ADR 0010](0010-navigation-resolves-via-analysis-api.md) — the navigation feature whose Analysis API dependency is exactly what moves behind this boundary. +- `plugin-manager/.../PluginLoader.kt` — the existing `DexClassLoader` pattern this decision extends. diff --git a/docs/adr/README.md b/docs/adr/README.md index 9bb6db0c4a..aa8903a426 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -24,3 +24,4 @@ Format is lightweight **MADR / Nygard**: Context → Decision → Consequences | [0008](0008-retain-androidide-namespace.md) | Retain the `com.itsaky.androidide` namespace after rebrand | Proposed | | [0009](0009-jetpack-compose-for-new-ui.md) | Build new UI in Jetpack Compose, not XML Views | Proposed | | [0010](0010-navigation-resolves-via-analysis-api.md) | Kotlin navigation resolves via the Analysis API, not the symbol index | Proposed | +| [0011](0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md) | Lazy-load the Kotlin Analysis API via a carrier APK + DexClassLoader | Proposed | From b06ccfe8e987128618e8ee5ff70186048cafda73 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Tue, 4 Aug 2026 18:55:34 -0700 Subject: [PATCH 04/11] ADFA-5010: Fix spotless ratchet violations in moved lsp/kotlin-compiler-impl files The ratchet is file-level against origin/stage: files moved to a new path under this refactor need full spotless compliance now, even for pre-existing style that was fine at their old path. - ContextKeywords.kt, completion/ModifierFilter.kt, utils/ModifierFilter.kt: replace `import ... KtTokens.*` wildcard imports with explicit per-symbol imports (standard:no-wildcard-imports). The exact member list was derived from the Kotlin compiler's own "unresolved reference" errors after removing the wildcard, not guessed, to avoid missing or misnaming a token. - AbstractKtModule.kt: rename the `_baseSearchScope` backing field to `baseSearchScope` (standard:backing-property-naming -- it backs `override val baseContentScope`, a different name, so the underscore convention didn't apply). `_contentScope` is untouched: it correctly backs `override val contentScope` and was already ktlint-clean. Note: spotlessCheck still reports one line-length violation on ResolutionScopeProvider.kt:L20 that doesn't correspond to any actual line over 140 chars in the file (verified by raw byte count, tab-width expansion, and non-ASCII scan; survives a Gradle daemon restart and --no-build-cache). Left uninvestigated further pending user input -- likely a spotless/ktlint tooling quirk in this environment, not a real formatting defect. Co-Authored-By: Claude Sonnet 5 --- .../compiler/modules/AbstractKtModule.kt | 40 ++-- .../lsp/kotlin/completion/ContextKeywords.kt | 116 +++++++--- .../lsp/kotlin/completion/ModifierFilter.kt | 203 ++++++++++++------ .../lsp/kotlin/utils/ModifierFilter.kt | 203 ++++++++++++------ 4 files changed, 396 insertions(+), 166 deletions(-) diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt index 9bd2f128cb..15ea4588bf 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/modules/AbstractKtModule.kt @@ -12,21 +12,21 @@ import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope internal abstract class AbstractKtModule( override val project: Project, override val directRegularDependencies: List, -) : KtModule, KaModuleBase() { - +) : KaModuleBase(), + KtModule { private val searchScopeLock = Any() - private var _baseSearchScope: GlobalSearchScope? = null + private var baseSearchScope: GlobalSearchScope? = null private var _contentScope: GlobalSearchScope? = null private fun maybeCreateScopesLocked() { - val searchScope = _baseSearchScope + val searchScope = baseSearchScope if (searchScope != null) { return } - _baseSearchScope = computeBaseContentScope() + baseSearchScope = computeBaseContentScope() _contentScope = KaContentScopeProvider.getInstance(project).getRefinedContentScope(this) - Sentry.addBreadcrumb("createSearchScopes(mod=$this, base=${_baseSearchScope?.hashCode()}, content=${_contentScope?.hashCode()})") + Sentry.addBreadcrumb("createSearchScopes(mod=$this, base=${baseSearchScope?.hashCode()}, content=${_contentScope?.hashCode()})") } /** @@ -45,22 +45,24 @@ internal abstract class AbstractKtModule( fun invalidateSearchScope() { synchronized(searchScopeLock) { Sentry.addBreadcrumb("invalidateSearchScope(mod=$this)") - _baseSearchScope = null + baseSearchScope = null _contentScope = null } } override val baseContentScope: GlobalSearchScope - get() = synchronized(searchScopeLock) { - maybeCreateScopesLocked() - checkNotNull(_baseSearchScope) { "failed to create base content scope" } - } + get() = + synchronized(searchScopeLock) { + maybeCreateScopesLocked() + checkNotNull(baseSearchScope) { "failed to create base content scope" } + } override val contentScope: GlobalSearchScope - get() = synchronized(searchScopeLock) { - maybeCreateScopesLocked() - checkNotNull(_contentScope) { "failed to create content refined scope" } - } + get() = + synchronized(searchScopeLock) { + maybeCreateScopesLocked() + checkNotNull(_contentScope) { "failed to create content refined scope" } + } override val directDependsOnDependencies: List get() = emptyList() @@ -70,9 +72,9 @@ internal abstract class AbstractKtModule( @OptIn(KaExperimentalApi::class) override val moduleDescription: String - get() = "module '$id' (ref=${hashCode()}, baseScope=${_baseSearchScope?.hashCode()}, contentScope=${_contentScope?.hashCode()}, deps=${directRegularDependencies.joinToString { it.id }})" + get() = + "module '$id' (ref=${hashCode()}, baseScope=${baseSearchScope?.hashCode()}, " + + "contentScope=${_contentScope?.hashCode()}, deps=${directRegularDependencies.joinToString { it.id }})" - override fun toString(): String { - return id - } + override fun toString(): String = id } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt index 345a3e904c..3ae6cb79e7 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt @@ -1,27 +1,67 @@ package com.itsaky.androidide.lsp.kotlin.completion import org.jetbrains.kotlin.lexer.KtKeywordToken -import org.jetbrains.kotlin.lexer.KtTokens.* +import org.jetbrains.kotlin.lexer.KtTokens.BREAK_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CLASS_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CONSTRUCTOR_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CONTEXT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.DO_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ELSE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FOR_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FUN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.IF_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.IMPORT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INIT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INTERFACE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.OBJECT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PACKAGE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.RETURN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.THROW_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.TRY_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.TYPE_ALIAS_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VAR_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.WHEN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.WHILE_KEYWORD /** * */ object ContextKeywords { - /** Hard keywords valid as *statement starters* inside a function body */ - val STATEMENT_KEYWORDS = setOf( - IF_KEYWORD, ELSE_KEYWORD, WHEN_KEYWORD, WHILE_KEYWORD, DO_KEYWORD, FOR_KEYWORD, - TRY_KEYWORD, RETURN_KEYWORD, THROW_KEYWORD, BREAK_KEYWORD, CONTEXT_KEYWORD, - VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD,// local declarations - OBJECT_KEYWORD,// anonymous / local object - CLASS_KEYWORD,// local class (rare but legal) - ) + val STATEMENT_KEYWORDS = + setOf( + IF_KEYWORD, + ELSE_KEYWORD, + WHEN_KEYWORD, + WHILE_KEYWORD, + DO_KEYWORD, + FOR_KEYWORD, + TRY_KEYWORD, + RETURN_KEYWORD, + THROW_KEYWORD, + BREAK_KEYWORD, + CONTEXT_KEYWORD, + VAL_KEYWORD, + VAR_KEYWORD, + FUN_KEYWORD, // local declarations + OBJECT_KEYWORD, // anonymous / local object + CLASS_KEYWORD, // local class (rare but legal) + ) /** Declaration starters at top-level / class body */ - val DECLARATION_KEYWORDS = setOf( - VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD, CLASS_KEYWORD, INTERFACE_KEYWORD, OBJECT_KEYWORD, - TYPE_ALIAS_KEYWORD, CONSTRUCTOR_KEYWORD, INIT_KEYWORD, - ) + val DECLARATION_KEYWORDS = + setOf( + VAL_KEYWORD, + VAR_KEYWORD, + FUN_KEYWORD, + CLASS_KEYWORD, + INTERFACE_KEYWORD, + OBJECT_KEYWORD, + TYPE_ALIAS_KEYWORD, + CONSTRUCTOR_KEYWORD, + INIT_KEYWORD, + ) val TOP_LEVEL_ONLY = setOf(PACKAGE_KEYWORD, IMPORT_KEYWORD) @@ -31,23 +71,45 @@ object ContextKeywords { * @param ctx The declaration context. * @return The keyword tokens for the declaration context. */ - fun keywordsFor(ctx: DeclarationContext): Set = when (ctx) { - DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> TOP_LEVEL_ONLY + DECLARATION_KEYWORDS + fun keywordsFor(ctx: DeclarationContext): Set = + when (ctx) { + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + TOP_LEVEL_ONLY + DECLARATION_KEYWORDS + } - DeclarationContext.CLASS_BODY -> DECLARATION_KEYWORDS + - setOf(INIT_KEYWORD, CONSTRUCTOR_KEYWORD) + DeclarationContext.CLASS_BODY -> { + DECLARATION_KEYWORDS + + setOf(INIT_KEYWORD, CONSTRUCTOR_KEYWORD) + } - DeclarationContext.INTERFACE_BODY -> setOf( - VAL_KEYWORD, VAR_KEYWORD, FUN_KEYWORD, CLASS_KEYWORD, - INTERFACE_KEYWORD, OBJECT_KEYWORD, TYPE_ALIAS_KEYWORD - ) + DeclarationContext.INTERFACE_BODY -> { + setOf( + VAL_KEYWORD, + VAR_KEYWORD, + FUN_KEYWORD, + CLASS_KEYWORD, + INTERFACE_KEYWORD, + OBJECT_KEYWORD, + TYPE_ALIAS_KEYWORD, + ) + } + + DeclarationContext.OBJECT_BODY, + DeclarationContext.ENUM_BODY, + -> { + DECLARATION_KEYWORDS - setOf(CONSTRUCTOR_KEYWORD) + } - DeclarationContext.OBJECT_BODY, - DeclarationContext.ENUM_BODY -> DECLARATION_KEYWORDS - setOf(CONSTRUCTOR_KEYWORD) + DeclarationContext.ANNOTATION_BODY -> { + setOf(VAL_KEYWORD) + } - DeclarationContext.ANNOTATION_BODY -> setOf(VAL_KEYWORD) // annotation params only + // annotation params only - DeclarationContext.FUNCTION_BODY -> STATEMENT_KEYWORDS - } + DeclarationContext.FUNCTION_BODY -> { + STATEMENT_KEYWORDS + } + } } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt index eeb98519cb..cfe6d68e21 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt @@ -2,30 +2,60 @@ package com.itsaky.androidide.lsp.kotlin.completion import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtModifierKeywordToken -import org.jetbrains.kotlin.lexer.KtTokens.* +import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ACTUAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ANNOTATION_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.COMPANION_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CROSSINLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.DATA_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ENUM_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXPECT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXTERNAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FINAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FUN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INFIX_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INNER_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.LATEINIT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.MODALITY_MODIFIERS +import org.jetbrains.kotlin.lexer.KtTokens.MODIFIER_KEYWORDS_ARRAY +import org.jetbrains.kotlin.lexer.KtTokens.NOINLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.OPERATOR_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.REIFIED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.SEALED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.SUSPEND_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.TAILREC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VALUE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VARARG_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VISIBILITY_MODIFIERS /** * Helper for filtering modifier keywords for keyword completions. */ object ModifierFilter { - /** * Returns which modifier keywords are valid to suggest given the * current context, declaration kind, and already-present modifiers. */ - fun validModifiers( - ctx: CursorContext, - ): Set { + fun validModifiers(ctx: CursorContext): Set { val (_, _, _, _, _, _, declCtx, declKind, existing, _) = ctx val candidates = MODIFIER_KEYWORDS_ARRAY.toMutableSet() candidates -= existing // remove mutually exclusive groups - if (VISIBILITY_MODIFIERS.types.any { it in existing }) + if (VISIBILITY_MODIFIERS.types.any { it in existing }) { candidates -= VISIBILITY_MODIFIERS.types() - if (MODALITY_MODIFIERS.types.any { it in existing }) + } + if (MODALITY_MODIFIERS.types.any { it in existing }) { candidates -= MODALITY_MODIFIERS.types() + } when (declCtx) { DeclarationContext.INTERFACE_BODY -> { @@ -40,61 +70,97 @@ object ModifierFilter { // local declarations: only a small subset of modifiers are legal candidates.retainAll( setOf( - INLINE_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - SUSPEND_KEYWORD, TAILREC_KEYWORD, - DATA_KEYWORD, // local data class (Kotlin 1.9+) + INLINE_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + SUSPEND_KEYWORD, + TAILREC_KEYWORD, + DATA_KEYWORD, // local data class (Kotlin 1.9+) INNER_KEYWORD, - ) + ), ) } DeclarationContext.OBJECT_BODY, DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> candidates -= INNER_KEYWORD // inner only valid inside a class + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + candidates -= INNER_KEYWORD + } + + // inner only valid inside a class - else -> Unit + else -> { + Unit + } } when (declKind) { DeclarationKind.PROPERTY_VAL -> { - candidates -= setOf( - LATEINIT_KEYWORD, // lateinit requires var - VARARG_KEYWORD, - NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, // lateinit requires var + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) // const only on top-level or companion object val - if (declCtx !in setOf( + if (declCtx !in + setOf( DeclarationContext.TOP_LEVEL, DeclarationContext.OBJECT_BODY, - DeclarationContext.SCRIPT_TOP_LEVEL + DeclarationContext.SCRIPT_TOP_LEVEL, ) - ) + ) { candidates -= CONST_KEYWORD + } } DeclarationKind.PROPERTY_VAR -> { - candidates -= setOf( - CONST_KEYWORD, // const requires val - VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + CONST_KEYWORD, // const requires val + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) } DeclarationKind.FUN -> { - candidates -= setOf( - LATEINIT_KEYWORD, CONST_KEYWORD, VARARG_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, + CONST_KEYWORD, + VARARG_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) // abstract fun can't be inline/tailrec/external simultaneously if (ABSTRACT_KEYWORD in existing) { candidates -= setOf(INLINE_KEYWORD, TAILREC_KEYWORD, EXTERNAL_KEYWORD) @@ -102,16 +168,23 @@ object ModifierFilter { } DeclarationKind.CLASS -> { - candidates -= setOf( - LATEINIT_KEYWORD, CONST_KEYWORD, - VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - REIFIED_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, + CONST_KEYWORD, + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + REIFIED_KEYWORD, + ) // sealed is a modality modifier and conflicts with open/final/abstract - if (SEALED_KEYWORD in existing) + if (SEALED_KEYWORD in existing) { candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) + } // value class requires @JvmInline in practice, but `value` keyword is valid } @@ -120,22 +193,32 @@ object ModifierFilter { // interfaces are implicitly abstract; most modifiers don't apply candidates.retainAll( setOf( - PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - EXPECT_KEYWORD, ACTUAL_KEYWORD, - SEALED_KEYWORD, // sealed interface - EXTERNAL_KEYWORD, FUN_KEYWORD, // fun interface - ) + PUBLIC_KEYWORD, + PROTECTED_KEYWORD, + PRIVATE_KEYWORD, + INTERNAL_KEYWORD, + EXPECT_KEYWORD, + ACTUAL_KEYWORD, + SEALED_KEYWORD, // sealed interface + EXTERNAL_KEYWORD, + FUN_KEYWORD, // fun interface + ), ) } DeclarationKind.OBJECT -> { candidates.retainAll( setOf( - PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - EXPECT_KEYWORD, ACTUAL_KEYWORD, EXTERNAL_KEYWORD, - DATA_KEYWORD, // data object (Kotlin 1.9+) + PUBLIC_KEYWORD, + PROTECTED_KEYWORD, + PRIVATE_KEYWORD, + INTERNAL_KEYWORD, + EXPECT_KEYWORD, + ACTUAL_KEYWORD, + EXTERNAL_KEYWORD, + DATA_KEYWORD, // data object (Kotlin 1.9+) COMPANION_KEYWORD, - ) + ), ) } @@ -150,7 +233,9 @@ object ModifierFilter { // the exclusion rules above already handled mutual exclusions. } - else -> Unit + else -> { + Unit + } } // expect and actual are mutually exclusive @@ -166,9 +251,7 @@ object ModifierFilter { return candidates } - private fun TokenSet.types(): Set = - types.filterIsInstance().toSet() + private fun TokenSet.types(): Set = types.filterIsInstance().toSet() - private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = - this.contains(token) + private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt index 6284f3ddd3..2a970e8a38 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt @@ -4,20 +4,48 @@ import com.itsaky.androidide.lsp.kotlin.completion.DeclarationContext import com.itsaky.androidide.lsp.kotlin.completion.DeclarationKind import org.jetbrains.kotlin.com.intellij.psi.tree.TokenSet import org.jetbrains.kotlin.lexer.KtModifierKeywordToken -import org.jetbrains.kotlin.lexer.KtTokens.* +import org.jetbrains.kotlin.lexer.KtTokens.ABSTRACT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ACTUAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ANNOTATION_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.COMPANION_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CONST_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.CROSSINLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.DATA_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.ENUM_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXPECT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.EXTERNAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FINAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.FUN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INFIX_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INNER_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.INTERNAL_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.LATEINIT_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.MODALITY_MODIFIERS +import org.jetbrains.kotlin.lexer.KtTokens.MODIFIER_KEYWORDS_ARRAY +import org.jetbrains.kotlin.lexer.KtTokens.NOINLINE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.OPEN_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.OPERATOR_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PRIVATE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PROTECTED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.PUBLIC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.REIFIED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.SEALED_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.SUSPEND_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.TAILREC_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VALUE_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VARARG_KEYWORD +import org.jetbrains.kotlin.lexer.KtTokens.VISIBILITY_MODIFIERS /** * Helper for filtering modifier keywords for keyword completions. */ internal object ModifierFilter { - /** * Returns which modifier keywords are valid to suggest given the * current context, declaration kind, and already-present modifiers. */ - fun validModifiers( - ctx: AnalysisContext, - ): Set { + fun validModifiers(ctx: AnalysisContext): Set { val existing = ctx.existingModifiers val declCtx = ctx.declarationContext val declKind = ctx.declarationKind @@ -26,10 +54,12 @@ internal object ModifierFilter { candidates -= existing // remove mutually exclusive groups - if (VISIBILITY_MODIFIERS.types.any { it in existing }) + if (VISIBILITY_MODIFIERS.types.any { it in existing }) { candidates -= VISIBILITY_MODIFIERS.types() - if (MODALITY_MODIFIERS.types.any { it in existing }) + } + if (MODALITY_MODIFIERS.types.any { it in existing }) { candidates -= MODALITY_MODIFIERS.types() + } when (declCtx) { DeclarationContext.INTERFACE_BODY -> { @@ -44,61 +74,97 @@ internal object ModifierFilter { // local declarations: only a small subset of modifiers are legal candidates.retainAll( setOf( - INLINE_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - SUSPEND_KEYWORD, TAILREC_KEYWORD, - DATA_KEYWORD, // local data class (Kotlin 1.9+) + INLINE_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + SUSPEND_KEYWORD, + TAILREC_KEYWORD, + DATA_KEYWORD, // local data class (Kotlin 1.9+) INNER_KEYWORD, - ) + ), ) } DeclarationContext.OBJECT_BODY, DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> candidates -= INNER_KEYWORD // inner only valid inside a class + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + candidates -= INNER_KEYWORD + } + + // inner only valid inside a class - else -> Unit + else -> { + Unit + } } when (declKind) { DeclarationKind.PROPERTY_VAL -> { - candidates -= setOf( - LATEINIT_KEYWORD, // lateinit requires var - VARARG_KEYWORD, - NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, // lateinit requires var + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) // const only on top-level or companion object val - if (declCtx !in setOf( + if (declCtx !in + setOf( DeclarationContext.TOP_LEVEL, DeclarationContext.OBJECT_BODY, - DeclarationContext.SCRIPT_TOP_LEVEL + DeclarationContext.SCRIPT_TOP_LEVEL, ) - ) + ) { candidates -= CONST_KEYWORD + } } DeclarationKind.PROPERTY_VAR -> { - candidates -= setOf( - CONST_KEYWORD, // const requires val - VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + CONST_KEYWORD, // const requires val + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) } DeclarationKind.FUN -> { - candidates -= setOf( - LATEINIT_KEYWORD, CONST_KEYWORD, VARARG_KEYWORD, - INNER_KEYWORD, COMPANION_KEYWORD, DATA_KEYWORD, - ENUM_KEYWORD, ANNOTATION_KEYWORD, SEALED_KEYWORD, - VALUE_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, + CONST_KEYWORD, + VARARG_KEYWORD, + INNER_KEYWORD, + COMPANION_KEYWORD, + DATA_KEYWORD, + ENUM_KEYWORD, + ANNOTATION_KEYWORD, + SEALED_KEYWORD, + VALUE_KEYWORD, + ) // abstract fun can't be inline/tailrec/external simultaneously if (ABSTRACT_KEYWORD in existing) { candidates -= setOf(INLINE_KEYWORD, TAILREC_KEYWORD, EXTERNAL_KEYWORD) @@ -106,16 +172,23 @@ internal object ModifierFilter { } DeclarationKind.CLASS -> { - candidates -= setOf( - LATEINIT_KEYWORD, CONST_KEYWORD, - VARARG_KEYWORD, NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, - TAILREC_KEYWORD, OPERATOR_KEYWORD, INFIX_KEYWORD, - REIFIED_KEYWORD, - ) + candidates -= + setOf( + LATEINIT_KEYWORD, + CONST_KEYWORD, + VARARG_KEYWORD, + NOINLINE_KEYWORD, + CROSSINLINE_KEYWORD, + TAILREC_KEYWORD, + OPERATOR_KEYWORD, + INFIX_KEYWORD, + REIFIED_KEYWORD, + ) // sealed is a modality modifier and conflicts with open/final/abstract - if (SEALED_KEYWORD in existing) + if (SEALED_KEYWORD in existing) { candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) + } // value class requires @JvmInline in practice, but `value` keyword is valid } @@ -124,22 +197,32 @@ internal object ModifierFilter { // interfaces are implicitly abstract; most modifiers don't apply candidates.retainAll( setOf( - PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - EXPECT_KEYWORD, ACTUAL_KEYWORD, - SEALED_KEYWORD, // sealed interface - EXTERNAL_KEYWORD, FUN_KEYWORD, // fun interface - ) + PUBLIC_KEYWORD, + PROTECTED_KEYWORD, + PRIVATE_KEYWORD, + INTERNAL_KEYWORD, + EXPECT_KEYWORD, + ACTUAL_KEYWORD, + SEALED_KEYWORD, // sealed interface + EXTERNAL_KEYWORD, + FUN_KEYWORD, // fun interface + ), ) } DeclarationKind.OBJECT -> { candidates.retainAll( setOf( - PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD, - EXPECT_KEYWORD, ACTUAL_KEYWORD, EXTERNAL_KEYWORD, - DATA_KEYWORD, // data object (Kotlin 1.9+) + PUBLIC_KEYWORD, + PROTECTED_KEYWORD, + PRIVATE_KEYWORD, + INTERNAL_KEYWORD, + EXPECT_KEYWORD, + ACTUAL_KEYWORD, + EXTERNAL_KEYWORD, + DATA_KEYWORD, // data object (Kotlin 1.9+) COMPANION_KEYWORD, - ) + ), ) } @@ -154,7 +237,9 @@ internal object ModifierFilter { // the exclusion rules above already handled mutual exclusions. } - else -> Unit + else -> { + Unit + } } // expect and actual are mutually exclusive @@ -170,9 +255,7 @@ internal object ModifierFilter { return candidates } - private fun TokenSet.types(): Set = - types.filterIsInstance().toSet() + private fun TokenSet.types(): Set = types.filterIsInstance().toSet() - private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = - this.contains(token) + private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) } From 66911cf0ec28419458cd2a6336d647ab04ab0a9c Mon Sep 17 00:00:00 2001 From: David Schachter Date: Wed, 5 Aug 2026 21:07:52 -0700 Subject: [PATCH 05/11] ADFA-5010: Fix ClassCastException from stale Kotlin code actions across project switches Closing a project and opening another tears down the KotlinLanguageServer and its DexClassLoader-loaded compiler session, then creates a fresh one. LSPEditorActions.ensureActionsMenuRegistered de-duplicated by action ID and skipped re-registration, so the first session's action objects (bound to its now-dead classloader) stayed wired into the shared, main-dex ActionsRegistry. Invoking one against the second session's data threw ClassCastException on two same-named-but-differently-loaded classes (e.g. AddImportAction casting env to AbstractCompilationEnvironment). Fix: replace stale actions by ID instead of skipping when re-registering, and have KotlinLanguageServer.shutdown() proactively unregister its actions so a dead session never leaves classloader-bound objects in the shared registry. Also hardened AddImportAction's cast to degrade gracefully instead of crashing if a cross-classloader mismatch occurs some other way. Reproduced and verified fixed on-device: switching between two Kotlin projects and running "Import class(es)" on the second project's session no longer crashes. --- .../androidide/lsp/util/LSPEditorActions.java | 128 +++++++++++------- .../lsp/kotlin/api/IKotlinCompilerSession.kt | 14 +- .../lsp/kotlin/actions/AddImportAction.kt | 11 +- .../compiler/KotlinCompilerSessionImpl.kt | 4 + .../lsp/kotlin/KotlinLanguageServer.kt | 6 + 5 files changed, 111 insertions(+), 52 deletions(-) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java index e379b54edc..9e3fe3b794 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java @@ -1,50 +1,78 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.util; - -import com.itsaky.androidide.actions.ActionItem; -import com.itsaky.androidide.actions.ActionMenu; -import com.itsaky.androidide.actions.ActionsRegistry; -import com.itsaky.androidide.actions.locations.CodeActionsMenu; -import com.itsaky.androidide.lsp.actions.IActionsMenuProvider; -import com.itsaky.androidide.utils.ILogger; - -/** - * @author Akash Yadav - */ -public class LSPEditorActions { - - public static void ensureActionsMenuRegistered(IActionsMenuProvider provider) { - final var registry = ActionsRegistry.getInstance(); - final var action = - registry.findAction(ActionItem.Location.EDITOR_TEXT_ACTIONS, CodeActionsMenu.ID); - - if (action == null) { - ILogger.ROOT.error("[LSPEditorActions] Cannot find registered editor actions menu"); - return; - } - - final var editorActions = (ActionMenu) action; - for (final var item : provider.getActions()) { - if (editorActions.findAction(item.getId()) != null) { - continue; - } - editorActions.addAction(item); - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.util; + +import com.itsaky.androidide.actions.ActionItem; +import com.itsaky.androidide.actions.ActionMenu; +import com.itsaky.androidide.actions.ActionsRegistry; +import com.itsaky.androidide.actions.locations.CodeActionsMenu; +import com.itsaky.androidide.lsp.actions.IActionsMenuProvider; +import com.itsaky.androidide.utils.ILogger; + +/** + * @author Akash Yadav + */ +public class LSPEditorActions { + + public static void ensureActionsMenuRegistered(IActionsMenuProvider provider) { + final var registry = ActionsRegistry.getInstance(); + final var action = registry.findAction(ActionItem.Location.EDITOR_TEXT_ACTIONS, CodeActionsMenu.ID); + + if (action == null) { + ILogger.ROOT.error("[LSPEditorActions] Cannot find registered editor actions menu"); + return; + } + + final var editorActions = (ActionMenu) action; + for (final var item : provider.getActions()) { + // Replace rather than skip: a stale entry with the same ID may belong to a previous + // language server session (e.g. a prior project's DexClassLoader-loaded Kotlin compiler + // module), whose action objects are bound to a now-dead classloader. Keeping it around + // would let it later execute against data produced by the new session, causing a + // ClassCastException between two same-named-but-differently-loaded classes. + final var existing = editorActions.findAction(item.getId()); + if (existing != null) { + editorActions.removeAction(existing); + } + editorActions.addAction(item); + } + } + + /** + * Removes every action in {@code provider}'s menu from the shared editor actions menu, + * matched by ID. Call this when a language server session (and the classloader its action + * objects are bound to, e.g. a {@code DexClassLoader}-loaded module) is being shut down, so + * a dead session's actions cannot outlive it in the shared, app-wide {@link ActionsRegistry}. + */ + public static void ensureActionsMenuUnregistered(IActionsMenuProvider provider) { + final var registry = ActionsRegistry.getInstance(); + final var action = registry.findAction(ActionItem.Location.EDITOR_TEXT_ACTIONS, CodeActionsMenu.ID); + + if (action == null) { + return; + } + + final var editorActions = (ActionMenu) action; + for (final var item : provider.getActions()) { + final var existing = editorActions.findAction(item.getId()); + if (existing != null) { + editorActions.removeAction(existing); + } + } + } +} diff --git a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt index 2b0f172613..984e578325 100644 --- a/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt @@ -25,6 +25,18 @@ interface IKotlinCompilerSession : AutoCloseable { /** Registers the Kotlin code-actions menu. Deferred here (off the eager setup path) since it needs classes from the isolated dex. */ fun registerCodeActions() - /** Schedules the already-open documents in [activeDocumentFiles] for analysis, mirroring what setupWithProject used to do eagerly for every open file. */ + /** + * Removes this session's code actions from the shared editor actions menu. Call this on + * shutdown -- otherwise a dead session's action objects (bound to a now-closed + * `DexClassLoader`) stay wired into the app-wide menu and can later execute against a + * different session's data, throwing `ClassCastException` on same-named-but-differently + * -loaded classes. + */ + fun unregisterCodeActions() + + /** + * Schedules the already-open documents in [activeDocumentFiles] for analysis, mirroring + * what setupWithProject used to do eagerly for every open file. + */ fun openFileIfNeeded(activeDocumentFiles: List) } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt index a99b7c36a4..86b8489a2d 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt @@ -59,8 +59,17 @@ class AddImportAction : BaseKotlinCodeAction() { val (env, action) = extra val nioPath = data.requireFile().toPath() + // Safe cast: env is normally an AbstractCompilationEnvironment (same isolated dex, same + // classloader). A failed cast here means the diagnostic outlived the session that + // produced it (e.g. a stale action from a torn-down session) -- degrade to no edits + // rather than crash. + val abstractEnv = + env as? AbstractCompilationEnvironment ?: run { + logger.warn("compilationEnv is not an AbstractCompilationEnvironment: {}", env) + return emptyMap() + } return withContext(Dispatchers.IO) { - computeImportCandidates(env as AbstractCompilationEnvironment, nioPath, action.referenceName) + computeImportCandidates(abstractEnv, nioPath, action.referenceName) } } diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt index a775b3ade1..ad3d556db3 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt @@ -34,6 +34,10 @@ internal class KotlinCompilerSessionImpl( LSPEditorActions.ensureActionsMenuRegistered(KotlinCodeActionsMenu) } + override fun unregisterCodeActions() { + LSPEditorActions.ensureActionsMenuUnregistered(KotlinCodeActionsMenu) + } + override fun openFileIfNeeded(activeDocumentFiles: List) { activeDocumentFiles.forEach { file -> compiler.compilationEnvironmentFor(file)?.openFileIfNeeded(file) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt index 7370cb3763..a326575603 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinLanguageServer.kt @@ -103,6 +103,12 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) scope.cancel("LSP is being shut down") + // Unregister before closing: once closed, loader.currentSession() is null and the + // session's action objects (bound to this session's DexClassLoader) would otherwise + // stay wired into the shared, app-wide editor actions menu -- executable, but with a + // classloader that no longer matches anything a future session hands out. + loader.currentSession()?.unregisterCodeActions() + codeActionsRegistered = false loader.close() initialized = false } From b571e81b0957636a6fc4909daef981aa13c18a95 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Wed, 5 Aug 2026 22:27:28 -0700 Subject: [PATCH 06/11] ADFA-5010: Resolve phantom ktlint max-line-length blocking spotlessApply ResolutionScopeProvider.kt:20 (`override fun getResolutionScope(...)`, 71 chars) has been failing ktlint(standard:max-line-length) with a reported limit of 140 since it moved into this module, despite no line in the file exceeding that length. Confirmed live/dynamic rather than a stale cache by padding the file with leading comment lines and observing the reported line number shift by exactly the same amount -- ktlint 1.5.0 genuinely misattributes a max-line-length violation to this declaration, for reasons that don't trace to anything in this file's content (checked byte length, tabs, non-ASCII, and a `.editorconfig` blank_line/header shift). Suppress it with the same `@file:Suppress("ktlint:standard:max-line-length")` convention already used elsewhere in this module (KotlinCompletions.kt), matching this repo's actual mechanism for it (`ij_formatter_tags_enabled` governs IntelliJ-style suppression, not this per-file annotation, but the sibling file shows this is the established pattern here). This was blocking `./gradlew spotlessApply` for the entire repo, not just this module -- fixed now, and a real (previously-masked) trailing-comma violation in the same file came along for the ride. --- .../com/itsaky/androidide/lsp/util/LSPEditorActions.java | 5 +---- .../lsp/kotlin/compiler/services/ResolutionScopeProvider.kt | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java b/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java index 9e3fe3b794..dfff6b0332 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/util/LSPEditorActions.java @@ -54,10 +54,7 @@ public static void ensureActionsMenuRegistered(IActionsMenuProvider provider) { } /** - * Removes every action in {@code provider}'s menu from the shared editor actions menu, - * matched by ID. Call this when a language server session (and the classloader its action - * objects are bound to, e.g. a {@code DexClassLoader}-loaded module) is being shut down, so - * a dead session's actions cannot outlive it in the shared, app-wide {@link ActionsRegistry}. + * Removes every action in {@code provider}'s menu from the shared editor actions menu, matched by ID. Call this when a language server session (and the classloader its action objects are bound to, e.g. a {@code DexClassLoader}-loaded module) is being shut down, so a dead session's actions cannot outlive it in the shared, app-wide {@link ActionsRegistry}. */ public static void ensureActionsMenuUnregistered(IActionsMenuProvider provider) { final var registry = ActionsRegistry.getInstance(); diff --git a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt index 489ac5137b..d33e703889 100644 --- a/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/services/ResolutionScopeProvider.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:max-line-length") + package com.itsaky.androidide.lsp.kotlin.compiler.services import org.jetbrains.kotlin.analysis.api.impl.base.projectStructure.KaBaseResolutionScopeProvider @@ -9,7 +11,6 @@ import org.jetbrains.kotlin.com.intellij.psi.search.GlobalSearchScope import org.slf4j.LoggerFactory class ResolutionScopeProvider : KaBaseResolutionScopeProvider() { - companion object { private val logger = LoggerFactory.getLogger(ResolutionScopeProvider::class.java) @@ -24,7 +25,7 @@ class ResolutionScopeProvider : KaBaseResolutionScopeProvider() { override fun buildSearchScope( module: KaModule, - analyzableModules: Set + analyzableModules: Set, ): GlobalSearchScope { logger.debug("buildSearchScope(module={}, analyzableModules={})", module, analyzableModules) return super.buildSearchScope(module, analyzableModules) From b68f7990384fa149961fde7170f8aaa5501a4250 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Wed, 5 Aug 2026 23:01:15 -0700 Subject: [PATCH 07/11] ADFA-5010: Move kotlin-compiler-carrier.apk into assets/data/common/ Matches the existing convention for other build-artifact assets shipped straight in the base APK (plugin-api jar, logsender AAR, both placed under data/common/ via AddFileToAssetsTask) instead of sitting bare at the assets/ root. KotlinCompilerLoader now distinguishes the in-APK asset path (data/common/kotlin-compiler-carrier.apk) from the local extracted file name (kept flat, unprefixed, in the app's private per-app directory -- no reason to nest that too). --- .gitignore | 2 +- app/build.gradle.kts | 2 +- ...1-lazy-load-kotlin-analysis-api-via-dexclassloader.md | 2 +- .../androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt | 9 +++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index f81dcfa003..80d7a4268c 100755 --- a/.gitignore +++ b/.gitignore @@ -176,7 +176,7 @@ assets/gradle-*.zip app/src/release/assets/database/documentation.db app/src/main/assets/database/documentation app/src/main/assets/database/documentation.db -app/src/main/assets/kotlin-compiler-carrier.apk +app/src/main/assets/data/common/kotlin-compiler-carrier.apk # AI plugin development artifacts (moved to plugin-examples repo) *.cgp diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f1223bdba5..58d15e377e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -435,7 +435,7 @@ tasks.register("copyKotlinCompilerCarrierToAssets") { rootProject.layout.projectDirectory .dir("subprojects/kotlin-compiler-carrier/build/outputs/apk/v8/release") .file("kotlin-compiler-carrier-v8-release-unsigned.apk") - val destFile = layout.projectDirectory.file("src/main/assets/kotlin-compiler-carrier.apk") + val destFile = layout.projectDirectory.file("src/main/assets/data/common/kotlin-compiler-carrier.apk") inputs.file(sourceFile) outputs.file(destFile) doLast { diff --git a/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md b/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md index 362eb29c84..00a82174cd 100644 --- a/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md +++ b/docs/adr/0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md @@ -25,7 +25,7 @@ Any class that directly imports Analysis API types must live in the *same* dex/c - **`lsp/kotlin-api`** — the bridge: `IKotlinCompilerSession`, `IKotlinCompilationEnvironment`, `IKotlinCompilerSessionFactory`, plus small shared value types (`DiagnosticAction`, `KotlinDiagnosticExtra`). Resident. Depended on by both sides. - **`lsp/kotlin-compiler-impl`** — the isolated payload: `compiler/`, `completion/`, `diagnostic/`, `navigation/`, `signaturehelp/`, `utils/`, and the four diagnostic-driven code actions (Add Import, Organize Imports, Null Safety, Implement Members) moved out of `lsp/kotlin`. Implements the bridge interfaces against the real `Compiler`/`CompilationEnvironment`. Depends `implementation` on `subprojects/kotlin-analysis-api` (the only dependency actually bundled here); everything resident it needs (`lsp/kotlin`, `lsp/api`, `lsp/models`, `lsp/jvm-symbol-index`, etc.) is `compileOnly` — those types are already loaded by the parent (main app) classloader by the time the carrier dex runs, so `DexClassLoader`'s normal parent-first delegation resolves them there. -- **`subprojects/kotlin-compiler-carrier`** — a `com.android.application` module (`isMinifyEnabled = false`, matching the existing "ship intact, don't shrink" call for this dependency graph) whose only purpose is to produce a real, D8-dexed APK from `lsp/kotlin-compiler-impl`. Never installed or launched; `app`'s build copies its output straight into `app/src/main/assets/kotlin-compiler-carrier.apk` (`copyKotlinCompilerCarrierToAssets`, wired into `preBuild`) rather than through the brotli-compressed installer-zip pipeline used for the JDK/Android SDK — this is a few MB, not hundreds, and belongs in the base APK unconditionally. +- **`subprojects/kotlin-compiler-carrier`** — a `com.android.application` module (`isMinifyEnabled = false`, matching the existing "ship intact, don't shrink" call for this dependency graph) whose only purpose is to produce a real, D8-dexed APK from `lsp/kotlin-compiler-impl`. Never installed or launched; `app`'s build copies its output straight into `app/src/main/assets/data/common/kotlin-compiler-carrier.apk` (`copyKotlinCompilerCarrierToAssets`, wired into `preBuild`) rather than through the brotli-compressed installer-zip pipeline used for the JDK/Android SDK — this is a few MB, not hundreds, and belongs in the base APK unconditionally. - **`lsp/kotlin`** (resident) — `KotlinLanguageServer` becomes a thin wrapper. `KotlinCompilerLoader` extracts the carrier APK from assets to a stable private file on first use and loads it via `DexClassLoader(apkPath, optimizedDir, null, parent)`, the same construction `PluginLoader` already uses for optional plugin APKs. The trigger point moves from `setupWithProject()` (every project open, Kotlin or not) to the first call actually gated on a `.kt` file — `onDocumentOpen`, `complete()`, `analyze()`, `findDefinition()`, `signatureHelp()` — which also fixes the pre-existing bug where every project open paid the Analysis API bootstrap cost even for pure-Java projects. Two risks specific to this boundary, both mitigated in `KotlinCompilerLoader`: diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt index bef762bc93..30734652f8 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt @@ -26,8 +26,8 @@ class KotlinCompilerLoader( private fun extractCarrierApk(): File { val dir = context.getDir(CARRIER_DIR_NAME, Context.MODE_PRIVATE) - val dest = File(dir, CARRIER_APK_ASSET_NAME) - val markerFile = File(dir, "$CARRIER_APK_ASSET_NAME.marker") + val dest = File(dir, CARRIER_APK_FILE_NAME) + val markerFile = File(dir, "$CARRIER_APK_FILE_NAME.marker") // The main APK's own mtime as a cheap "has the app been updated/reinstalled since we // last extracted" marker -- avoids re-extracting on every process start. @@ -36,7 +36,7 @@ class KotlinCompilerLoader( if (!dest.exists() || previousMarker != currentMarker) { logger.info("Extracting Kotlin compiler carrier APK to {}", dest) - context.assets.open(CARRIER_APK_ASSET_NAME).use { input -> + context.assets.open(CARRIER_APK_ASSET_PATH).use { input -> dest.outputStream().use { output -> input.copyTo(output) } } markerFile.writeText(currentMarker) @@ -112,7 +112,8 @@ class KotlinCompilerLoader( companion object { private const val CARRIER_DIR_NAME = "kotlin_compiler" - private const val CARRIER_APK_ASSET_NAME = "kotlin-compiler-carrier.apk" + private const val CARRIER_APK_FILE_NAME = "kotlin-compiler-carrier.apk" + private const val CARRIER_APK_ASSET_PATH = "data/common/$CARRIER_APK_FILE_NAME" private const val FACTORY_CLASS_NAME = "com.itsaky.androidide.lsp.kotlin.compiler.KotlinCompilerSessionFactoryImpl" private val logger = LoggerFactory.getLogger(KotlinCompilerLoader::class.java) From 24f1ee0dd0c14309852252c52059a3446a9458a8 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Wed, 5 Aug 2026 23:18:25 -0700 Subject: [PATCH 08/11] Optimize res/ PNGs with pngquant Quantized all 53 tracked PNGs under res/ directories with pngquant (--skip-if-larger, quality 65-100); 47 shrank, 6 were already at or near optimal and left untouched. Total size across the set drops from 172KB to 103KB (~40%). Dimensions, alpha channels, and visual appearance are unchanged -- verified by inspecting file(1) output and a visual spot check of the app icon, logo, and template screenshots, and by a full :app:assembleV8Debug rebuild. --- .../drawable-xxhdpi-v4/leak_canary_dump.png | Bin 301 -> 295 bytes .../leak_canary_icon_foreground.png | Bin 2506 -> 1923 bytes .../leak_canary_icon_monochrome.png | Bin 2539 -> 1892 bytes .../drawable-xxhdpi-v4/leak_canary_info.png | Bin 545 -> 513 bytes .../drawable-xxhdpi-v4/leak_canary_leak.png | Bin 448 -> 350 bytes .../leak_canary_tv_icon.png | Bin 765 -> 740 bytes .../drawable-xxxhdpi-v4/leak_canary_dump.png | Bin 301 -> 295 bytes .../leak_canary_icon_foreground.png | Bin 2506 -> 1923 bytes .../leak_canary_icon_monochrome.png | Bin 2539 -> 1892 bytes .../drawable-xxxhdpi-v4/leak_canary_info.png | Bin 545 -> 513 bytes .../drawable-xxxhdpi-v4/leak_canary_leak.png | Bin 448 -> 350 bytes .../leak_canary_tv_icon.png | Bin 765 -> 740 bytes .../res/mipmap-xxhdpi-v4/leak_canary_icon.png | Bin 3746 -> 2725 bytes .../mipmap-xxxhdpi-v4/leak_canary_icon.png | Bin 3746 -> 2725 bytes .../res/drawable-xhdpi/ic_action_more.png | Bin 282 -> 205 bytes .../ic_launcher_notification.png | Bin 1364 -> 1322 bytes .../res/drawable-xxhdpi/ic_action_more.png | Bin 417 -> 341 bytes .../ic_launcher_notification.png | Bin 1948 -> 1613 bytes .../ic_launcher_notification.png | Bin 3047 -> 2169 bytes .../template_basic_activity.png | Bin 2428 -> 2013 bytes .../drawable-xxxhdpi/template_no_activity.png | Bin 2004 -> 1079 bytes .../res/drawable/app_dev_for_all_logo.png | Bin 2933 -> 2776 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2077 -> 1473 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 2218 -> 1536 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 2467 -> 1685 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1291 -> 1235 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 1399 -> 1321 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 1570 -> 1406 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3073 -> 1982 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 3250 -> 2027 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 3616 -> 2217 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5231 -> 2790 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 5522 -> 2817 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 6117 -> 3106 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 7438 -> 3664 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 7697 -> 3680 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 8585 -> 4020 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3593 -> 2340 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 5339 -> 3035 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2636 -> 1964 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 3388 -> 2293 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4926 -> 2867 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 7472 -> 4001 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7909 -> 3949 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 11873 -> 5671 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10652 -> 5046 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 16570 -> 7386 bytes 47 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png index 4f5b301faff6fd5ca0bf24e71223bf139465295f..1d81da0904126b7b491dc0e30b9122c0224b5164 100644 GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8v!3-o%-;NLhQXT<5A+GKPK0qerz_U&5GSC$9 zk|4iehWH=VoNSV{tSps(E*w516&w8S@5}F>eA3_W0hN?{x;TbdoPImWldnNRfF&?c zFs1PA|NkNU8nbv#opec_>#^)o;sVKP!)vM5=PvnOee4iFF{oWwcCLv+Q&#rI$j2=j zyFOm(;a)PmXu*Rkt`&?k6sCT8q&ngEV=1ZXLk7Q=E?>H2zp(T1#uRwnFboFyt=akR{0AjvbkpKVy literal 301 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8v!3HG%u&z)5QeQk>978JN-cB>*J7gf@{GY)? zhH>4|9t(D-Iek+XUQb9&Xk=u$@9a9OYtopxhQ(9hy&>$YY)bH$wz zNqlIvdmeMEsq8Qg++%wv< zXKkU#dfQ!|6;t;IY~x?Jcj3{a&P%V{mSC*X`=wW<|0{pblW5*oSEhQop5^YyRf<}z S{P!`?{|uh4elF{r5}E)St#)Yu diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_foreground.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_foreground.png index f67149c79ceeebee0cc26e04178db46e786b087c..a3449ad5f9a77ffcfdc31ec81384e05fd6603f07 100644 GIT binary patch literal 1923 zcmZWoc~leE7mkW3K`cu`RKNg5f^1195WuRHuo*y>5D=|a0TD>B;Bt_y;BxRsD2Nmr z#f4q0kZtU+;{JH@AtiT-Y`Of{0!jR z;4m1>fV|(E0)8u@cjG$HOB}^JVX(E4A%RpMzvMu*2vDnGY7s%>0ksGZ#w&aqbuSoe zjQ|C`SSZttgJOY)24$)Zlvo3C9GQ^V|`{ z%judf2tZK7U{K2`FoRblXu+E5utt^84Vn*0`hSadGG4+#fca%E_LKUQIZ9_=8#*#LKM1$9e0i+zH6dckE$P*y@%q}7<$Ggbl3357C9U|qRL}r*xPulWPauG%JsIr;q&XTSm2!~_op?p3#V3K? z3g24FL#c#2OR{&~$g)@bftIyzS?$5Db~~${Iw-qr6=#9fPC#)U$f9Amx1AQB;2oW; z(&8}gL@R!UvxT*#jJIvI2~afv@%=+&zPuB$#b zORt_isLOsf+ApmPKShGUboP?HJ*la0W=mvYCZ323ZUu(d_8GSrdhcUxw7yin*0lC` zQY1TSo+BI0Xf`&~aU9YgQu3zO7c>X72Apa2h#Qr8ot#_zZn1ZGq%jG*aSgcj|32=K z6`4;e{N0YED4CrG|NAQnQOY-S*n09_rNIVl+!nF8?3_Jy!9Gst*r;#nOko~cCUK&C z)7Q70bUVQ;@A$#A>~M`a2mbwY91Hd0$`Adsc{!*nfanl^<6#F(XeKxLM@--8(pNu4 zH`NrK>5#sw+EXm(%iUda{#Jfe?B=OH<$cc5GZcFXHGpAknXxO}trX#IWbP)?M-y+= z^cUj-Co>+Od*p6W=G>6}J?*cn>_>M!bT&!sxzQM9XWBx=Ko4P@fEmng&VP&hl7ef& zzHV%o3-Yu5H8pec7_Gc zkMYu`n((h4cbVVy*MB;ZeJUa*M1-9D?Q(3!P#N)`f;*eC3SR$yAtja-)@@#fBNckG z%l5JF)hkiMOFx|#j8S5aoOXE+s8S2mzVH%+Y#ZJ)4lUi!!eCxshjc&D)VG}Rz{NEFB(3C3(DOO ziw49`=RI5h8C!5C-rz>(6^^J{+H`|4L-xB8P;%F|bQ#k8vKKxeaN%NmznGAMW;$CR zNVF%am*~i&haRK`^;z@Y%zUh8z$rD!j(I}sZ;|tR(!9x5@G#!Z*{>MA6jhUHw1q9- zy^O#PXQ2g_M~=$A^SA9z-eH8C6jtA4%zqp_xZNwjWG{c+qTO6)s*Ve55+{m?pnOD(a_AGX>Oqls-ZWq|$|M>)crZ|Li#AuU8(zJ+ zwKcCgI`ILO|3cUj;gj=Auy4ed*8YJvsR9~-H7=DDJU^33XF9yuUMgW&px;+=4`hOh6u@Z2R^3ky5r(mP*aPWB1%t|J}4^e;mj B+4%qf literal 2506 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 O3K-PRvfBJs!v6t<>&Wx~ diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_monochrome.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_monochrome.png index e10476ea13a41611aad365e2325a3ded3ec72a88..732ca750026d59ea1278f3cc729a3df9331f287d 100644 GIT binary patch literal 1892 zcmZWoX;>5I7ETd2Z~+q(i3m!N8=!M9>2Z#9#(fGXuGQAyPXNri@rPIEZfic_?JKv@NI zDht^-Wq?@~8~{|3*L%S05?5C4#|bE2PS@v z6mwx2GJ%n#VK)dQ4LS-Tewhv}aZ$?=kh}p}xdKV(_?09`AyAg^v;ikz0~kpJemR1$ ze3k%^V&2Ya8W!{dF{}rkq0)c`pht)YC1S4DA_oN&F1$j442YF5czTQVKsLZ=0X@Jg z1LPuz!H&RD0?rt7*ni!c0Fspuqzq7{C7}_3R9QKGC6543e37dqje~$TIR{eYSuN8E zplanD0rV1cP%Hcc;s`KjO03wwmJ2^nwnOvUB9R|^Jc=xKxB%1M_-vguBD6lUF~jB z`W;)}&YlXx#BcOhqH_Jey-fc#Xu>|{bfZg^jJ)QVtutC6o)S&_B@d0R&V)>@p(Ycm z*tRcD2tI8&<~nQrEK`_r5@MWmw-4>ch5ejR{NN-1<&CVH&;PA4^f5*LW#abUmIcQ7 zU|Af@QLOS-ZJ6KkBC}xfrJIAq8vjfAnaD)!Ai6)L^dcoEhm=9-H)|)DYMgB+8czxW4i`?{&tEO#-EWHayM?uoWj}p)ha5db8LAP*X^yeC z^2#Z8FX)qk`QYk*#ffrXj?K(6Xeir+Ms?%Iqvoxnoqw#^_TJU&JHwl_F}S-a&4{0& z8b$NnRwXV;N7f!rW}*!yYW@@V+H1>|gjjB+!&y$mBw01eq;M#xUixQUzlQf~Vnf-X zXxCemj{Cbxh0NNvb3DFt%;emMsnyfWQ1uPX#t*}^i#+L?d;A01<2p`?_ott#$aqs~BnNf3l9) zekz9l)bf2)*JD$&oz~4i<5P<-E?ZY)Of#IUo3r-wDAjAvEJHoo3h>wTmii`#a<-@1 zd6NpSjbd^F>m$QX&i~`NRN*~vN%)X7m=kEdl!OWmyBr?N9_dSfCwoJ2-gtBSp$04Wq`=Xd!z`NANp3?Q1FzlGaxZ&wMuZ z^Uc>#J+FJ*_7>@L7V9v<{*|jq;68fx^$XQ__qu@=lH342ZsEk-U5U{>hcc0S>5U!S z;^7|_P1mQV&p1_NAfMhN+{8us7d!JBpi_A^G}+OUMtk@^q!ORz=`u50l^=gPG`_E9 z*fZ4+Ra~_Ws~f`WX&k<1wLTARNiNLm615P*V?F6-w*9)`@uIfM{E+Lm#?4UnE^mij z&wV`C{LXFZh`E>7@3)J(iEmyM`6q^K>ysh47C!omsDQ^?4%x*<63+r9FLdX8F n(zn4zGZ;PwfBSPm;*uiw2#fQh`x$wK4ZKcqq>>&S4f_7SC?Cu) literal 2539 zcmbVO_fr!H6OC{xLB7i>EhUEk~XIQV&yV}yV zv6L1R(El)dZ-$|xm&FRMt3(!jS)v{!6J?vf?98$b1J{R%8BxF0{5L2>M@2R6Apvz3 zeygw*0lL;E>4dXfm`ePLV5h?DOz1VNV7DmA$A|crDA>PTcWd76^^OGR`i-wK+kIS; zyx0s7G(@}gW_tph+uHsGNnUgUtA01)YF53ZZh>H7)mYyCQLk|(Jos9H>Sz&G+k~8S z`21BYF#H?cQ)u;{nEPwJw&OKg>0{CCYLs>2zg`y=v(5@Uj>B`FkNO;%ZF!NE-1ZL& zm#21@jwcCYUDBVt)iuY42i#mDt}fKI#3lIEX&YPwYuX4A?E?>U4jTa?#RS?5w^cZz zDBjvgX`r*_F{xnmL8Z^KX&isDzwc9=E%bUvk5O9jN9VopH? zqVv;%cJidOVx5;9uTDtEj`RLO>StEVfZwR#NV*0DB5fr>rnrjyptWlehYau{d9Vlb zk^f@{-w+sy-jQ;-0<5U_Gv?^yDm=b=jbXKH*Nc`?2Ag`-w}aUjJLR1etjbfyk?ttX z;A7m=@@R)c@r_-sjF=O(X?#eiQYr$uy6eOcb_pJC+NEoqO7o~#Dh&+z+Osf~N|~7B z7e}a{B(-q~x!f-UZs=m+_!G*}24oR)UUfl)5TTm!7=!Z<%D1i{#O>&F_7OHhD=|q7 zn{TJCcN`yJ5$nGK^*N=^DSpqsLi^AhRu=0nm!ed4SyVP=@ob%(g-lpm zWb>Pnr=Wu)%kAh3&%jilOKY3@sXXjm7PJaHki@V(WL+MO##Vk$FJt4XShXSf@H3B! zPaV~cb<=>CcbYpAP2yO~jEK*yYaid|e94gCml7LEAjO0Ck7C3V!zFaZ1al4OFHbl1 zg5d#U$9DBqEJu;X5EyfHWIX294+(}#KHi(E2jlCy{I?J`78 zLF+*ypExc3@HA}Cp|1DJb-W)=rLM;%K9(C=XeDaynxb+FG>V|IRh@WnQ{2FIvpYID zPu1=%sUf-g!ItE19OIZR?qlc+plcXB7@w{K9(dZ~#J_JXIDw(#xWO=5eJu=^5}FZp zx7eOFHPPp<(8_6Xmi-=>D()H<3o`808l755UdXZV-uze4_YNllUH0J~FTE6JCF;6o z=rfcj#?F&17&b)b5~Q_DeUrl0bypL;O6uCDst3MW>p0c;r76sR#0J7KQ z&+O5D?a=D{u4z;c=F1<=Lkei8o1z0OkhkuGP=zXk9x`v0<%K6fAi4yx1l89O&y4#1 zaGw5$Y9SP3M^8d$1<=q#9UnO_d5HzZ!Zw{s6V1`L|>e>04#q zHP_;!RKn?rB(?3X>OAlgOL$Ju8rc-&2=w&*Do>kog&#-%6bovHv3L&OL#qPi2u^<$+S+-jHA@4a(xqW z+vJNaDgGy>REW&4zi$v==~D6<1PVOwx7etex8Awc9P+U(BDV7Q{W9}{=zRF5gPhmh z8n2s@QoZ-d_86TH6g)*$WRpi^jyX3Z$V?Pvyj#?%!e(Ds@4zjQlCu>N&v zM2CG&$I7E0tGD(VZsrY$BToiYZCY^*joGIq)w`_7-D`?Nw15N*`j>QJ>4uf>tw}O& zv$u?0rf0vc%~`B8aA3g;9T&T~@&6EI7q?EFvrSe&%yU_~T&Ajg+4sWc^{yJ{4vu_tL1}RV&r1<^W|Yr25-ih58l{^`7Zh!CFFYgX)Z_o;yaO$SOMD3u0E?NY~7B z0hz0X_!N^Jfetea4ggRn$vNGqq*gVlGzk37VQy??R0q8u_J7@W=_LRF diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_info.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_info.png index 718690d9f47c80063c6d92e67a950da2807e906c..38f6d759c8eec8ba8b0ec726907f54dcf6d34d17 100644 GIT binary patch literal 513 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`ZS^|7RT-^(NL}cWEJP{Ff5g7#_ zLsUi{h{R;%A>t6OsI)u;0mXp`BB-LGA_@dD3P7%c0+5sulaYg}mjf|Gq=85Yq+V7Q zDh|;LHcbR%1<0t^@8|9TTC80Xr|i2i^+i{==D8(z85kHDJzX3_EKVPta$Ky*fyb3Splii~1pS1id;b5o z-z<^PRN;3gBWz~D`8`uOz5bqmlU*92KmE0H=~jD%m;c(X<^3+{=&UvP{MbCj=KBlh zh?Lz|v!;tG%I^8ODPRov{!NJW+b4<8hFNkuvwCS>L!eEnl+Z#kuHH zH?N*KvB_=LqTPRTxnmoA7pF3YE#ASX_x7l&z>0DYR)?D^g^O<8-eqOEYNA!)r8?d_ z?d=8ZS#RwcJ{y#<$=#}D+9@?D*z&`bcY-f|WvH!_dGLex$P<|#?F}z3_}J_S3j3^ HP6P1fc|w8Gi-<007|tn3wA10uYM3+OQjMi<#9UbI=x4>)Mq_rfn3PRi&O<- z-dNI4UxjyM^SE;X9()nAmVpE1E zJ80;U-!lZgaQm)6?$xYDJ#N>K!@v-9#qGNSxs5slZhzO1aio<8r)S(=ghk5)avOq1 z+^!+xTnpN;yK?F-YLM-=T+p1`H7*LWXqkne8ZE26sBHuxw^8TsMQyX~Ed^EF_ErzZ z+JdS-7=>%E4&d6$)xF~faO!}TvPphEtzG8kas4HBNm>Gu-&IVUH{P01982 V_{{^B3=9AO002ovPDHLkV1kVB^l|_I diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_leak.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_leak.png index d65d7d2253970faea9e6122c26d426ea176f2123..1ed5c903ed05d69f1f738001c34fa12f7c5eb11c 100644 GIT binary patch delta 334 zcmX@We2-~@WIZzj1H-j@8LNPlM}SX=t9yZusI-Epf`X`wyojuV7>JY?Q&bd_mJ^Yc z6P1-0l~oXtkr$Pg11SI^d3hj1R8CGv4yZtuL0cOtHUQpsO?@baXpg8n56_3UyJMeZD7ICikvTK1s#Z@Et&+UL3a?nJyRY%uq)T@vMqXc889R$TD9nBHt$APXSCsGL X`7CUEyW^E4(CG}Gu6{1-oD!M<=a+bU delta 433 zcmV;i0Z#tj0>A^18Gi-<007|tn3w z`WxAgwaJkXRqJ2F_CDOof?ZOd1- z{sr}7y*@!*#eXWSVfn(j9q4bUQLMrmmM^T({;0`s)F@VA4a+C10{RX$id9&H`a-{; zCjA6f4lPVzzQfb`rG+OBy%aWU7%Y&?oVcdjqL;#E4TA-esT^rxYNOXgn>7p;NZwoV z3i%Csv%mt$n;kWI^P@KlG*Dt2{0bIGkgr7fT9mIwJSv|JE70WpyFjV)D;u*ku0_iH bTKEYc(%NV~TI9&m00000NkvXXu0mjfA(hWN diff --git a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_tv_icon.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_tv_icon.png index c04699eba9f813099294be37c449ebf3e2f9224b..d8797f576f357009f5d9cd96055082ff6efe827b 100644 GIT binary patch delta 727 zcmV;|0x13c1>^;g8Gi!+000fQ{6GKz0fSIXR7L;HGXMYo^}Q7T(oFx&HvjtT|JPmr z%{Bl2`v3j-|Ia`F{Ph3givG(m`rnQJ|NZ^SGIL`mka#Ko&O86yasSXl|K5B5-g^Je zIRDmJ|I$hS)m8t^H2=>*|I<(Z*I@tl+W+K||M=nm?6Lpgf`9+(u>bq-|NHL$z#a9z6#v*^{>v};!5#R)9Qwo~|ItVP-+uqyb^qUe z^}QAU(MJE(SO3{%|Ia@6$U67H8vo{*|KNlF?6d#$)c^kd|N83x+>#hIekN^Aa|L?#1#wYl~9skZb|IR%1z8Uqt75vC9_QWpu z(_;VAQ2*U_{LxSO)ocI${Qv5y|L(i|?!^B2>Hhoi{qDj4`R4!q_WIzD{Oh*=)?5Gi z==f_i`F~LPdpG%iQUA;|{GK%Zu1x>mfB(%l`?8k(%YV54@WKD$i~PQ){_pAi%(?&h z_x{|F%zfW>0003TNkldp|Gf;hDZ&WxeP#1 zT#{c46-dg-#;1Wh4yas6fPtYD!cXIiVM@fO!3ku(pdbT7c^;6L8XL*P#KgzQnB|2_ zgEUBkfPWB>NKW^3U;=}Tl=yHqTpENKU?9N&tU4+zIs(XZ#-%|9uE7kb+S(+P7sB(# zr9l?1!O_rElNZL}^1-D+1g1n=%a{QHcyNZ2Dg!nks)kboD+@LaE_h;E42ybSd`Uxr z9bLJ(4nN++r--arSI@%I)eV1YZ|5&#byB$Ad2n!*48002ov JPDHLkV1m&|yMX`z delta 752 zcmVnu~K~#8N?U&1I6G0ru`$yD+Vost(kSZFj zFBCl}KA`m=A|m*}Q%@p_Shb*dQPk=|P{FHs@C`l?V=6(aeZ;h;RjKxAu}zyOG>^^h z=Id_~lDfYMo6IH=3Vz^A*xk(hzTX*UcD4$$oGN}}w(&mz$A4^p!EaTRvIa0aUP5bb z$XsY~>%$6BLI4x^)d~z>0tT9q>TiM8R7U}JL4R@shUYGCP!XI3(8hq?eL%-@CO3>b zZX_DrNJYkZD=R4jM(7!mgIDMe@>V%g+BlK}ml5;T zBKo=-TCkD-MSltP0An|$kw130Cav7P&N`ZVAw)fM)5fzBVRD!;AmW|5 zE&}xc!QD~-G5TT_EcYkwA*KI@K5`rJwzU@1f6ENhItsr6zMTNN)&PC`c|}{)19YvE z0_2kss7KW6k_@NinS;bz7mT)A>TkWZ?cjM{(H8Xpoqul0Y(6D@3z%^--Rgq=t_G?0 zWzzq1ntIMF+M*u7zez5dM{(~w8tG~>J)cZ3vNxTTeLDXfgYxVU40&-1r*pqGk6V%_R8$eYYY iP$HEH@DG{w0sa7$C(Qi~tcBbF0000eA3_W0hN?{x;TbdoPImWldnNRfF&?c zFs1PA|NkNU8nbv#opec_>#^)o;sVKP!)vM5=PvnOee4iFF{oWwcCLv+Q&#rI$j2=j zyFOm(;a)PmXu*Rkt`&?k6sCT8q&ngEV=1ZXLk7Q=E?>H2zp(T1#uRwnFboFyt=akR{0AjvbkpKVy literal 301 zcmeAS@N?(olHy`uVBq!ia0vp^DnP8v!3HG%u&z)5QeQk>978JN-cB>*J7gf@{GY)? zhH>4|9t(D-Iek+XUQb9&Xk=u$@9a9OYtopxhQ(9hy&>$YY)bH$wz zNqlIvdmeMEsq8Qg++%wv< zXKkU#dfQ!|6;t;IY~x?Jcj3{a&P%V{mSC*X`=wW<|0{pblW5*oSEhQop5^YyRf<}z S{P!`?{|uh4elF{r5}E)St#)Yu diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_foreground.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_foreground.png index f67149c79ceeebee0cc26e04178db46e786b087c..a3449ad5f9a77ffcfdc31ec81384e05fd6603f07 100644 GIT binary patch literal 1923 zcmZWoc~leE7mkW3K`cu`RKNg5f^1195WuRHuo*y>5D=|a0TD>B;Bt_y;BxRsD2Nmr z#f4q0kZtU+;{JH@AtiT-Y`Of{0!jR z;4m1>fV|(E0)8u@cjG$HOB}^JVX(E4A%RpMzvMu*2vDnGY7s%>0ksGZ#w&aqbuSoe zjQ|C`SSZttgJOY)24$)Zlvo3C9GQ^V|`{ z%judf2tZK7U{K2`FoRblXu+E5utt^84Vn*0`hSadGG4+#fca%E_LKUQIZ9_=8#*#LKM1$9e0i+zH6dckE$P*y@%q}7<$Ggbl3357C9U|qRL}r*xPulWPauG%JsIr;q&XTSm2!~_op?p3#V3K? z3g24FL#c#2OR{&~$g)@bftIyzS?$5Db~~${Iw-qr6=#9fPC#)U$f9Amx1AQB;2oW; z(&8}gL@R!UvxT*#jJIvI2~afv@%=+&zPuB$#b zORt_isLOsf+ApmPKShGUboP?HJ*la0W=mvYCZ323ZUu(d_8GSrdhcUxw7yin*0lC` zQY1TSo+BI0Xf`&~aU9YgQu3zO7c>X72Apa2h#Qr8ot#_zZn1ZGq%jG*aSgcj|32=K z6`4;e{N0YED4CrG|NAQnQOY-S*n09_rNIVl+!nF8?3_Jy!9Gst*r;#nOko~cCUK&C z)7Q70bUVQ;@A$#A>~M`a2mbwY91Hd0$`Adsc{!*nfanl^<6#F(XeKxLM@--8(pNu4 zH`NrK>5#sw+EXm(%iUda{#Jfe?B=OH<$cc5GZcFXHGpAknXxO}trX#IWbP)?M-y+= z^cUj-Co>+Od*p6W=G>6}J?*cn>_>M!bT&!sxzQM9XWBx=Ko4P@fEmng&VP&hl7ef& zzHV%o3-Yu5H8pec7_Gc zkMYu`n((h4cbVVy*MB;ZeJUa*M1-9D?Q(3!P#N)`f;*eC3SR$yAtja-)@@#fBNckG z%l5JF)hkiMOFx|#j8S5aoOXE+s8S2mzVH%+Y#ZJ)4lUi!!eCxshjc&D)VG}Rz{NEFB(3C3(DOO ziw49`=RI5h8C!5C-rz>(6^^J{+H`|4L-xB8P;%F|bQ#k8vKKxeaN%NmznGAMW;$CR zNVF%am*~i&haRK`^;z@Y%zUh8z$rD!j(I}sZ;|tR(!9x5@G#!Z*{>MA6jhUHw1q9- zy^O#PXQ2g_M~=$A^SA9z-eH8C6jtA4%zqp_xZNwjWG{c+qTO6)s*Ve55+{m?pnOD(a_AGX>Oqls-ZWq|$|M>)crZ|Li#AuU8(zJ+ zwKcCgI`ILO|3cUj;gj=Auy4ed*8YJvsR9~-H7=DDJU^33XF9yuUMgW&px;+=4`hOh6u@Z2R^3ky5r(mP*aPWB1%t|J}4^e;mj B+4%qf literal 2506 zcmbVO`9BkkA76RoSm^0-WF8i|p5$6`ABCmKZK20Zu9yg|Fh(fJQFAZH90_ZM+?(_3 z%;TCI(a=PhYt|+Vjc?Dt@VsBI_vih~`~7}>etai8*jb4PgM~#5x zpe4b&4Orede>_zn_EID>O&|B3B;ptu_N z@1l*fCie{ELQWX{o$E^@bo$Yl&Kv`P=NMjBb3p-Hl$uVq+Q_ByWy?0|BJxo6=SYGN zxOC1XfFJbICA>!fx~RkL@zP!+mS8R`!b+b27)67{4U#tIYMb)c$b#b~KYEq0sZ{|St2#*aKs(*FQgYFuwb8Sq z8LaAvh8lwI+FDuE!gVYQCpecJyt3cLs-_OS@x|R^&lfwVMFh>Ar*{^P6M*YZA7}#3 z8DpQ4kN;_}n8wGv(rxl(zAp0i&|aCONlPozY>+XRY0}+Cwvbx985rD-7_~4s*)JIr z2i%k%9mqajXmjC5;ZIrOsTfsdFWg{vp87#}Raxk~Fm0-~Kk{x%8t*xfJ+vcdAci!2 z7~0i+U$Nm4keTKf38@{k|H`u9cj(5bNDZjf-QvJDDrN4=e}0FO@WZ_S+MD&P0iTNg zCNgQ&_e(!D*A;KX^gC-a z5^TcQb+jxx@DldG9AC4Pw&~PPuc?G_p(>11b@|I zA%Ax){|c6m@y!}Y-vC!TY?Y-bNL0ln02PD9Z7}21zUgk}cfKOrL-O$K4;U>8iZ-t$ zYlC$`I6ug#S_gviVsy)wd|NnCwT{8P5S`W zZF6r@5l@zBm@UuX+0HG$6q13$gT@J|s1@#c&^k(sKGa&69W11@aDrEL{oDaau2Q-W z9`MT$uyRJqGvsC}%PDgwrcS-|H{(&vK!9)YMVfPnxQi+5J(>HwdV%hmlq&q$SyR28 zD+;#9VE(L~IVtPzCH|=)IZ-90rbutqREl2Sc6Ro6>)wJFG6)c@^E+2d);&nFP_<*Y z_L~l^Jr~Q`n>*dysqxG6s>VK7gy{nhlTNHm)wIkj`=ig5Qbbk%YmHXt&~z>`E;?@- z0h&v4Tja{`Vy)H!UadzB_7`{{!H@M1d=KKS$nj$NBFMwE`!&+6Jq=0EEOvgJ(oiDV zr3oDQIJzRFRVND5Q!x8s^TCt@mV&w3BpdlObjr_tvvdr{dS=aGDjx zXc!GQ5dH{KLgp2I;12kXvS&`wU6uvzu886#;)se&Bfsb?c{T`$!}q@Jt+Sof-R|?> z1$C*Ak9UwB5hDBx?F?Ljnva|cDg+<=o9RN>qD0&A{bv#o%_U>SSpjTsLD|7b4urn= zGv$3^NM1#@EHrl?^?cI^d$elnK<#4f{{S6LcrwflcGo0j2gYhK46 z^)SneMu1;A7I8L+s}A%2?wLF4h)BJyPS3%-Lk0VYuN?EZ8`S5iQ?FUTtOhY#!sbkr z!4E?HTf)z+A!Cz~zCC`*+f%pp&qj=IUUbEdd9N1Ot{M3$6|yGEi?El$GE7kC>-5;e z#0o!W{Ap4`%M-lo%KGx4nA>8wWenk6k^iPpA7!hP;}%+r&?Q3V)MC>~K5hB~1uOQ9 z;Rq9{Z+iM~(0BpTnO&`2@>}jLWz=p~OP-S5)QIg|{m&V<)hD>B;*2~pY-DJ z?kbr_91<3by1HHTV6Fpbq=xS8?b=j9xP)(85^NpmIfRC0G>(GMI0sK=dtab?*r=tu zVlYt@)#By%0$jH4s6TApS*e5Fp9-Uad-7fY#tT~3W(sx`_|}{^>O{_-B{ODRC@d;l zZPYU|$)A7-7d&%mbaCXP5So89zL)7=Yf!DQ@8ShJFw)s$lh&x~Q7iF7us45*txa!L z?30I5s$RO;J9-YCrUW_o{a{~hXP}BNx!y}Jd!N9<5Aofwqfh92F-sOaEB5{6T(FMw zCtNl7hUM>**vx&n`{lb@*;zi~&87)q3&uLJp)-g4uUTcfz#d#EYs0wz(*=gevi!E! z4UH!_PDQj?&Jn4rGpps;GHkt_YbiQ>?IFOm_+YA1v<&_;Da=!TTOGM}`zq`pYfa&= zJtTm?v?KRUq;m&q(U?^kX}pVxoioWe__22@=O1*J{@3IGZO;GzNUoW8Q-w{Y$_D4( O3K-PRvfBJs!v6t<>&Wx~ diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_monochrome.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_monochrome.png index e10476ea13a41611aad365e2325a3ded3ec72a88..732ca750026d59ea1278f3cc729a3df9331f287d 100644 GIT binary patch literal 1892 zcmZWoX;>5I7ETd2Z~+q(i3m!N8=!M9>2Z#9#(fGXuGQAyPXNri@rPIEZfic_?JKv@NI zDht^-Wq?@~8~{|3*L%S05?5C4#|bE2PS@v z6mwx2GJ%n#VK)dQ4LS-Tewhv}aZ$?=kh}p}xdKV(_?09`AyAg^v;ikz0~kpJemR1$ ze3k%^V&2Ya8W!{dF{}rkq0)c`pht)YC1S4DA_oN&F1$j442YF5czTQVKsLZ=0X@Jg z1LPuz!H&RD0?rt7*ni!c0Fspuqzq7{C7}_3R9QKGC6543e37dqje~$TIR{eYSuN8E zplanD0rV1cP%Hcc;s`KjO03wwmJ2^nwnOvUB9R|^Jc=xKxB%1M_-vguBD6lUF~jB z`W;)}&YlXx#BcOhqH_Jey-fc#Xu>|{bfZg^jJ)QVtutC6o)S&_B@d0R&V)>@p(Ycm z*tRcD2tI8&<~nQrEK`_r5@MWmw-4>ch5ejR{NN-1<&CVH&;PA4^f5*LW#abUmIcQ7 zU|Af@QLOS-ZJ6KkBC}xfrJIAq8vjfAnaD)!Ai6)L^dcoEhm=9-H)|)DYMgB+8czxW4i`?{&tEO#-EWHayM?uoWj}p)ha5db8LAP*X^yeC z^2#Z8FX)qk`QYk*#ffrXj?K(6Xeir+Ms?%Iqvoxnoqw#^_TJU&JHwl_F}S-a&4{0& z8b$NnRwXV;N7f!rW}*!yYW@@V+H1>|gjjB+!&y$mBw01eq;M#xUixQUzlQf~Vnf-X zXxCemj{Cbxh0NNvb3DFt%;emMsnyfWQ1uPX#t*}^i#+L?d;A01<2p`?_ott#$aqs~BnNf3l9) zekz9l)bf2)*JD$&oz~4i<5P<-E?ZY)Of#IUo3r-wDAjAvEJHoo3h>wTmii`#a<-@1 zd6NpSjbd^F>m$QX&i~`NRN*~vN%)X7m=kEdl!OWmyBr?N9_dSfCwoJ2-gtBSp$04Wq`=Xd!z`NANp3?Q1FzlGaxZ&wMuZ z^Uc>#J+FJ*_7>@L7V9v<{*|jq;68fx^$XQ__qu@=lH342ZsEk-U5U{>hcc0S>5U!S z;^7|_P1mQV&p1_NAfMhN+{8us7d!JBpi_A^G}+OUMtk@^q!ORz=`u50l^=gPG`_E9 z*fZ4+Ra~_Ws~f`WX&k<1wLTARNiNLm615P*V?F6-w*9)`@uIfM{E+Lm#?4UnE^mij z&wV`C{LXFZh`E>7@3)J(iEmyM`6q^K>ysh47C!omsDQ^?4%x*<63+r9FLdX8F n(zn4zGZ;PwfBSPm;*uiw2#fQh`x$wK4ZKcqq>>&S4f_7SC?Cu) literal 2539 zcmbVO_fr!H6OC{xLB7i>EhUEk~XIQV&yV}yV zv6L1R(El)dZ-$|xm&FRMt3(!jS)v{!6J?vf?98$b1J{R%8BxF0{5L2>M@2R6Apvz3 zeygw*0lL;E>4dXfm`ePLV5h?DOz1VNV7DmA$A|crDA>PTcWd76^^OGR`i-wK+kIS; zyx0s7G(@}gW_tph+uHsGNnUgUtA01)YF53ZZh>H7)mYyCQLk|(Jos9H>Sz&G+k~8S z`21BYF#H?cQ)u;{nEPwJw&OKg>0{CCYLs>2zg`y=v(5@Uj>B`FkNO;%ZF!NE-1ZL& zm#21@jwcCYUDBVt)iuY42i#mDt}fKI#3lIEX&YPwYuX4A?E?>U4jTa?#RS?5w^cZz zDBjvgX`r*_F{xnmL8Z^KX&isDzwc9=E%bUvk5O9jN9VopH? zqVv;%cJidOVx5;9uTDtEj`RLO>StEVfZwR#NV*0DB5fr>rnrjyptWlehYau{d9Vlb zk^f@{-w+sy-jQ;-0<5U_Gv?^yDm=b=jbXKH*Nc`?2Ag`-w}aUjJLR1etjbfyk?ttX z;A7m=@@R)c@r_-sjF=O(X?#eiQYr$uy6eOcb_pJC+NEoqO7o~#Dh&+z+Osf~N|~7B z7e}a{B(-q~x!f-UZs=m+_!G*}24oR)UUfl)5TTm!7=!Z<%D1i{#O>&F_7OHhD=|q7 zn{TJCcN`yJ5$nGK^*N=^DSpqsLi^AhRu=0nm!ed4SyVP=@ob%(g-lpm zWb>Pnr=Wu)%kAh3&%jilOKY3@sXXjm7PJaHki@V(WL+MO##Vk$FJt4XShXSf@H3B! zPaV~cb<=>CcbYpAP2yO~jEK*yYaid|e94gCml7LEAjO0Ck7C3V!zFaZ1al4OFHbl1 zg5d#U$9DBqEJu;X5EyfHWIX294+(}#KHi(E2jlCy{I?J`78 zLF+*ypExc3@HA}Cp|1DJb-W)=rLM;%K9(C=XeDaynxb+FG>V|IRh@WnQ{2FIvpYID zPu1=%sUf-g!ItE19OIZR?qlc+plcXB7@w{K9(dZ~#J_JXIDw(#xWO=5eJu=^5}FZp zx7eOFHPPp<(8_6Xmi-=>D()H<3o`808l755UdXZV-uze4_YNllUH0J~FTE6JCF;6o z=rfcj#?F&17&b)b5~Q_DeUrl0bypL;O6uCDst3MW>p0c;r76sR#0J7KQ z&+O5D?a=D{u4z;c=F1<=Lkei8o1z0OkhkuGP=zXk9x`v0<%K6fAi4yx1l89O&y4#1 zaGw5$Y9SP3M^8d$1<=q#9UnO_d5HzZ!Zw{s6V1`L|>e>04#q zHP_;!RKn?rB(?3X>OAlgOL$Ju8rc-&2=w&*Do>kog&#-%6bovHv3L&OL#qPi2u^<$+S+-jHA@4a(xqW z+vJNaDgGy>REW&4zi$v==~D6<1PVOwx7etex8Awc9P+U(BDV7Q{W9}{=zRF5gPhmh z8n2s@QoZ-d_86TH6g)*$WRpi^jyX3Z$V?Pvyj#?%!e(Ds@4zjQlCu>N&v zM2CG&$I7E0tGD(VZsrY$BToiYZCY^*joGIq)w`_7-D`?Nw15N*`j>QJ>4uf>tw}O& zv$u?0rf0vc%~`B8aA3g;9T&T~@&6EI7q?EFvrSe&%yU_~T&Ajg+4sWc^{yJ{4vu_tL1}RV&r1<^W|Yr25-ih58l{^`7Zh!CFFYgX)Z_o;yaO$SOMD3u0E?NY~7B z0hz0X_!N^Jfetea4ggRn$vNGqq*gVlGzk37VQy??R0q8u_J7@W=_LRF diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_info.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_info.png index 718690d9f47c80063c6d92e67a950da2807e906c..38f6d759c8eec8ba8b0ec726907f54dcf6d34d17 100644 GIT binary patch literal 513 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`ZS^|7RT-^(NL}cWEJP{Ff5g7#_ zLsUi{h{R;%A>t6OsI)u;0mXp`BB-LGA_@dD3P7%c0+5sulaYg}mjf|Gq=85Yq+V7Q zDh|;LHcbR%1<0t^@8|9TTC80Xr|i2i^+i{==D8(z85kHDJzX3_EKVPta$Ky*fyb3Splii~1pS1id;b5o z-z<^PRN;3gBWz~D`8`uOz5bqmlU*92KmE0H=~jD%m;c(X<^3+{=&UvP{MbCj=KBlh zh?Lz|v!;tG%I^8ODPRov{!NJW+b4<8hFNkuvwCS>L!eEnl+Z#kuHH zH?N*KvB_=LqTPRTxnmoA7pF3YE#ASX_x7l&z>0DYR)?D^g^O<8-eqOEYNA!)r8?d_ z?d=8ZS#RwcJ{y#<$=#}D+9@?D*z&`bcY-f|WvH!_dGLex$P<|#?F}z3_}J_S3j3^ HP6P1fc|w8Gi-<007|tn3wA10uYM3+OQjMi<#9UbI=x4>)Mq_rfn3PRi&O<- z-dNI4UxjyM^SE;X9()nAmVpE1E zJ80;U-!lZgaQm)6?$xYDJ#N>K!@v-9#qGNSxs5slZhzO1aio<8r)S(=ghk5)avOq1 z+^!+xTnpN;yK?F-YLM-=T+p1`H7*LWXqkne8ZE26sBHuxw^8TsMQyX~Ed^EF_ErzZ z+JdS-7=>%E4&d6$)xF~faO!}TvPphEtzG8kas4HBNm>Gu-&IVUH{P01982 V_{{^B3=9AO002ovPDHLkV1kVB^l|_I diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_leak.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_leak.png index d65d7d2253970faea9e6122c26d426ea176f2123..1ed5c903ed05d69f1f738001c34fa12f7c5eb11c 100644 GIT binary patch delta 334 zcmX@We2-~@WIZzj1H-j@8LNPlM}SX=t9yZusI-Epf`X`wyojuV7>JY?Q&bd_mJ^Yc z6P1-0l~oXtkr$Pg11SI^d3hj1R8CGv4yZtuL0cOtHUQpsO?@baXpg8n56_3UyJMeZD7ICikvTK1s#Z@Et&+UL3a?nJyRY%uq)T@vMqXc889R$TD9nBHt$APXSCsGL X`7CUEyW^E4(CG}Gu6{1-oD!M<=a+bU delta 433 zcmV;i0Z#tj0>A^18Gi-<007|tn3w z`WxAgwaJkXRqJ2F_CDOof?ZOd1- z{sr}7y*@!*#eXWSVfn(j9q4bUQLMrmmM^T({;0`s)F@VA4a+C10{RX$id9&H`a-{; zCjA6f4lPVzzQfb`rG+OBy%aWU7%Y&?oVcdjqL;#E4TA-esT^rxYNOXgn>7p;NZwoV z3i%Csv%mt$n;kWI^P@KlG*Dt2{0bIGkgr7fT9mIwJSv|JE70WpyFjV)D;u*ku0_iH bTKEYc(%NV~TI9&m00000NkvXXu0mjfA(hWN diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_tv_icon.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_tv_icon.png index c04699eba9f813099294be37c449ebf3e2f9224b..d8797f576f357009f5d9cd96055082ff6efe827b 100644 GIT binary patch delta 727 zcmV;|0x13c1>^;g8Gi!+000fQ{6GKz0fSIXR7L;HGXMYo^}Q7T(oFx&HvjtT|JPmr z%{Bl2`v3j-|Ia`F{Ph3givG(m`rnQJ|NZ^SGIL`mka#Ko&O86yasSXl|K5B5-g^Je zIRDmJ|I$hS)m8t^H2=>*|I<(Z*I@tl+W+K||M=nm?6Lpgf`9+(u>bq-|NHL$z#a9z6#v*^{>v};!5#R)9Qwo~|ItVP-+uqyb^qUe z^}QAU(MJE(SO3{%|Ia@6$U67H8vo{*|KNlF?6d#$)c^kd|N83x+>#hIekN^Aa|L?#1#wYl~9skZb|IR%1z8Uqt75vC9_QWpu z(_;VAQ2*U_{LxSO)ocI${Qv5y|L(i|?!^B2>Hhoi{qDj4`R4!q_WIzD{Oh*=)?5Gi z==f_i`F~LPdpG%iQUA;|{GK%Zu1x>mfB(%l`?8k(%YV54@WKD$i~PQ){_pAi%(?&h z_x{|F%zfW>0003TNkldp|Gf;hDZ&WxeP#1 zT#{c46-dg-#;1Wh4yas6fPtYD!cXIiVM@fO!3ku(pdbT7c^;6L8XL*P#KgzQnB|2_ zgEUBkfPWB>NKW^3U;=}Tl=yHqTpENKU?9N&tU4+zIs(XZ#-%|9uE7kb+S(+P7sB(# zr9l?1!O_rElNZL}^1-D+1g1n=%a{QHcyNZ2Dg!nks)kboD+@LaE_h;E42ybSd`Uxr z9bLJ(4nN++r--arSI@%I)eV1YZ|5&#byB$Ad2n!*48002ov JPDHLkV1m&|yMX`z delta 752 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 M07*qoM6N<$f&g?SA^-pY diff --git a/app/src/debug/res/mipmap-xxxhdpi-v4/leak_canary_icon.png b/app/src/debug/res/mipmap-xxxhdpi-v4/leak_canary_icon.png index 9ad0073e342b67673a8ee29dc8c83548823e9373..396499cc5a31715bf1d7d668c7c32bf680ee8d91 100644 GIT binary patch literal 2725 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 M07*qoM6N<$f&g?SA^-pY diff --git a/resources/src/main/res/drawable-xhdpi/ic_action_more.png b/resources/src/main/res/drawable-xhdpi/ic_action_more.png index b9df4260c511a9ee234536fa16d0e026420ef72e..bc57cb1c6e34c181af3ef6fd3e0a16aa0a12c3c3 100644 GIT binary patch delta 189 zcmbQmbe3^~L_G^L0|UcSohMs>lxToYh^u>n&wmWC{S@OZpaQm%AirP+kG&UmoVY7~ zGX^M<=;`7ZVsSb-L4q}e`AmWlFW&>99IhQIlL|bSNIsb4!8oa@ph0D#Si{v&ht7}~ zp-euYTwtq!o@a2ws)ik0W}Zr&DV!mT3!?mP?6k1$(Do=`Tg13huvI&&P^~S{fsuhh XAF^b`grOC`+r$y<7P>x0yZ(^%(fo{^o3m`J@`++9sUqwdN4Efu)zI}gX%KxUjMpgI*Z@~qR=Q59f z*gQGK&UgHbQPro(3hR--+oweJ^SPI zy>%+d4Q{%fTTkj8T=g(=<}ab8F$r2Jxt2*1vOcrr)LiCmwyvGJe12bbuwYSXS?~L= zBIkBo3*bmxd~EW*2~XSd?>e?eM6u|f*fXo+y!oY;)lT9Y_a6wqy9=14?cKJrGiT0}*gsDCJ;!wO6aNeM>gUy- z{~cN7Kh5RDB(eFQ-o>u7T4M6};pgJdcfS1Qzr-+2`>m*?af#{)#w@o$!y7VTx2Df( z&wKDJlyAa;hy}_~tF|;>>@!mS!!&un(2Fo_J)Sv_7~f_cFjbgtxRuQ&gn3oN;x*Yv zIwW3kujQU6kRj-7o{-v@^`*lxd|%>0?K>)>)$DfXcK8>BcYVuk+OVzc?61;kLU*6_ zPWsAm-r_*kj;HN^)O=kVPlg1%ynjj4+IB|5=i*r}7MLtuoAzh2RHX`-x%GKxwK*p+Z5)#ZzP#bRxk=4IJEj;O!%U!M%IDaTTBm4Kh9KWo14@B zCj6GW()Ik{!>s3aTx?6++U#RrJ;^-y_)g)Y8{c|u*S}W(aZBDyxyYJd+|gZYzRE=M RwF1*PgQu&X%Q~loCIC7hhHn4> delta 1356 zcmV-S1+)693e*aa8Gi-<00374`G)`i1sh33K~!i%?U;GURYe$wy;+gAFIJkAwrCll zR1`%8A!ZPoplw7cL5P%CP$^Ng2Ni{sSt5d@kr65NS6O@pVFjtFEt+OxE=gspm8JFd zJfAamy4-u`bkDhjbO(N%WoCZwyvxk{o>S?zm$%AZzW#1)1%Li)54Jtf_CP@kv_q(% z0Siz6e|ey@v-4m~HWZu=4h0)4mC92^%n0W#%s&qN2tENFaDJDUGnWStasY(afcrt; z4EcKqJ1?)WTI1ke!uzu_ecQNy7|eqEhZJ{S1;#)$0~`T<0xN)4`V=9zr-U@>M}a5k zKN%bb&Is3ExPRUY_hTvUyb3IbXfV(s!@wr+JJ^X3UEg#=0Yh?JrET5nCl?048rV@=VJ8CWUz$3g*g?tfN>+h-PM~E zzSnrKk#^A6BgL0jfewf+2CE=dk$*IV_UFZH;0a|r9+)3|ZwPJ#?|{$2F7PUt0@sH% zxE$x)Er0Zz;2O5P3M_zS6!;9%Q))4y#HGCV1nC6~E5k(Z*!}=Jfg!dYJPFV9b$FKY z-jH0)n9~X?z{2)}WErr0SPbcfS<(&jhak zb3sgKe{#PyDxOu~5IUv;H<-=7gMv~n{vI$Rdn%x`E_Qz$`!5$ChkwJ(NI2h(FTx3B z>GC)@23Q;E4a3l87>hU+|O`79_X!;qG#&xr4?`@q;8~9fAVxlVp-f34gEZ z*Sm($o4ofR+0L_{u%&ol3oLHvw;=UoXjwx6H}F-!gP{lV3V+b(r5(Vd@kI!)ZrSP~ zMgeotq#&ag^2`-=TQY%2ze&U<`r-zUWj|ZBJ3RC$A5iSMn-_K+H?y^(fKAss{xaj#i3z)H2nSrDXf4$ zpZrd>v@nIg0jyCrAz*gXtKV&$8@QO%4hJ3(o^~3=y6dJCcXAI%fDr4Q`@lHhUy9gT z@ng$VXFiyW0%rDxEtdj83CErvl(1g0dR>W-D^o%m^<$iu={IBy6^lko=|x5G#_F>i z1&S=Pm2-1@pp}a#!rfM&i1o^t+x9>iRxHBN_COKql`*&NfikREgyS!T#I-=g(vw2~ O0000#wt`Gcv!u|LqD(u=Zq$hwmS3oNKKIl;7y-;usQf`0cdQe1{Z7T(TYI>~5|4 z{Qv$dC-J3G3Wa}mH&9MOE1r7n}7Va{ax9Chjuub zN0SEiXGdBM@=P|q>iVxnh^h=G@Hb3+$SZOMA>Wr000000000>C+Wd&i%P%o zn(A$ay7z0TE*<1C)n}?Zzpqr9yVpE+el6AM7?An$`%0;ifPc)p-(Nxv1!N8io^EO^ zAk+DAIchK> zGYa)dYA_&E+qn1TRZ5j_K^g2V01r+Xc)KEYd^*L%J wAU<9gqhmmP{&J4^fC>Nr000000Pxoj2U|o^h?y{T01E&B07*qoM6N<$g6PY*hyVZp diff --git a/resources/src/main/res/drawable-xxhdpi/ic_launcher_notification.png b/resources/src/main/res/drawable-xxhdpi/ic_launcher_notification.png index 3b8dc987f4b6016e833a9f3b7c92d034aa861f10..70302fe8e20a3ea03f3de88af6f6fd0fc1373b40 100644 GIT binary patch literal 1613 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`ol6-Qm}~=lLR{SoeEyGu(GVaj1om|< zOa-Q=eI-GD!3;b8W&UZqYQ)&bUeDb6w`?D_$E)Ug%d1L$?U+9CVdG45 zr=3T$G&m||PdeCsY3c%lTN`~-a|A^#oU6EGtwplq_(~eq@9tjw=*h}Ems|GES{HX!`S$+i_Z827KW*mm)2h3% ze8VB`&Ls>C%#5Bcjv*0;-%kG?f7wCeSaQ_c4Hv|sxL74yjj~P)xuoRZ{|DG+&REo|c|_ZUW68OjEo{1vIHLs=cSf9h7FOW$ zB==&*Hm^vrY?GU}W4_FKG;zV(1PcrII;C=%;uoVV!Yl?$);-}gLUM{Y$O6D;LF< zavqDElO}Mp(jd;Q=^2y%=k2?46PNLA-116$(%iy25nfwQkARv7qJ^sACsbUo{Ja>N zaqmEve8tWi29p$eySQ(1_8z}vsr#P8@V>}5@8GKk9ba+p>)Q9VWx{g)CV5`tmA4k( z6F$Vx5j!#Y$^0x0(F%zpvj$seV7E9j2zK0Voq0gHF&%`4fMomvp^i+%Tn z$)2&VUi2L3=zl!_!Sy%MyV|Xy3j*%zOi*w3>)L$WdIQrV-DM}v*B8f>z2m-XwfFLp z!ozz1--y~8mA(qGEw@ox@>u)L)l1@>o^R?Oaz*?}`zvtw)BP7S;||-HT=USoZF@gC zazi=id5luKElUr?o4ec&kXBRXqK~dx`XqqCV+k z&2E-?GG-?|0{A~H{cvP%<16;#e}(jXf>t;DPpiDb*R)aMq2Yn!5ltD6?Fu!f1+v~3 zObwo$*yA9p<5-!Xls#RKPdWctZy>Y@D zrwUFH$@<;5)?JrgqQkcLUR~`3vB#coe1x~KoH6TO>UGL6VdIT6CePO}az|D7eU;v{ zKuW$QHJ!~nV6UIPb^onh))&LF*F2d%@oH7MMcnE?YXg^fTr7Fbzqa+~xfyyFlY!Zq N!PC{xWt~$(697XxDgpoi delta 1945 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 b+`IkNR03CDLfV|Z{n!8u8jGs3BGUc=;^|%G literal 3047 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 JE6V~;`ws)_y#@dP diff --git a/resources/src/main/res/drawable-xxxhdpi/template_basic_activity.png b/resources/src/main/res/drawable-xxxhdpi/template_basic_activity.png index 042c8a9f879d10a8da696caf5647bc6c60331dd7..2aabffe943844732939378c7c1569666b7e638b1 100644 GIT binary patch literal 2013 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 pUr!fsPwlz+=`$m0CX|s!| zm4*!sbu)+@p;KpxmrctwFE}k7csG&qI&3>Tr*k@I?;qdyyx-?MpXYntKOTYBbM)o4 zyS3B-05kFQQPpL7WRJH80C5%ouulUptFqXC0T5*ifSe8h;W_|oB5plC;HWC71?_Wl z0ZZpH8pTjx$mrQnTMH0ACFuaHHN{`2$EtS$XW(lf%!lRe?Tnpig)oTZpDtocJ=oG8xJ` z+)3VJ#1zRd2}Ugjs9DTmX=#4mj2pFyQjVXOZ|aUjtY+Y6P)gLs@ygk4Bd6E==^C^0 z)UTe!TeseN8CdiPddv(Gr{7cNbZ!?OUMQaA|5XR|=7?$z)x4JpQ0x%qxGiKNP_!lC zIx(a)j9%%J-(KU^kqUf+kBlWfYvlJ2KTl%aHaCcGbQOn}FgGUYe%T;}46A4t;HGga z1@~~itxG@s@(`E9I*iRvTq>HLa`)~&g@Q@IckP9unn$^VsSXC|(326;DWUY*V6z!r z9|A=ShjwS2^@7Y3MKWWXHf2BKXHI<0T&sTu<>EIcERhcUHaTmmeo~iiX9z~_F@AZ(_fSggo>oQa7_4X4q1XU*(`?6vdvRttOg zE(2xOcMCs9v|+^hrkeu|XkZE32TI5^jS)BBmWSIqVxorxQeRT-x{eH9UIyQ> zK2RT2k~;JcN$nEf1yQl;(OG4mBSijOMCjN~RE_&>^B;4N+hAuA%p0siD9t=rIJ`rL z0n7Z2G4jmDSR?5*YLViC zFT?SEq;r!uu#gbATI&Sdi<(vA*rPPH`J=FhTH7Bi2&O)+R?h1S`qQ;hRPIPJz>o6Q2xbGyl>#l*;)oMGmsos!U zm)KB+0wB-YORboUY)p!BL{|LEfQ?MC3L&QB`5PH6iS`J;$%H zkFC|P{R~)>n}H8pazy`w-q6ClsA_#zl;Uyl{x|yPAV7cc53ON`Km4{;xYiwmM=KV+ zEtmg|?Sj`m)^4c*V}w+y`)KE*{PiVOk5#xP%Jiez8~{mv>oP$;sWOr_Bh@{ORFDeh)GqYl=c5f|;|)ag}^E8ZHO zdASK&guazcT63-tAPu4PC{+hQmb^MxK_?|G^tJ{9>2*Lld%=B0M$`?(D# zmDKsUkcXmm&4J#WzD`bt>^KKDG38xf1E-)<-o}Qbjg0#1m?ou9?0dpr@b3x~)mr_; zdfw1h4$L&yu+Ys_Yx;z!Co(*2J*u@JE7e08WZVTTcM;F8$>X#`@N@j{LZ}h6iXBc1 z?aK}7%C>?LY=Ea{e<&}EwXcZtI`Z9MaV+!ToL-w^qsd8U>h{V}W?Z%rLg|INF)@Od2b*2R`Sy9MMr^_D6 L)zgKyi=Of~oQ&_! diff --git a/resources/src/main/res/drawable/app_dev_for_all_logo.png b/resources/src/main/res/drawable/app_dev_for_all_logo.png index 6341ea2e62cd3e43ab5ff6f5c0e78d4b9bcd4447..667231b2a061ba1dbf5600395feb7a4f5ff12afa 100644 GIT binary patch delta 2749 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 FV1maI18@KU diff --git a/resources/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/resources/src/main/res/mipmap-hdpi/ic_launcher_foreground.png index b36de1bd4548bbe65afb422c16f738a0f74a65cd..af50c0beb90e973ca4aaf2c16a872982369d90ce 100644 GIT binary patch delta 1529 zcmVg(EVJATW#* zC50n2jtLAO2n-zv3mgXu84x3c5+Q6BD2E*_iybbF_V)M>A%PJcWD+BV7$}1iA#J0p z(W$W0`1tu3D1;j;i5n?#A~K2^C~zP#ha4($8YXETFp42EiGLz8jw3UU|NsB|{rwdu ziL<%ct+dzv|Ni~|{rvv?`~3YHER65_?;|vjOed^nd*HEXgcM(MkUP|F5>!Av2I5GLI-ZlTBKsJxrb^IGL@q)*Hkduj{Xx z<(ik}myY6&IL$cy{{1e>E}-Y255Nz+?z}O}F>TsybJ}!G(oIs+Qu_Y-j+e%lpUX^J ztUF4b5F&;O9fO&m%M>PvnW4*efxG_y|9ghLwY%CMF@KMMioY8zj6zYM87qrKQ=ug_ zktR2iKTe+>FOHw)pBcm%v$@zo%|Y$@?Et+12EPV9%{}`3`!dTiFv&2nxYoJtxfH_` z?)&bi>8GRTqw@Un_5Af>*J2643C;4&+xFZ0{rjEfopRf9((}@F+jVHzXvOix>-y^h zz5{RBZ-2(_$UDtDWY=U`)moqBpy>JN%kRyqv(ps76pi4H0Js47`TLWb$x2zPDm#?; z`1|_%{f3aj#>?Z8n#fULuIA|SB{!HPHIY?5*=r@nC0*5B@cZzy>$5xi7Hj|j04H=(PE!E!`T6SJ_u%=G;F{T(i}C2~?Ofi)tpET5$ zQGXZ)U`%L=G<)y*8BLaukdCC20w(m{JA_`P7m+3iL3$4z1?;`|ioJ{Frx@VE8OEDE zFgniUz1qEbX3y@P_snNxWQZY#{L@&hwKO&+5=@8$Q!`Tn(S$%WHny}{irZPf=1gMJ z>9aI0*DK{>iWiql^SP9in7nZrUT9VF3V#5|Nh4*X=H_{)W{}dd^!FPsTj7OPu7guT zp`E8FJyUc+l$kDaIxiHSgY~QNLQD30*unhRdF&(Mhd>Wy+x?D*9s*Qo{u3FX{{^C8 z7)Dt*gpp?nGPW>?tY;1=g6bJ_m&lNQMUfT!hU)UFio^OfB106gcYe-qX^L>{dVeHS zYJq|XIZ(8L1D!6=EPD#iLVE&r6bCfuD1^0uTmhBrDe_|#^tu2d1i@^GW?`5;n-LCS zk-!cC6es}pMvNct{S2US)#+b)eCN1Z2OPlBHuF#uz* zet>%%8Z1V8P5K{0%b1}7bTKRS=@Tg3C_(WRwQI*&)cQ!4eP`+4Jq zcG*&?g%ZE~LaN03x|f7Xy>cymK$B2y&GAAz{LM@9ovw&Wd`LNvNAlrP@4+pxx&Kz& zL(R6UwA_L$8m(AdaHB|Ee68%}-TP(!TW}9GMi!fQ6E<1Tj9E5jHUuJ(U~Oi)XFL8e f%Me2hS={^rUREdT@Lk!^00000NkvXXu0mjfsznQq delta 2217 zcmV;a2v+xi45|^38Gi-<0033(vqt~`2ysb7K~#90?OS9iIWd(Mp z2WF!l9LGXcRe$FghTc!jNVGjgRqdQQa@;lg=Q`)?9eYF*WH6-|;?iHdR#=do_2VC< zLKJoI`4luBv46tt_JQX)+5wMO#`0xm?Ek}0@XDT(7#N;~pyMccKA#Moj>C&p4@0lx z;F7%f``H0luXOj$3^e7qX8RoB90MQD#>IrE_ddK~UDh9)N+nSg48i<=joNXb@jOgM zJ(UhZnye@|+1`VJp&3k0xxn)*l`MzTgI(3d_)Sf5sDIsGS9zpuJO7q(`c&CL*KEVD zB9rsE63iuQi%acs_ z*-x`6i3CIy1wn84WyNac^qJ1*07Nc99M6g-qn=e&fah3hTY{)~dhP$!X25h2>0>#b z4@qT25`RQM215pQFNkeXWQC@4fmCH#qHYh8G#>~%LEi*2)_bLCF(Wd1xq#3Jngbd^ zb3h|#4rl}|##!vYp+KP?br8gZW*5nSx1a>cstlGPRIm@M3du57Q5A3uhwmIgYQRMd z4t~%*h#QU@aL5iAIU{mRIY<>!X?gHjCj5bi#eYGN0wttA?+?F^BZEiKKG}{WAqmsI zX)M((#gKalC2=LFOs>QW$uB%mZ$2@vzo-NTIpv!|$<-3P+509XXGiJ|^la=wwkaE5 z{NM|`yy9iFPqbrS?>;3@}!`X+y zgMWuhcEO`~u-~#DC5a`dNv**nv5#Q=rS%x|j@{Qz6snjC792`#rFb&oNxYr)HY^t{ zXv}J)rII31o8wiz)b1Rx96*_^4E;as$E-Yyr>;E(>jo=i$k8c5I*v!vU=t?2lQ_Kg zFg~CB96ZD0iTEc%vl;CW;}5syoAZ$_q<^DjxCM=C8ey01XdGySj^mioRB8CkE z4H%QgaCG=6&P|`g)@55mGnt7q(V0qMgb-OfQ+MLy(T^#CMT-Qffq@LT2e3+9MWu-NJ0$^6ix3in3QAH3FSR#<+L+d~*YH!t2sS!Dm4n9a0 zku2f}lIcFU1|xOJ(t{9I(^Z4E!8WWjtwVGYWc)tTV$yKSc?*TsLVV@;3V$~pH)%N` zAfAt>DOqX|X(Q{hv(smB+i@EOmkX$)m>HXiY-4ty<4kneAT}^%=Py~0CHxY+mhl>u zlHFhK#`f#mkzvfhGTkyNDMSU!ZRNC__&vkYFg45vvkpRF1yprbp=e3beeFhpAH?{7 zh~-h6R*TBcN_@TXYwWe`#eeI!U&o=i%0r!s@-#t$*!Q2|PY8O^Z8<)#}b_dcX4gO7-f@RW*q0i`nX! zXN=z4W^HQ9xfFW8t9zGPXe|u1Q`Uanh%EbnUBs9dbF0hJ)rxUJZR@@wVz zZ1OYM95y`P`8=A|G{MZ7A;}W9-q?zcnGVQ`Ol=c+K2et#2$wL?8!%%7Fvohdql z=AmY^47K3c&@r5zI)6=Pssd{P96kpgkAED`E`Ju)Db@GIlH_@g3C(*%kp&x4m8#?_ zdiA^esiAf$F2ry#)RQJ5SAyWP3l+p)pm*~OO@m>MG$D;%_u@Mpv=1UU`2Y3VXQ9bi zH1EaOZ+PvqSg<(W&MjhS1kC}BpgEusv=|6d5xEFEK`hIBtABeEgYA%ZBYv7Un*@XOLW3s}gbZ%TY=SaS9bhfoRa7C@%$9Z9{On zJap@CaN~GUet!y<#~N_4qo3})CuJN9MWOVf#+gc|%dc%Gk rN`nRs8X`1k(4aws1`QfCVBYa>;G3WK+@o$>00000NkvXXu0mjf7l$_x diff --git a/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png b/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png index 49f6bbdaa2d04bc7d73ba352988d0b8574e16031..036ee78cd92b882392f06fac7f3b2dc87e8e4325 100644 GIT binary patch delta 1680 zcmV;B25R*xTzKE{qo`iPqWb8!e3%CyCnK z>{MWGetD?67hK9y5psedv;npR}2EIgP0|Ns2_ z{S_yPv$@%=wAcRs{{8>`{Qms={QXQ?r5Y@Z@cZy1G>}bOrTYB* z{QJ4>xj4-@ndO<&^wTcNE}-Y255Nx_#TzlpF>TsybJ}!G(oIs+QVJb|{{R0wN}ZXY z%M>Pv5F&<|q06na)smaZA2E-1fxApwtd5t)m!HdfhJU>qEsTJQzqPyC87qq>HFO8q(pBcm%v$@zo%|Y$@?Et+1t+dt#zXm%p;*Au;6z==(r|GAo=cD!f^?aQ`TI&)tLEtPB{!J(`1?gup;2J2pr_ANVW~t@p@WUV-s10DXsk$A zqcugEo8_B#+jjuF0MGN!=J@78&q3k$;VZ~18h^zaR@7Fz?z?-`#O}sx*=r@nC0*5B{Qdol;Ej&pj~2V+?EnA(D0EUzQvmq< z{QUg<`_tFY_S5|Q{QTD1{QTGS``XU!(EIKF`0)Go^waRt_6*+g761SPc1c7*R9M5! z*njs|(@+!#aBLA2P~3aZFV!}-wsdM+HU$CMTUoLcP!wD&2cV*Y7IA>M>H-A!-dl0+ zt^bKpkLP$wP1^JwkN1Z*>C5??Kt^o@{M&V$AF$ghMQ?M#XIjj;WGCO48a92l19=|(}?|(GFiO=iK=Q%pMy7gd}%nleb@?7!K!c$VU zI%>h01yO3XG^TKAanYEbY?Ar-(4rCa=dxTue!=1u(FOT}+$Hq>*voEgS7$yvs0?Sl zy{AQb?~52Um<8%G+-+)vN0%QxBBltKCma?a(9nF3X}`lGf`5i! z34#@n`5Cc6unz)p2=+oC48eW~)<7bPqD1yXlt#)!T$jsZrPV;5_vK~ z_$!ozIolBsh$>a5@O}pN2Y~?s1Am0MDzqsn%BYgpVNM!Ud%y)+hcBtex9xum=z>l3 zq-QrD58aKmwnIB`Oy7Fs(1ymRn*%ndG_`0AZTb(`WIHq)-@<{=kJ&ZK^@?D_XQd9= zUonddl$EJ7>2-inkrIY5eTQ!EVQ$dA92iv{VZso9IVibuI>NKKLG33pLx1MUg<-~k zU3HMp3pskCoeQ)B?=!;v)063Kr4b?Z>7Q~G$GA-PI_%aT2z>smCOqfWhM?f|jR)Q- zleql-KW<0`YJ*?wd9!s(a_5nH1T=!%olQR?N1=ELsAeGLF+SEXKO#&IZB=4cS}OHg z(TSkBch{JNp4`)jA@op7Lx1S5C)FDbCeCT(XJTvi(`M}4sr9>KPh$I(U*)Y$fmWqj zV#)s>y^d^oLH>X5{{L`&WO?Z~pTpA65)^51GFGxcU8T{nGOwNJ=!Z%S$Ah=&Mm0iA3XjQ>u}ya<+Bl?~>qLx_^Apd7si4!7{86 z#Hb}VaLdOzVFGJ`WH#Jy*ahk0_$5+loKO6fYd&#ttFD*bE>D=)*XHu5r^`5g|6X*N z)!V&~dv6|(->Z+)y^oa-}G_;+R~yPOgDKuBAb+$wIE}L$DD=ur)@oS!Z+m a@H>n!>A+`5;T-?~00{s|MNUMnLSTY2JapIq literal 2467 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 delta 1282 zcmV+d1^xQd35yDl8Gix*007uvZqNV#1ky=FK~#90)tG-s97hz#-^~8F-MjNHr{$=6 zYBXunD+Uu|)h1~|+A5Vo|A`?KEiu#z0fjUqS7<`DfeIxM=pO-#l=w%JAT$>G15Kn5 z^G6!npv2IMZPX;!iy9O4yxY6m+ufP6cm5Hql*ae><&rtLNGQp z9o|)**W3Nx_kR}-)>IWA{kYdq{{Vmt-PEtW{nfya?3s2LmIbC5ZPE%5e1e~WV3Q_?k!r;jSxVH%CgGy$WZ@=1yOw@9sNHA zk_eh*V2lx3a$Cv1rh=XZAz+x5LvS_u`hR}x{|N=E1AlaBm%wn)tfe50Y7AnMA@%f= zXCR0smLxg$NO)vHU2w9_5_?JVNz%Hmf^cG0&IQhO`aA8ModDqxKOdiO^fb13T9Vcj zdp6pD>JdYtB-$|Aa3gplSIoV-{c4$`tan@QCfla#3)c^i9+otT0--?HLl6Oi0Jlwk zTI;T@aDP-Z=QWF1oQ=;~kmY#(@f{gE_Ph7DO|~Jxgr;8*p~T8qCgq6>Q(t4iL!qIX z%o+s9#knxTJDocxViOT9(mm5X9{W>dctS~fV~7C34D?L*9Lqn}Ki5CFFz6fixmefj z@a?nyvy1BD;QSzmIA4LU=kd2Ea(o2REC>Mt0)PJ4gVuu6f60CU3kTc>#-rm1kR97g zU8RyH)jeNVBov*xcM@nSC(ZaNLl7mgD^L0g#X>jlF3l({%_!}k?{CU&0)U~=kV2Hp zvzH|z0ZJvR4JKF6XT8O`rTt#}MsZ`lkRQ@Q7G#l$JQJT$sX8W&U7NplKYG7Dw{EFL zT7Ly$z(j-U%<9q5*ata{^}p3inuHO)8NBJc>l3grFh6jq_)^Eij%rsmC4hlRvmk^c znp`Yy6|f}`547a9cx~RYjIu+chdN*Cyd&N5+P!|+&j3?m+ZKt%F~Vt&A&f8ua%S_H zBV$KYs`fqU>)F~EL3s^YsJzwr94@3i1 z!&SEyZ|xh}=a2X+9hF_)E|C!t;MCGtin)wXiV^O zo>LTr5(6iTjS_pw@`7X)Rne;IAqFwa3ba)gLu*hmqJ$1uI}8gVgc72!$-l@*T7L;i zCEFX!Wk`P=`;X3|$fXe4#stZ6<;E|5i-plNsu99-iGr0vET#$qn<(*7R8}YftH{Z5 zH6|UMR}qSp54Rk9nS9Sth*~ z4QJ{_Nx%glIOKqhfnbdOtQy zKMH?z`lrF608tfDwxKA sr7@Tc3?>7E$-rPTFqjPdtOYFRZvjsQ%Kb*h#Q*>R07*qoM6N<$f*Iy%y8r+H diff --git a/resources/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/resources/src/main/res/mipmap-mdpi/ic_launcher_foreground.png index ca37eac5e9c7d9fae7397e9dacefba6075a7ffe8..eb4870ad1df4674151de07017f6a20fe34333f59 100644 GIT binary patch delta 1313 zcmV++1>XAi3aJW^8Gi!+003az3AF$K0^CqcR7FQ{Og>DTB{q;FG>_};^dK>gBsGvc zOPM-Lmpe&T(B=L9{{H{{lH-!Pzuf=*|Ns2|F2^ol)?ohr{{8*`yTIM`_WJku`WPyU^!557 zGLJw|o*ONU!+*!&%g^O0JDknX=KTEr_4oNcP@MDj`2G3+isOmU^U$;Gv-bV=TGd)~ z+jL*nUoXcmUe;bY%sEQXN@dt(ZQ5;3(M@L8X8Ha3jpB_yO`rGo`kSQHn4#3~@b_J5 zt?%*o^Yr-+AcYPfh8HP{9WRX>E{$MpuX=^Ny1?A9wtv^@>-0WNoOD2 z#mVBv%i_ez;j*~d$<5^T_xk?(|36Ti{`&v){Pgzx_T2Z{(Dct~*l8Wa9aPg)!SBJ* z^U?SH_kVWWci#Bjl;o6c+iYgoX0hwB((}@jk)Y}stn_R=oMF8}-gJIp(t=ALidZED$S#qh;T(MvALE|24n&hpNU;f;~w zk+tl#X4++q%JIs!?Y4j3 ze^}I5Eyyh>#wgPC(x>RB$??f0#wEM%yZHV1Y1wH`(oSC0Ucc|ZO{rmg%_4WM(k@EwOi2M7A`Stbt`TGR>`}+ftkBEu&`}_Oz{rma(`~Ca<_4UtW z348zm0m?~4K~zY`V_+BsgAwovi%GCDG08~FGBL4=i1Kq2(jbt-l3!NnRa#zBT$Go~ zlFdU%gP^#Pk+-{pL#0E7dx4RWP$nS_yni09ZbmTR=9W&DGd%e&Y5WmnTmjrky*qV(xTbB7$|>Df_!`9;BZ? zxN;s5!OA$zZn4YBqL?=FylQ$U| zh@>p)Z60h61HtCC1T`2Kp@JGh8q_D+Or2z76W||U6ExXBXo4;w4SJg0oozjS^$pE_ zz3p9IIxU1WCkG5$Gd6zL!6AgXgbUnUf|t(&VPC5oO{l_=N=%BJo3mR zk0%+#4zr~ABwyKE9f8`0Af;R$8;?^(q3JnFP#Y-0t>FonjES*Sis7jem2psG=;p6S zrtW^(Zl)rJ9UWvl%)YWW_IK=S3$1^(n#r<6-`~}P?(eU|?RG$xDW0oefGgKWq3b3R z2?MXKTZ}^=y?+A3H05tkt$FW*-Js*QKY2H2E3o?IBdN1LybYOB;fPo~g`a*2gL5wC zq6hbZptt!4@|sln?^z5wBc$_8-Y9rLagdJb#>%bi(A!fcs!6CrJ<2X*RmD z28#bVS}vA175J_Af{LL$;!Ufe%EZw=tH-p(j_b>=E2jY;@qAC`KS z&W&g2=A8j_*5ijq4x=mF1&`tZ1A~J&5YZ!Ot!PDCMcW)**1ijyNXI%uf+et}e+$k} zokzJ^j(^k7pGIS0BTg+lg+&F6aBlJ(w)SsD%!~K(=Yau?Gq@!uhLb}${^#eQptuyhgmuO1 zM4bB-KPaJC?OTl@Z3uBQj+3J&F{s^yDmesyPJeU2@;;S-?!UURqjCp&CwkF0*@w_z z2!7d*D>GMcFnkcx#x(jOeNc#kP*n)sU=wOwHQ0T7m$e;bWiPV73lSi}H2iq42LUyJQb#Eoyba>% zjeqoFTlqFYW`$OYrSSFG*NCy0;N!VAKYyf8>XK*KR2QB5B<|O3Q04r&k2{acsqdz);cgkAP@>`1t7b|%$OkF_eR!D@^ z*~nR(s6T9%Ss6(T-5u5urqwCo& zT#H>pou>}rWEc`8EG<}uxTXvKoEnfZ!aA4^)E>b0f$bt(dhYe0dr3E{TveE1GuSh{ zN0bzoqnFY7OsAmF)`?8*3P{(PRD{XVx}#W8xB?@I5v;wu78j>4qUll-!tpRx6@RY6 zvASbWB~{R8s~(cGB1If(TzJ*0cY+l`QOr%Hro>@Z@2Q_tI762$$k`HsZ?ra0jKSdvqznerWO%x!1d2>?{l++CnHUa- zV*enu!>mPbeC#bOk~ue7)0zN+tt*A+tyQS1D8d(?Za{UVU%VRzM|*G$_Gs^V5vFM{ zueV6nrf(jyYkpV4I7gcO|Nl5h zn>a?BPFtobI+H3ol;q~}S!JtRXszez^DaJ?BQ%gEIFvR-ntxwwttB;(I7pfnD2N^~ zj$Ui9JWHG&FOEG-oJ3NgCpncQHIN!CivR!r{{H_$QK0zv`}X(y{r&#_|Ni{^{rvv? z{r>&^{{GC+<^KQulH-!Pzuf=*|Ns2|F2^ol)?ohr{{8*`yTIM`_WJku`WPyUAu^Bj z_4>=tuL8-Fc~!^hz%JDknX=KTEr_4oNcP@MDj`2G3+isOmU^U$;Gv-bV=TGd)~ z+jL*nUoXcmUe;bY%sEQXN@dt(ZQ5;3(M@L8X8Ha3jpB_yO`rGo`kSQHn4#3~@b_J5 zt?%*o^Yr-+AcYPfh8HP{9WRX>E{$MpuX=^Ny1?A9wtv??O`hrN^g~jhKu@0^F^)u1 zq4xIr87qq*F^)b^n-nIAA~cabOr1hdpBgNS7%GaOr_aaB;>OD2#L3~ZxY;yAm}GFV zBQ%g2ER4m;;>OG3$<5^T_xk?(|36Ti{`&v){Pgzx_T2Z{(Dct~*l8Wa9aPg)!SBJ* z^U?SH_kVWWci#Bjl;o6c+iYgoX0hwB((}@jk)Y}stn_R=oMF8}-gJIp(t=ALidZED$S#qh;T(MvALE|24n&hpNU;f;~w zk+tl#X4++q%JIs!?Y4j3 ze^}I5Eyyh>#wgPC(x>RB$??f0#wEM%yZHV1Y1wH`(oSC0Ucc|Z`uhIl(D~2i&HerQ`px|P{Qdp?{p8Tk=KcNp{muOR{{7AV{rUR({r(fq zZH52<0v}04K~zY`V_?7pMsUDD1TaDaq^TyP$H<5z9Y%Nnsp_%mFft-dhmjF!I^e?m zq9PKkOia>JGE7XYV#0jf*mWR81#(#O%YO>JO3O=%i}G?=vU#xDfg~s>Ze--+?%+`A zP~l!+WF(Y{)m2DhydJJ@aE_a678f=R$YP9UM#jdL5MXRu5-2rvao1C6JYePi+_%b zNjj4fy>@}iEIUsVj3kAwAA4|A*g{peCej6aYjb>jf|2v&Yv^W zkB|lqr>%!C-gx=pRmN54y{jV}LkVf%+`Q+~%@?0OKmYhB_VkwJPBRE<*m>;M`?o;% z7#U?;-o1vf4IEC#uctnHm!9zYaerFsjtIwQ!k*rK;`SYvCr=-yojbH*?sPvQf_2*| z`@3%*q@O>yavl-E$~et#vCGM%dnp(8$0W?g7ID~;hJF0m!~@Ru7DhxTK8wipt1O64 ze1wuW$r*^!Eb3z(Yz_m#<_*|1py<#;b922Bfr3eQvhCC?PS^xk507*qoM6N<$ Eg3a?5&Hw-a delta 1564 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 N002ovPDHLkV1kP+=E48~ literal 3073 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 aJ-!xkvzfkEec*kx2bdXQ4a+dMi2npVx7MHl diff --git a/resources/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/resources/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png index 83e86560f030b8e6e1f3cfa12faea41a407608b0..74ccddd0e78e375dcb29fba36c2e82dac6d7524e 100644 GIT binary patch delta 2024 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 diff --git a/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png index e1821982d198e57d54426f8efa682619c0e87025..5de2e8d84e215d7024fe31496ebd1ed5131c84d0 100644 GIT binary patch delta 2216 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 diff --git a/resources/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/resources/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png index 5f7a54e2cd14ae842279cbae1a30833c7f2e59b4..458587f3d7ec9b9118402da8fd573b034d2bdf6e 100644 GIT binary patch literal 2817 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 T#MX`-00000NkvXXu0mjfbtH7I literal 5522 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 NS9zn{p!hE0e*jN&f$snS diff --git a/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index c28efe8f9f970ebe8f21111880f881bc8e68003d..3a1c19d637256874dd16393a8f230d708eca2dca 100644 GIT binary patch literal 3106 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 literal 6117 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 diff --git a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png index e4864e55e3ce1c4fd90ea447f318eb40d4f1ffde..53dd67f675c5a80ece468c1c0acbef3af853f94d 100644 GIT binary patch literal 3664 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 NLp2Q4D^=}+{|CTT3K{?a literal 7438 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 diff --git a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png index 04393d500738ae5b08063266a0c558eb88c124a9..8a0f94a3909af6e191f3387ac8bc54882f6b9664 100644 GIT binary patch literal 3680 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 diff --git a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index 6ca1ccca11377beea762115c72f88627e3d532ef..0e056db413117da6390b757b154e55ce03a7bb8c 100644 GIT binary patch literal 4020 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 ~3 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 P00000NkvXXu0mjfP=x?Y diff --git a/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 61da551c5594a1f9d26193983d2cd69189014603..5a5093e3e40707e751fdde9bd8553c296122a0ee 100644 GIT binary patch literal 3035 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 literal 5339 zcmV<16eR13P)Id|UZ0P}EI-1@)I=X~DGdw1?T_xsK{_uTvL8wG`@xdHSL zi(gOK!kzzrvteWHAo2y%6u%c~FYnJ<{N`T=3@w2g$1Fm|W?3HbvT3QGvT;S=yZYsV z;Ux5#j?uZ!)cIU&lDjT_%=}{Tn4nc%?;kSe8vq_&%eGAXoY=)gfJHN3HRxZ>B(Z_MschsoM6AUCjPu&A03`pU`P@H& z-Hldo)2LhkOv(g+79zsWLK6F$uY^-8!$ow=uuO2jh2SxRvH;PPs;xr%>aSRNI!<*k zq54?efxFGi!}O%x@0qhGX;;FAnHp6DCoZk~0VY&zmNZ7(K!PJ_APP1drc`bP>0_;h z&Qm$bcWJm(}i`WLgp2 zB!Saf;inDgfjrc$$+TEt@mPcR1IsBF%ve$XBbby0fpkyuOahYhptv_F4TPl^cFuY% z?j|wKCAHsATwcEiKD!!=-Rcj*rL{kREWvXSay1%O)$IkoG9;U>9D$AX2iq+}=c!zK zW#~F|y=6S-m(=bSuBh7sp;w||;ji02=~j1>n56y%KZ-d`CU}*Vr4Kbx#$l%nQktf zay7|dPxqqVP#g?4KFBTpC4g94a7d(I?Axdoz50FWHg^b+VQIjj*168V!-BZvwln~A zbKH-RtH}*WGN*#QmN8LoJ=px$01}Vc?i>8J3A9hHnIyNX`EfxD=_YXVIKs{VT3Ndn zW>tOBQlZBH$fP_7=2U+P&b2>w91zzwom{tMxdOJt%p6O<(sru*9vm-yM{=LrGg*A; zdzO^ZUi!GSIH4T8kpm@-mto`OgS_RuFCT{W^#^#*lhAo8$9JBR$l9jsaNtH3yDncj z9=-2VI~SII2{y5Q#*d6e5)(5m5qxJ>5ez6o)AC@Dmht5wuo5#@bKJK+ClNCgSImHK z-n$L4f1hQ)kyUO%%{MT;DuTBj5;{-iWSt||N^Q6Z*Y7p3>zTDvk2$AzYh73y(Ykaq z-S$a`7~Y)6@=WksXsXwxd#=vLpuN{KnDUhFcejffqj+47gj>yxu;Skx*L=&ijF8^lE3`V9ohnj~S&~kFu#to{@S-dohp8hv1H|3H&ftNS7f~Utf0s z-0Ba3@0BRndhI0axt07RCPdAk(OH`c?f>Mvkw)i#6?2gwcRS#Z7G zd>2F_5wA3$3sv9!1Cnl?gV3unFu8II%&++xD(_x{jN2uw{;mRg;AZ(A*EBq*^_OPS zqW3b$^)#DVy#pT1?REno`cCElZvG#G)QHy99*{=~0lSF3y@HHeTsgFs+5^r|WbX5XGTV4F1VJhg!y=hf7Reuqp}5 zpjo-u)jNf=s&|4cp{$jH>RjCOm6?Yz;^2*JxF>3UtZ*dKh{2k!N7v=kX)dSt9Dcop zb81lcyzm@k@zO&sTre7HI`lsiOGC;R*6af7$}J)ahO)%EGMpu4HrV~jI&WLG9e&21 zsJmTC9+#u*QYRowFVdIvCjDi%>vNHH^;Vcw_<5!BNaa2c12vZv4G*(@+qhJ4jaHo2}dFnxWlf-cFM)5Co`@Hf~jXV|1r?XR4QTQ0IB`3a47oVt z|6g6V5B_<=meX43`m1qB(K;T<3&^(kvxbr0HY3{r`e4_B5m;#>1JsFb9^)44eq||r zPuL7M8yn#EKX0t_p#Y8CWhr{I@fJ*t_J%S09bnu6C)j^6u}gryx)1{z z$5(=Sv@^^~4S~O!WMB72Qv<9l`<`YFI~IeALT?Y=U_MF;khm8cvUXB`qZ0oP2Wc83 z#osChA)h-mVaA)Z1=J9Z_Mv4EQKU`0Hs=d~uWLHHTj8F9fi!(vsQuh;Y9yGaXi_p3%9HylQ<{^u|E!Jpr zY4t0U3I+e|NG9!Y>09{qPVF-dsPK9j%*YIZDH(y_R=OYc-^rUv&#w9c?Be_n6N?s8 z9^Am}C9TAD-W?gNlC}N*&tK0ppev0xU{3z$pqt_X^K-X=L7_MAVAb%vKN#(G4ki|| z2CFZAwC7VR2B_UZ-$Otf>JRYdBF~DDeyfUhfnJI$1Eib25%kY`Kj__9fTqtCfnZSN z3+h2LXA+B+vx;J0>)HR4aYLq;ZoMM!gxQvBC!T3I5(z4a1ie%O6wUzYWD+DFsT?SP zO_=Fqx?LS;{=o=h(dLy0j@WC~g~8Fxg5;QT4XloWxSBkOtLCIeEb%q@kX~C136}~W z{!;!!sV!(Bsr5yWTz3}Y>+pMBAtcndmE_Askap!)NVt3&60XRQ-_JnO?`I+V+IdLC z&xu#1<7WJTkCaZW%6ugjd1<_`8UKkBlY z0Le3HPfsN^POO44|8)?{0Y@fde{uqwC=bv&v>e7pE@q z8(`eg?mj^_Z1R%;MZ&a)J+NoLmJOajThV#;*a*1Wppyfh8O(*koU0dg@3+iTmx-3%pq!1D#A~P}?85fI(%ICB387Z+3225a;)w{qpIRI>qdBW1z zFqn4S2W*aeflag*Oo{OpORNt}IpG6SPx^vWVi?R%2m#ypO<Q@c_!eeohr+BJl-$n%^@rJc zVJrtCu`dV*&tLa~{pqb>e+K0&?Y9Z-i?)H~Pa86@&HYs@Enk**Wmz8;Un@HUbREg- z1@g`)8lLw9tyAk@>Tz$-j&g3}R?-3alM`NG7VFx^t)v68d7=kcC;PQ=D@iaWF-&oT zIoY3qPO3`_w|WqasawzTfQ4rwKtIO=-3r|-&;7n`p(ki!T?3by%%?VMEYXl}}eR0u~8-*>a7egC@(77 z0ebnKpj+S})JAty@v{!0HV(4Wd!;iAU3(}SjHJgO!_=c!#v7LSv(=#;ee_JLNvT1y zx^k;{AC~8|mjp6EsR6ujDCRIgc?gIH4#gY;w46o7Xh8+u&ARAjs=MYV(Zd|>5l<)I zq!ydq8;WngK2|GjL#6ng2SIa3pUo2_YEbJuhcaZ!bJ|M+3DA@@K^wP{&U1`1Ji$Jn z0J+J8Lovr7-wPaycQhMdw>~yi0A+MG*48?Xw#eSAWmkVP<>noS@arM=%bUAyX2#;LLWhoZSwe7Dd3P#rU~6 zqIuD8I~kmb8|JQ~HVif#{YH1fk!(F*8$FmR9;Ul?nv-6Z`z>y~#uj9EWSuk(aOv(_ zC;72FM|Kh@4$2eKFze0?lxaBoWI4n7 zst!_O^F5Dg>)A*91N!HK_XgOEvq9IWqHJ6I-g`jDUdcqLQ*%Qw&++2TkjbScru)Lw ztRP-E6myJoykY(s9EfsBAmuqag`OgEwJ`@5SG{TRkuB*wP^|l7e+#rlT(7;8E-aa$zBqnCzNuow4YP46D)HB_>({al(7k>W(V`ap_pTmi-6FrbZPj2 z88Rh-TKHSlukBAMzM`m2y7tw3yq41@CcU9CjNT?5i1N{h&C`OkQeFP0?wq|hUnXc? zTqECW;WlOAY<92p@IexgCuZV676I|WAuBP?^S(d-?6zjTLNCzCaRc>Z&VQ?TTWv<& z=w;r4oUTv&Ut@YGXbkApYlt!}dK{r-q%vvrUWXX!HRzc*`{#wqP@y5u%w&sYz~Yxm zWac@OGI5lj6Cx81rX3=h&oL?Rg#|_1(N)*MhhNNzRZ<^HFYu1&rQEAO>G(9@NN+Fp z`CuUV_F$TGd)LWu(YS+4(mpNPE;7FuBzC=uKoNVag0Q4#2BgKdwz1Fjw1=bRbtuz;rX1c3LE7MhE zk>xL(o*OD8C}=S>MarOPAw;#K&R0K-m=)Q7nkG$G(2|v5z2ENr&a+@OeA^33Ix2lR zwf~Hn)lLp7ENta?tmUvR#BG(^XESLpd z4eagIqL$Z>+GQU%++~u_tHb-5aTYVIm$GtyB^4z~{+^5f5_*9Ky1hSQ7WFPIKcaxy z=iRrAK6D)Kq!YFv%y|FGsF^4IbEc;RmRV)`Uzwa6c*D9N_!fy(j^M_GIFBpi53en= z*uO5v;_H=B8h$gwROT5uQ5~GMP@RLxYL!Q_LG|Pfr5(4%amYp?ni6?hSP#J z>irZI7001yQKOYK-kbQA?r=*I`b@|0oFR%gg(hbi&wNAMhDh9rN!*QC-;)_kFFo6Y0000ONi0?W zs!ZICP~MM7-HZVLfL7O#EQME5-H%Y*jy}hGMbd>f%z8D(cYjdbjXv6hMB0f}-jYny zh%l32Mcjx&)Pz;tkU`3RJK2I%&523ci#@`4N4|Uj000iNOy7@4#(ohqB0|xEFuHC= z&4WbPh7vU-6MsA=6Fnw1s%j>2OGwj)JiT{0(tcCYj5NV>Grn?1*M?Hhh&|ea{r&wy z+lN%$kyPK4N8E~6-ji40l|w+lWuyjy>ChKiY*)-jHnKpblYH-c;U_H^g>L$Amu2eKE0W@A~xN`0LQ`-b}@U&+FVW zuW35aeSbUEfHA9P=k@LQ{Q5JuZ8W-XJ+^Z{zIQ>ncRIs&NV$4V!h%l8hE2i9f@7Gp%Vr)q+&okUz(KKFNDf-;h4qgU{;QNxgmH^66p4jz+LT znmWgMQPYg5?Z#2oj6v3fI?;XH_vl2da*gS?E~aC~^V(9~kdE81pVYUV*}3ES?SjXl zhta8Gyo%rO=0)0wSjCA^!-Ph}eowf5MZ0k z?(y-K{VnSy@a!u@L$V^9s`>H#{r&xQ@P7%=000C4Nkl}eFeRnpM;1shu`osSgc1%X z_4H)y$<8`^>1<8*o|NA`g#D2F^WN8abC<__m^<&+y$893HDuk1L4Z41*nd4OSs|5~ z8DE1aT!#7*^}oJVq)gK!l_|T4^Y+q!!IU4HB30LZyAvK z)Q90|s2^s40;ufeGPskL?SBV3@5{z-Xnuf&(xvhkY@q!6-#FAzf;nkg40iCSbXOl% zKm6OfckijadlxP|HEH3(g_Dj=nlyd-vHA1oKPv4i>BFX>ch3F$cb6_*y6x!EIaBIZ zt!b!VS>0A$onN`2t4f)P8%t{4etzlQZQHgj+I;l<`4yAj*H5k9w10lnrnPHl?Y!2y z7;ApmapnKv&6}@$e7A1hz6+<@+Sac>uwd4P4JWr0M|B%uO?*q%ZCiA4&Y?qfD;g$G zoLXJIskE|vX4S@Bt;(H>a#&OAj)seKrmR}EqM`oG#A=|11wi%JHl8a{=EtESar@LM zb!#RA{R`AklwUausDHkxcy=fMVq6+BmQ({Z)UTYndLmH$+F3yLtso7z_2h7sd^<`{ zU)a}n@XhN|podRxEH3W;eEquPO=M5opq8z-N++J#wCP~!+ODphTXvnBJ-fTVzkjm-O}8i^;(1I1@j6 z2VPz!GhQG76JeUXP8P<-t` literal 2636 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 Hu0mjffZn4d literal 7472 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 THH{$s836j)###*;jxqlO6Wh~* literal 7909 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 diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index b216f2d313cc673d8b8c4da591c174ebed52795c..5e9215a4e3ae1b7a4c099d1fb096d9b61fd53617 100644 GIT binary patch literal 5671 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 N)s(apOXOb%{U5Y_{n-Ej literal 11873 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 literal 16570 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 From 507a9f0bb4c06eb93047a315dc76d29ea920c350 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Wed, 5 Aug 2026 23:53:46 -0700 Subject: [PATCH 09/11] Optimize PNGs inside kotlin-compiler-carrier.apk with pngquant The carrier APK's res/ entries (androidx.core notification-background nine-patches, pulled in transitively) are dead weight: this APK is never installed or its resources loaded, only ever opened as a raw dex source via DexClassLoader (ADFA-5010). No source in this repo produces them -- they come from a third-party AAR -- so the only way to shrink them is a build-time post-process of the assembled APK. copyKotlinCompilerCarrierToAssets now rewrites the carrier APK's res/*.png entries with pngquant after copying, using this file's existing raw-zip helpers (readRawZipEntries/RawZipWriter) to touch only the PNG bytes without recompressing the ~63MB of dex payload. Verified byte-identical dex entries before/after and a full :app:assembleV8Debug build. Skips quietly if pngquant isn't on PATH, since this runs unconditionally via preBuild. Some of these PNGs are AAPT2-compiled nine-patches (npTc/npOl chunks) that pngquant's re-encode strips -- inconsequential here since nothing ever renders them, but would matter for a real resource. --- app/build.gradle.kts | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58d15e377e..d9bced77ec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -440,9 +440,126 @@ tasks.register("copyKotlinCompilerCarrierToAssets") { outputs.file(destFile) doLast { sourceFile.asFile.copyTo(destFile.asFile, overwrite = true) + optimizeCarrierApkPngs(destFile.asFile) } } +// The carrier APK's res/ entries (pulled in transitively, e.g. androidx.core's notification +// backgrounds) are dead weight -- this APK is never installed or its resources loaded; it's only +// ever opened as a raw dex source via DexClassLoader (ADFA-5010). Shrinks them with pngquant +// (a lossy, palette-based recompression) purely for size; correctness of the images themselves +// is moot since nothing ever renders them. Some are AAPT2-compiled nine-patches (npTc/npOl +// chunks) that pngquant's re-encode will strip -- fine here, would not be elsewhere. +// Skips quietly if pngquant isn't installed, since this runs unconditionally via preBuild. +fun optimizeCarrierApkPngs(apkFile: File) { + if (!isPngquantAvailable()) { + project.logger.info("pngquant not found on PATH; skipping carrier APK PNG optimization") + return + } + + val rawEntries = readRawZipEntries(apkFile) + val sizeByName = + ZipFile(apkFile).use { zip -> + zip + .entries() + .asSequence() + .filter { !it.isDirectory } + .associate { it.name to it.size } + } + + val pngNames = rawEntries.keys.filter { it.startsWith("res/") && it.endsWith(".png", ignoreCase = true) } + if (pngNames.isEmpty()) { + return + } + + // Under the project tree, not the system temp dir: pngquant is snap-confined here and + // cannot read files under /tmp. + val tempDir = + layout.buildDirectory + .dir("tmp/carrierPngquant") + .get() + .asFile + tempDir.deleteRecursively() + tempDir.mkdirs() + var savedBytes = 0L + var shrunkCount = 0 + try { + val quantized = mutableMapOf() + ZipFile(apkFile).use { zip -> + for (name in pngNames) { + val original = zip.getInputStream(zip.getEntry(name)).use { it.readBytes() } + val workFile = File(tempDir, name.substringAfterLast('/')) + workFile.writeBytes(original) + @Suppress("DEPRECATION") + project.exec { + commandLine( + "pngquant", + "--force", + "--ext", + ".png", + "--skip-if-larger", + "--quality=65-100", + workFile.absolutePath, + ) + isIgnoreExitValue = true + } + if (workFile.exists() && workFile.length() in 1 until original.size.toLong()) { + val newBytes = workFile.readBytes() + quantized[name] = newBytes + savedBytes += original.size - newBytes.size + shrunkCount++ + } + } + } + + if (quantized.isEmpty()) { + project.logger.info("pngquant found nothing to shrink in ${apkFile.name}'s res/ entries") + return + } + + val tempZip = File(apkFile.parentFile, "${apkFile.name}.pngquant.tmp") + RawZipWriter(tempZip).use { writer -> + for ((name, raw) in rawEntries) { + val newBytes = quantized[name] + if (newBytes != null) { + val crc = CRC32().apply { update(newBytes) }.value + writer.addEntry( + name, + ZipEntry.STORED, + crc, + newBytes.size.toLong(), + newBytes.size.toLong(), + ByteArrayInputStream(newBytes), + ) + } else { + val size = sizeByName.getValue(name) + writer.addEntry( + name, + raw.method, + raw.crc, + size, + raw.data.size.toLong(), + ByteArrayInputStream(raw.data), + ) + } + } + } + Files.move(tempZip.toPath(), apkFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + project.logger.lifecycle( + "pngquant shrank $shrunkCount/${pngNames.size} PNG(s) in ${apkFile.name}'s res/ (saved ${savedBytes / 1024}KB)", + ) + } finally { + tempDir.deleteRecursively() + } +} + +fun isPngquantAvailable(): Boolean = + try { + ProcessBuilder("pngquant", "--version").start().waitFor() == 0 + } catch (_: Exception) { + false + } + tasks.named("preBuild") { dependsOn("copyKotlinCompilerCarrierToAssets") } From 687326ad2fe30066bd6c8af6149f7f9b9602b776 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Sat, 8 Aug 2026 13:39:02 -0700 Subject: [PATCH 10/11] ADFA-5010: Stop bundling kotlin-stdlib, coroutines, and Sentry into the carrier dex kotlin-compiler-impl declared kotlin-stdlib, kotlin-coroutines-core, kotlin-coroutines-android, and sentry-android-core as `implementation`, directly contradicting this file's own doc comment: every one of these is already resident (loaded by the parent/main-app classloader by the time the carrier's DexClassLoader runs), so `implementation` here just duplicates them into kotlin-compiler-carrier.apk's dex on top of the identical resident copies -- including the entire Sentry Android SDK. Changed all four to compileOnly, matching the pattern already used for every other resident dependency in this file. Verified via a clean rebuild in a worktree combining this branch with ADFA-5053/5052: the carrier's dex dropped from 66.02 MB to 63.41 MB (-2.61 MB) and its entry count from 144 to 102, with no change to the main app's own dex. --- lsp/kotlin-compiler-impl/build.gradle.kts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lsp/kotlin-compiler-impl/build.gradle.kts b/lsp/kotlin-compiler-impl/build.gradle.kts index 20e5299b9e..dec761cdf3 100644 --- a/lsp/kotlin-compiler-impl/build.gradle.kts +++ b/lsp/kotlin-compiler-impl/build.gradle.kts @@ -36,8 +36,8 @@ dependencies { compileOnly(projects.resources) compileOnly(projects.idetooltips) - implementation(libs.common.kotlin) - implementation(libs.common.kotlin.coroutines.core) - implementation(libs.common.kotlin.coroutines.android) - implementation(libs.sentry.android.core) + compileOnly(libs.common.kotlin) + compileOnly(libs.common.kotlin.coroutines.core) + compileOnly(libs.common.kotlin.coroutines.android) + compileOnly(libs.sentry.android.core) } From 966b661e54ec20b60267fe062347f90e14907f9f Mon Sep 17 00:00:00 2001 From: David Schachter Date: Sat, 8 Aug 2026 14:14:20 -0700 Subject: [PATCH 11/11] ADFA-5010: Fix flaky copyKotlinCompilerCarrierToAssets CI failures PR #1635's "Build Universal APK" check failed with: Property '$1' specifies file '.../kotlin-compiler-carrier-v8-release-unsigned.apk' which doesn't exist. Same defect as ADFA-5053's copyJavaCompilerCarrierToAssets (already fixed there): the task only had dependsOn(":...:assembleV8Release") -- task-ordering, not a real value-based dependency -- wired to a hardcoded path guessing the AGP-produced filename ("-unsigned" suffix included). That intermittently races the file's own write-to-disk. Fixed the same way: expose the release variant's real APK output directory via AGP's variant artifacts API (variant.artifacts.get(SingleArtifact.APK)) instead of a hardcoded path. Verified with 4 consecutive clean (--no-build-cache --rerun-tasks) runs of copyKotlinCompilerCarrierToAssets. --- app/build.gradle.kts | 33 ++++++++++++------- .../kotlin-compiler-carrier/build.gradle.kts | 13 ++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9bced77ec..a5b3986b44 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -425,21 +425,32 @@ tasks.register("downloadDocDb") { // doesn't need the root assets/ + assets-.zip pipeline -- it's a few MB, not hundreds, and // belongs in the base APK unconditionally rather than an optional first-run download. tasks.register("copyKotlinCompilerCarrierToAssets") { - // Deliberately avoids project(":subprojects:kotlin-compiler-carrier").layout... here: eagerly - // cross-referencing that (com.android.application) project's layout from within app's own - // configuration trips a "classloader scope must be locked" Gradle failure under - // org.gradle.configureondemand=true. A plain rootDir-relative path sidesteps it; only the - // (lazy, string-based) dependsOn below needs to resolve the other project's task. + // The source directory below is AGP's own Provider for the release variant's real + // APK output (see releaseApkOutputDir in kotlin-compiler-carrier's build.gradle.kts) -- + // wiring inputs.dir() to it, rather than a hardcoded path guessing the output filename + // ("-unsigned", versioned, etc.), ties Gradle's dependency tracking to the actual producing + // task (packageV8Release) instead of just dependsOn ordering, which intermittently raced the + // file's own write-to-disk on some CI runs (same defect as ADFA-5053's + // copyJavaCompilerCarrierToAssets, fixed there the same way). See + // evaluationDependsOn(":subprojects:kotlin-compiler-carrier") above for why reading its + // extraProperty here, rather than project(...).layout... directly, avoids the "classloader + // scope must be locked" failure under org.gradle.configureondemand=true. dependsOn(":subprojects:kotlin-compiler-carrier:assembleV8Release") - val sourceFile = - rootProject.layout.projectDirectory - .dir("subprojects/kotlin-compiler-carrier/build/outputs/apk/v8/release") - .file("kotlin-compiler-carrier-v8-release-unsigned.apk") + @Suppress("UNCHECKED_CAST") + val sourceDir = + project(":subprojects:kotlin-compiler-carrier").extensions.extraProperties["releaseApkOutputDir"] + as Provider val destFile = layout.projectDirectory.file("src/main/assets/data/common/kotlin-compiler-carrier.apk") - inputs.file(sourceFile) + inputs.dir(sourceDir) outputs.file(destFile) doLast { - sourceFile.asFile.copyTo(destFile.asFile, overwrite = true) + val apkFile = + sourceDir + .get() + .asFileTree + .matching { include("*.apk") } + .singleFile + apkFile.copyTo(destFile.asFile, overwrite = true) optimizeCarrierApkPngs(destFile.asFile) } } diff --git a/subprojects/kotlin-compiler-carrier/build.gradle.kts b/subprojects/kotlin-compiler-carrier/build.gradle.kts index fd4e35201f..eacfb613c5 100644 --- a/subprojects/kotlin-compiler-carrier/build.gradle.kts +++ b/subprojects/kotlin-compiler-carrier/build.gradle.kts @@ -1,3 +1,4 @@ +import com.android.build.api.artifact.SingleArtifact import com.itsaky.androidide.build.config.BuildConfig plugins { @@ -23,3 +24,15 @@ android { dependencies { implementation(projects.lsp.kotlinCompilerImpl) } + +// Exposes the release variant's real APK output directory as a Provider, for app's +// copyKotlinCompilerCarrierToAssets task -- consuming this via AGP's variant artifacts API +// (rather than a hardcoded path guessing the output filename) ties Gradle's dependency +// tracking to the actual producing task (packageV8Release), not just dependsOn ordering, which +// intermittently races the file's own write-to-disk on some CI runs (same defect as +// ADFA-5053's copyJavaCompilerCarrierToAssets, fixed there the same way). +androidComponents { + onVariants(selector().withBuildType("release")) { variant -> + extensions.extraProperties["releaseApkOutputDir"] = variant.artifacts.get(SingleArtifact.APK) + } +}