diff --git a/.gitignore b/.gitignore index af44d5bf1c..80d7a4268c 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/data/common/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..a5b3986b44 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) @@ -289,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) @@ -409,6 +417,164 @@ 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") { + // 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") + @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.dir(sourceDir) + outputs.file(destFile) + doLast { + val apkFile = + sourceDir + .get() + .asFileTree + .matching { include("*.apk") } + .singleFile + apkFile.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") +} + 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/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/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png index 4f5b301faf..1d81da0904 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_dump.png differ 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 f67149c79c..a3449ad5f9 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_foreground.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_foreground.png differ 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 e10476ea13..732ca75002 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_monochrome.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_icon_monochrome.png differ 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 718690d9f4..38f6d759c8 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_info.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_info.png differ 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 d65d7d2253..1ed5c903ed 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_leak.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_leak.png differ 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 c04699eba9..d8797f576f 100644 Binary files a/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_tv_icon.png and b/app/src/debug/res/drawable-xxhdpi-v4/leak_canary_tv_icon.png differ diff --git a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_dump.png b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_dump.png index 4f5b301faf..1d81da0904 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_dump.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_dump.png differ 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 f67149c79c..a3449ad5f9 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_foreground.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_foreground.png differ 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 e10476ea13..732ca75002 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_monochrome.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_icon_monochrome.png differ 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 718690d9f4..38f6d759c8 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_info.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_info.png differ 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 d65d7d2253..1ed5c903ed 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_leak.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_leak.png differ 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 c04699eba9..d8797f576f 100644 Binary files a/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_tv_icon.png and b/app/src/debug/res/drawable-xxxhdpi-v4/leak_canary_tv_icon.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi-v4/leak_canary_icon.png b/app/src/debug/res/mipmap-xxhdpi-v4/leak_canary_icon.png index 9ad0073e34..396499cc5a 100644 Binary files a/app/src/debug/res/mipmap-xxhdpi-v4/leak_canary_icon.png and b/app/src/debug/res/mipmap-xxhdpi-v4/leak_canary_icon.png differ 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 9ad0073e34..396499cc5a 100644 Binary files a/app/src/debug/res/mipmap-xxxhdpi-v4/leak_canary_icon.png and b/app/src/debug/res/mipmap-xxxhdpi-v4/leak_canary_icon.png differ 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..00a82174cd --- /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/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`: +- **`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 | 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/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..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 @@ -1,50 +1,75 @@ -/* - * 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/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..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 @@ -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 @@ -18,77 +15,66 @@ import kotlin.io.path.pathString * [KotlinMetadataScanner] for Kotlin classes, [JarSymbolScanner] for Java. */ 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) + 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 } - 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()) - } 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 2c30a93372..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 @@ -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 @@ -29,64 +26,51 @@ import kotlin.io.path.pathString * semantics like extensions, suspend, or nullable types. */ 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) - } + 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 } - } 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()) - } 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) + 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 } @@ -94,7 +78,6 @@ object JarSymbolScanner { private class SymbolCollector( private val sourceId: String, ) : ClassVisitor(Opcodes.ASM9) { - val symbols = mutableListOf() private var className = "" @@ -109,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 @@ -129,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 @@ -138,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( @@ -167,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 @@ -200,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( @@ -232,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 @@ -273,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 8c11a91c54..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 @@ -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 @@ -50,81 +47,76 @@ import kotlin.metadata.visibility * Skips non-Kotlin class files (no `@Metadata` annotation). */ 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) } + 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 } - } 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()) - } 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 + } } } @@ -134,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( @@ -166,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) { @@ -198,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, + ), + ), ) } } @@ -237,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 }, + ), + ), ) } @@ -263,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, @@ -298,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, + ), ), - ), ) } @@ -335,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, @@ -347,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) @@ -382,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 @@ -402,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) { @@ -421,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..984e578325 --- /dev/null +++ b/lsp/kotlin-api/src/main/java/com/itsaky/androidide/lsp/kotlin/api/IKotlinCompilerSession.kt @@ -0,0 +1,42 @@ +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() + + /** + * 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-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..dec761cdf3 --- /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) + + compileOnly(libs.common.kotlin) + compileOnly(libs.common.kotlin.coroutines.core) + compileOnly(libs.common.kotlin.coroutines.android) + compileOnly(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 88% 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..86b8489a2d 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 @@ -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, nioPath, action.referenceName) + computeImportCandidates(abstractEnv, 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..ad3d556db3 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/compiler/KotlinCompilerSessionImpl.kt @@ -0,0 +1,50 @@ +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 unregisterCodeActions() { + LSPEditorActions.ensureActionsMenuUnregistered(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 70% 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..15ea4588bf 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 @@ -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 - } -} \ No newline at end of file + override fun toString(): String = id +} 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 94% 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..d33e703889 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 @@ -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) @@ -37,4 +38,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-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 new file mode 100644 index 0000000000..3ae6cb79e7 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt @@ -0,0 +1,115 @@ +package com.itsaky.androidide.lsp.kotlin.completion + +import org.jetbrains.kotlin.lexer.KtKeywordToken +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) + ) + + /** 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 TOP_LEVEL_ONLY = setOf(PACKAGE_KEYWORD, 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(INIT_KEYWORD, CONSTRUCTOR_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.ANNOTATION_BODY -> { + setOf(VAL_KEYWORD) + } + + // annotation params only + + DeclarationContext.FUNCTION_BODY -> { + STATEMENT_KEYWORDS + } + } +} 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-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 new file mode 100644 index 0000000000..cfe6d68e21 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt @@ -0,0 +1,257 @@ +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.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 { + 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 }) { + candidates -= VISIBILITY_MODIFIERS.types() + } + if (MODALITY_MODIFIERS.types.any { it in existing }) { + candidates -= MODALITY_MODIFIERS.types() + } + + when (declCtx) { + DeclarationContext.INTERFACE_BODY -> { + // interface members are open by default; sealed/final don't apply to members + candidates -= setOf(FINAL_KEYWORD, OPEN_KEYWORD, SEALED_KEYWORD) + + // inner classes not allowed in interfaces + candidates -= INNER_KEYWORD + } + + DeclarationContext.FUNCTION_BODY -> { + // 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+) + INNER_KEYWORD, + ), + ) + } + + DeclarationContext.OBJECT_BODY, + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + candidates -= INNER_KEYWORD + } + + // inner only valid inside a class + + 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, + ) + + // const only on top-level or companion object val + if (declCtx !in + setOf( + DeclarationContext.TOP_LEVEL, + DeclarationContext.OBJECT_BODY, + 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, + ) + } + + DeclarationKind.FUN -> { + 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) + } + } + + DeclarationKind.CLASS -> { + 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) { + candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) + } + + // value class requires @JvmInline in practice, but `value` keyword is valid + } + + DeclarationKind.INTERFACE -> { + // 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 + ), + ) + } + + 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+) + COMPANION_KEYWORD, + ), + ) + } + + DeclarationKind.CONSTRUCTOR -> { + // constructors only take visibility modifiers + candidates.retainAll(VISIBILITY_MODIFIERS.types()) + } + + DeclarationKind.UNKNOWN -> { + // Cursor is after some modifiers but before any keyword. + // Keep all modifiers that are valid given what's already typed; + // the exclusion rules above already handled mutual exclusions. + } + + else -> { + Unit + } + } + + // expect and actual are mutually exclusive + if (EXPECT_KEYWORD in existing) candidates -= ACTUAL_KEYWORD + if (ACTUAL_KEYWORD in existing) candidates -= EXPECT_KEYWORD + + // noinline, crossinline and reified keywords are invalid if the + // function is not inline + if (INLINE_KEYWORD !in existing) { + candidates -= setOf(NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, REIFIED_KEYWORD) + } + + return candidates + } + + private fun TokenSet.types(): Set = types.filterIsInstance().toSet() + + private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) +} 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-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 new file mode 100644 index 0000000000..2a970e8a38 --- /dev/null +++ b/lsp/kotlin-compiler-impl/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt @@ -0,0 +1,261 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +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.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 { + val existing = ctx.existingModifiers + val declCtx = ctx.declarationContext + val declKind = ctx.declarationKind + + val candidates = MODIFIER_KEYWORDS_ARRAY.toMutableSet() + candidates -= existing + + // remove mutually exclusive groups + if (VISIBILITY_MODIFIERS.types.any { it in existing }) { + candidates -= VISIBILITY_MODIFIERS.types() + } + if (MODALITY_MODIFIERS.types.any { it in existing }) { + candidates -= MODALITY_MODIFIERS.types() + } + + when (declCtx) { + DeclarationContext.INTERFACE_BODY -> { + // interface members are open by default; sealed/final don't apply to members + candidates -= setOf(FINAL_KEYWORD, OPEN_KEYWORD, SEALED_KEYWORD) + + // inner classes not allowed in interfaces + candidates -= INNER_KEYWORD + } + + DeclarationContext.FUNCTION_BODY -> { + // 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+) + INNER_KEYWORD, + ), + ) + } + + DeclarationContext.OBJECT_BODY, + DeclarationContext.TOP_LEVEL, + DeclarationContext.SCRIPT_TOP_LEVEL, + -> { + candidates -= INNER_KEYWORD + } + + // inner only valid inside a class + + 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, + ) + + // const only on top-level or companion object val + if (declCtx !in + setOf( + DeclarationContext.TOP_LEVEL, + DeclarationContext.OBJECT_BODY, + 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, + ) + } + + DeclarationKind.FUN -> { + 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) + } + } + + DeclarationKind.CLASS -> { + 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) { + candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) + } + + // value class requires @JvmInline in practice, but `value` keyword is valid + } + + DeclarationKind.INTERFACE -> { + // 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 + ), + ) + } + + 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+) + COMPANION_KEYWORD, + ), + ) + } + + DeclarationKind.CONSTRUCTOR -> { + // constructors only take visibility modifiers + candidates.retainAll(VISIBILITY_MODIFIERS.types()) + } + + DeclarationKind.UNKNOWN -> { + // Cursor is after some modifiers but before any keyword. + // Keep all modifiers that are valid given what's already typed; + // the exclusion rules above already handled mutual exclusions. + } + + else -> { + Unit + } + } + + // expect and actual are mutually exclusive + if (EXPECT_KEYWORD in existing) candidates -= ACTUAL_KEYWORD + if (ACTUAL_KEYWORD in existing) candidates -= EXPECT_KEYWORD + + // noinline, crossinline and reified keywords are invalid if the + // function is not inline + if (INLINE_KEYWORD !in existing) { + candidates -= setOf(NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, REIFIED_KEYWORD) + } + + return candidates + } + + private fun TokenSet.types(): Set = types.filterIsInstance().toSet() + + private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = this.contains(token) +} 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..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 @@ -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,60 @@ class KotlinLanguageServer : ILanguageServer { override fun shutdown() { EventBus.getDefault().unregister(this) scope.cancel("LSP is being shut down") - compiler?.close() + // 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 } 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 +188,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 +213,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 +238,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 +253,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 +273,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 +283,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.openedFile) - ?.onFileOpen(event.openedFile) + compilationEnvironmentFor(event.openedFile)?.onFileOpen(event.openedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -312,9 +293,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.changedFile) - ?.onFileContentChanged(event.changedFile) + compilationEnvironmentFor(event.changedFile)?.onFileContentChanged(event.changedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -324,9 +303,7 @@ class KotlinLanguageServer : ILanguageServer { return } - compiler - ?.compilationEnvironmentFor(event.closedFile) - ?.onFileClosed(event.closedFile) + compilationEnvironmentFor(event.closedFile)?.onFileClosed(event.closedFile) } @Subscribe(threadMode = ThreadMode.ASYNC) @@ -336,16 +313,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 +332,7 @@ class KotlinLanguageServer : ILanguageServer { } scope.launch { - runCatching { compiler?.compilationEnvironmentFor(path) } + runCatching { compilationEnvironmentFor(path) } .getOrNull() ?.onFileCreated(path) } @@ -372,7 +347,7 @@ class KotlinLanguageServer : ILanguageServer { } scope.launch { - runCatching { compiler?.compilationEnvironmentFor(path) } + runCatching { compilationEnvironmentFor(path) } .getOrNull() ?.onFileRemoved(path) } @@ -391,27 +366,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/completion/ContextKeywords.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt deleted file mode 100644 index 433963f83e..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ContextKeywords.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.completion - -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( - 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 TOP_LEVEL_ONLY = setOf(PACKAGE_KEYWORD, 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(INIT_KEYWORD, CONSTRUCTOR_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.ANNOTATION_BODY -> setOf(VAL_KEYWORD) // annotation params only - - 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/ModifierFilter.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt deleted file mode 100644 index 5f0710e1d8..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/completion/ModifierFilter.kt +++ /dev/null @@ -1,174 +0,0 @@ -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.* - -/** - * 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 { - 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 }) - candidates -= VISIBILITY_MODIFIERS.types() - if (MODALITY_MODIFIERS.types.any { it in existing }) - candidates -= MODALITY_MODIFIERS.types() - - when (declCtx) { - DeclarationContext.INTERFACE_BODY -> { - // interface members are open by default; sealed/final don't apply to members - candidates -= setOf(FINAL_KEYWORD, OPEN_KEYWORD, SEALED_KEYWORD) - - // inner classes not allowed in interfaces - candidates -= INNER_KEYWORD - } - - DeclarationContext.FUNCTION_BODY -> { - // 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+) - INNER_KEYWORD, - ) - ) - } - - DeclarationContext.OBJECT_BODY, - DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> candidates -= INNER_KEYWORD // inner only valid inside a class - - 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, - ) - - // const only on top-level or companion object val - if (declCtx !in setOf( - DeclarationContext.TOP_LEVEL, - DeclarationContext.OBJECT_BODY, - 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, - ) - } - - DeclarationKind.FUN -> { - 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) - } - } - - DeclarationKind.CLASS -> { - 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) - candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) - - // value class requires @JvmInline in practice, but `value` keyword is valid - } - - DeclarationKind.INTERFACE -> { - // 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 - ) - ) - } - - 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+) - COMPANION_KEYWORD, - ) - ) - } - - DeclarationKind.CONSTRUCTOR -> { - // constructors only take visibility modifiers - candidates.retainAll(VISIBILITY_MODIFIERS.types()) - } - - DeclarationKind.UNKNOWN -> { - // Cursor is after some modifiers but before any keyword. - // Keep all modifiers that are valid given what's already typed; - // the exclusion rules above already handled mutual exclusions. - } - - else -> Unit - } - - // expect and actual are mutually exclusive - if (EXPECT_KEYWORD in existing) candidates -= ACTUAL_KEYWORD - if (ACTUAL_KEYWORD in existing) candidates -= EXPECT_KEYWORD - - // noinline, crossinline and reified keywords are invalid if the - // function is not inline - if (INLINE_KEYWORD !in existing) { - candidates -= setOf(NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, REIFIED_KEYWORD) - } - - return candidates - } - - private fun TokenSet.types(): Set = - types.filterIsInstance().toSet() - - 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/loader/KotlinCompilerLoader.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt new file mode 100644 index 0000000000..30734652f8 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/loader/KotlinCompilerLoader.kt @@ -0,0 +1,121 @@ +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_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. + 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_PATH).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_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) + } +} 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/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt deleted file mode 100644 index 406f9f0ac8..0000000000 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/ModifierFilter.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.itsaky.androidide.lsp.kotlin.utils - -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.* - -/** - * 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 { - val existing = ctx.existingModifiers - val declCtx = ctx.declarationContext - val declKind = ctx.declarationKind - - val candidates = MODIFIER_KEYWORDS_ARRAY.toMutableSet() - candidates -= existing - - // remove mutually exclusive groups - if (VISIBILITY_MODIFIERS.types.any { it in existing }) - candidates -= VISIBILITY_MODIFIERS.types() - if (MODALITY_MODIFIERS.types.any { it in existing }) - candidates -= MODALITY_MODIFIERS.types() - - when (declCtx) { - DeclarationContext.INTERFACE_BODY -> { - // interface members are open by default; sealed/final don't apply to members - candidates -= setOf(FINAL_KEYWORD, OPEN_KEYWORD, SEALED_KEYWORD) - - // inner classes not allowed in interfaces - candidates -= INNER_KEYWORD - } - - DeclarationContext.FUNCTION_BODY -> { - // 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+) - INNER_KEYWORD, - ) - ) - } - - DeclarationContext.OBJECT_BODY, - DeclarationContext.TOP_LEVEL, - DeclarationContext.SCRIPT_TOP_LEVEL -> candidates -= INNER_KEYWORD // inner only valid inside a class - - 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, - ) - - // const only on top-level or companion object val - if (declCtx !in setOf( - DeclarationContext.TOP_LEVEL, - DeclarationContext.OBJECT_BODY, - 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, - ) - } - - DeclarationKind.FUN -> { - 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) - } - } - - DeclarationKind.CLASS -> { - 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) - candidates -= setOf(OPEN_KEYWORD, FINAL_KEYWORD, ABSTRACT_KEYWORD) - - // value class requires @JvmInline in practice, but `value` keyword is valid - } - - DeclarationKind.INTERFACE -> { - // 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 - ) - ) - } - - 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+) - COMPANION_KEYWORD, - ) - ) - } - - DeclarationKind.CONSTRUCTOR -> { - // constructors only take visibility modifiers - candidates.retainAll(VISIBILITY_MODIFIERS.types()) - } - - DeclarationKind.UNKNOWN -> { - // Cursor is after some modifiers but before any keyword. - // Keep all modifiers that are valid given what's already typed; - // the exclusion rules above already handled mutual exclusions. - } - - else -> Unit - } - - // expect and actual are mutually exclusive - if (EXPECT_KEYWORD in existing) candidates -= ACTUAL_KEYWORD - if (ACTUAL_KEYWORD in existing) candidates -= EXPECT_KEYWORD - - // noinline, crossinline and reified keywords are invalid if the - // function is not inline - if (INLINE_KEYWORD !in existing) { - candidates -= setOf(NOINLINE_KEYWORD, CROSSINLINE_KEYWORD, REIFIED_KEYWORD) - } - - return candidates - } - - private fun TokenSet.types(): Set = - types.filterIsInstance().toSet() - - private operator fun TokenSet.contains(token: KtModifierKeywordToken): Boolean = - this.contains(token) -} \ No newline at end of file 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 b9df4260c5..bc57cb1c6e 100644 Binary files a/resources/src/main/res/drawable-xhdpi/ic_action_more.png and b/resources/src/main/res/drawable-xhdpi/ic_action_more.png differ diff --git a/resources/src/main/res/drawable-xhdpi/ic_launcher_notification.png b/resources/src/main/res/drawable-xhdpi/ic_launcher_notification.png index bfb43e591e..17f98aae1f 100644 Binary files a/resources/src/main/res/drawable-xhdpi/ic_launcher_notification.png and b/resources/src/main/res/drawable-xhdpi/ic_launcher_notification.png differ diff --git a/resources/src/main/res/drawable-xxhdpi/ic_action_more.png b/resources/src/main/res/drawable-xxhdpi/ic_action_more.png index d568dd0b2e..cd40ec1b5e 100644 Binary files a/resources/src/main/res/drawable-xxhdpi/ic_action_more.png and b/resources/src/main/res/drawable-xxhdpi/ic_action_more.png differ 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 3b8dc987f4..70302fe8e2 100644 Binary files a/resources/src/main/res/drawable-xxhdpi/ic_launcher_notification.png and b/resources/src/main/res/drawable-xxhdpi/ic_launcher_notification.png differ diff --git a/resources/src/main/res/drawable-xxxhdpi/ic_launcher_notification.png b/resources/src/main/res/drawable-xxxhdpi/ic_launcher_notification.png index 6a91fbc2e0..327e76ab34 100644 Binary files a/resources/src/main/res/drawable-xxxhdpi/ic_launcher_notification.png and b/resources/src/main/res/drawable-xxxhdpi/ic_launcher_notification.png differ 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 042c8a9f87..2aabffe943 100644 Binary files a/resources/src/main/res/drawable-xxxhdpi/template_basic_activity.png and b/resources/src/main/res/drawable-xxxhdpi/template_basic_activity.png differ diff --git a/resources/src/main/res/drawable-xxxhdpi/template_no_activity.png b/resources/src/main/res/drawable-xxxhdpi/template_no_activity.png index 9a14d7b695..632eb1ca11 100644 Binary files a/resources/src/main/res/drawable-xxxhdpi/template_no_activity.png and b/resources/src/main/res/drawable-xxxhdpi/template_no_activity.png differ 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 6341ea2e62..667231b2a0 100644 Binary files a/resources/src/main/res/drawable/app_dev_for_all_logo.png and b/resources/src/main/res/drawable/app_dev_for_all_logo.png differ diff --git a/resources/src/main/res/mipmap-hdpi/ic_launcher.png b/resources/src/main/res/mipmap-hdpi/ic_launcher.png index a9aaa4da0b..925b7c5444 100644 Binary files a/resources/src/main/res/mipmap-hdpi/ic_launcher.png and b/resources/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 b36de1bd45..af50c0beb9 100644 Binary files a/resources/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/resources/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ 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 49f6bbdaa2..036ee78cd9 100644 Binary files a/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/resources/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/resources/src/main/res/mipmap-mdpi/ic_launcher.png b/resources/src/main/res/mipmap-mdpi/ic_launcher.png index c3357a22d6..fe2de9f3e2 100644 Binary files a/resources/src/main/res/mipmap-mdpi/ic_launcher.png and b/resources/src/main/res/mipmap-mdpi/ic_launcher.png differ 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 ca37eac5e9..eb4870ad1d 100644 Binary files a/resources/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/resources/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png b/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png index 084d87e9bb..16769b3bb8 100644 Binary files a/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/resources/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/resources/src/main/res/mipmap-xhdpi/ic_launcher.png b/resources/src/main/res/mipmap-xhdpi/ic_launcher.png index f87bd3cc68..cd55655cb8 100644 Binary files a/resources/src/main/res/mipmap-xhdpi/ic_launcher.png and b/resources/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 83e86560f0..74ccddd0e7 100644 Binary files a/resources/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/resources/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ 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 e1821982d1..5de2e8d84e 100644 Binary files a/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/resources/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png b/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png index 74bcb2f4ca..5c758d2a00 100644 Binary files a/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/resources/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 5f7a54e2cd..458587f3d7 100644 Binary files a/resources/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/resources/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ 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 c28efe8f9f..3a1c19d637 100644 Binary files a/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/resources/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png index e4864e55e3..53dd67f675 100644 Binary files a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 04393d5007..8a0f94a390 100644 Binary files a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ 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 6ca1ccca11..0e056db413 100644 Binary files a/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/resources/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ 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..eacfb613c5 --- /dev/null +++ b/subprojects/kotlin-compiler-carrier/build.gradle.kts @@ -0,0 +1,38 @@ +import com.android.build.api.artifact.SingleArtifact +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) +} + +// 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) + } +} 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 @@ + + + + + + + + diff --git a/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher.png b/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher.png index a571e60098..fac9b9084f 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 61da551c55..5a5093e3e4 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/testing/resources/test-project/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher.png b/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher.png index c41dd28531..5c1406faab 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index db5080a752..25e25fb048 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/testing/resources/test-project/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 6dba46dab1..0300d70ca2 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index da31a871c8..c00181e93b 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/testing/resources/test-project/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 15ac681720..22ef3421aa 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 b216f2d313..5e9215a4e3 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/testing/resources/test-project/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index f25a419744..f788611c99 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index e96783ccce..3bf336fbb6 100644 Binary files a/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/testing/resources/test-project/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ