diff --git a/.gitignore b/.gitignore
index af44d5bf1c..f08cc9697a 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/java-compiler-carrier.apk
# AI plugin development artifacts (moved to plugin-examples repo)
*.cgp
diff --git a/actions/src/main/java/com/itsaky/androidide/actions/locations/CodeActionsMenu.kt b/actions/src/main/java/com/itsaky/androidide/actions/locations/CodeActionsMenu.kt
index 2915f7db2d..ba3047786e 100644
--- a/actions/src/main/java/com/itsaky/androidide/actions/locations/CodeActionsMenu.kt
+++ b/actions/src/main/java/com/itsaky/androidide/actions/locations/CodeActionsMenu.kt
@@ -1,52 +1,60 @@
-/*
- * 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.actions.locations
-
-import android.content.Context
-import android.graphics.drawable.Drawable
-import androidx.core.content.ContextCompat
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.ActionItem
-import com.itsaky.androidide.actions.ActionMenu
-import com.itsaky.androidide.idetooltips.TooltipTag
-import com.itsaky.androidide.resources.R
-
-/** @author Akash Yadav */
-object CodeActionsMenu : ActionMenu {
-
- const val ID = "ide.editor.code.actions"
-
- override val children: MutableSet = mutableSetOf()
- override val id: String = ID
-
- override var label: String = "Code actions"
- override var visible = true
- override var enabled: Boolean = true
- override var icon: Drawable? = null
- override fun retrieveTooltipTag(isAlternateContext: Boolean) =
- TooltipTag.EDITOR_TOOLBAR_CODE_ACTIONS
- override var requiresUIThread: Boolean = false
- override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
- if (icon == null) {
- icon = ContextCompat.getDrawable(data[Context::class.java]!!, R.drawable.ic_code)
- }
- }
-}
\ No newline at end of file
+/*
+ * 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.actions.locations
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.core.content.ContextCompat
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.ActionItem
+import com.itsaky.androidide.actions.ActionMenu
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.resources.R
+import java.util.concurrent.CopyOnWriteArraySet
+
+/** @author Akash Yadav */
+object CodeActionsMenu : ActionMenu {
+ const val ID = "ide.editor.code.actions"
+
+ // Registered/unregistered from LSP-dispatch threads (e.g. JavaCompilerSessionImpl's
+ // registerCodeActions()/unregisterCodeActions(), called from ensureProjectReset()/
+ // shutdown()) concurrently with the UI thread reading it every time the code-actions menu
+ // is rendered (ActionMenu.prepare()/isAtLeastOneChildVisible()) -- CopyOnWriteArraySet avoids
+ // both a ConcurrentModificationException on a plain set and needing external synchronization
+ // around every read, at the cost of a full backing-array copy per add/remove (cheap: this
+ // set is small and mutated only on session register/unregister, not per keystroke).
+ override val children: MutableSet = CopyOnWriteArraySet()
+ override val id: String = ID
+
+ override var label: String = "Code actions"
+ override var visible = true
+ override var enabled: Boolean = true
+ override var icon: Drawable? = null
+
+ override fun retrieveTooltipTag(isAlternateContext: Boolean) = TooltipTag.EDITOR_TOOLBAR_CODE_ACTIONS
+
+ override var requiresUIThread: Boolean = false
+ override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+ if (icon == null) {
+ icon = ContextCompat.getDrawable(data[Context::class.java]!!, R.drawable.ic_code)
+ }
+ }
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2f4fdf7ddc..6640c9d7e5 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -36,6 +36,15 @@ plugins {
alias(libs.plugins.google.services)
}
+// Forces :subprojects:java-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 copyJavaCompilerCarrierToAssets'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:java-compiler-carrier")
+
fun propOrEnv(name: String): String =
project.findProperty(name) as String?
?: System.getenv(name)
@@ -288,7 +297,6 @@ dependencies {
implementation(projects.eventbusEvents)
implementation(projects.gradlePluginConfig)
implementation(projects.subprojects.aaptcompiler)
- implementation(projects.subprojects.javacServices)
implementation(projects.subprojects.kotlinAnalysisApi)
implementation(projects.subprojects.shizukuApi)
implementation(projects.subprojects.shizukuManager)
@@ -409,6 +417,44 @@ tasks.register("downloadDocDb") {
}
}
+// Copies the isolated javac carrier APK -- built by :subprojects:java-compiler-carrier, which
+// bundles lsp:java-compiler-impl (the vendored javac fork + its LSP consumers) -- straight into
+// app's own assets, so it ships in the base APK and D8 never merges it into app's own
+// classes*.dex. JavaCompilerLoader (lsp:java) extracts and DexClassLoader-loads it lazily on
+// first real .java-file interaction (ADFA-5053, mirrors ADR 0011's Kotlin precedent). No PNG
+// optimization needed here (unlike the Kotlin carrier) -- this module has no resources at all.
+tasks.register("copyJavaCompilerCarrierToAssets") {
+ // The source directory below is AGP's own Provider for the release variant's real
+ // APK output (see releaseApkOutputDir in java-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 (ADFA-5053) even though locally it usually won
+ // the race. See evaluationDependsOn(":subprojects:java-compiler-carrier") above for why this
+ // avoids project(":subprojects:java-compiler-carrier").layout... here.
+ dependsOn(":subprojects:java-compiler-carrier:assembleV8Release")
+ @Suppress("UNCHECKED_CAST")
+ val sourceDir =
+ project(":subprojects:java-compiler-carrier").extensions.extraProperties["releaseApkOutputDir"]
+ as Provider
+ val destFile = layout.projectDirectory.file("src/main/assets/data/common/java-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)
+ }
+}
+
+tasks.named("preBuild") {
+ dependsOn("copyJavaCompilerCarrierToAssets")
+}
+
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..3137b344ea 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -17,9 +17,6 @@
-keep class javax.** { *; }
-keep class jdkx.** { *; }
-# keep javac classes
--keep class openjdk.** { *; }
-
# Android builder model interfaces
-keep class com.android.** { *; }
diff --git a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
index e8e0494c11..fbeb128466 100644
--- a/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
+++ b/app/src/main/java/com/itsaky/androidide/activities/editor/ProjectHandlerActivity.kt
@@ -57,7 +57,6 @@ import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult
-import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.models.Position
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
@@ -195,6 +194,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
const val STATE_KEY_SHOULD_INITIALIZE = "ide.editor.isInitializing"
private const val PLUGIN_SEARCH_TIMEOUT_SECONDS = 10L
+
+ private fun isCancellation(err: Throwable?): Boolean {
+ err ?: return false
+ return err is CancellationException || isCancellation(err.cause)
+ }
}
abstract fun doCloseAll()
@@ -613,7 +617,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
releaseServerListener()
if (result == null || !result.isSuccessful || error != null) {
- if (!CancelChecker.isCancelled(error)) {
+ if (!isCancellation(error)) {
log.error("An error occurred initializing the project with Tooling API", error)
}
diff --git a/composite-builds/build-deps/google-java-format/build.gradle.kts b/composite-builds/build-deps/google-java-format/build.gradle.kts
index 08029829f3..0b37114aa7 100644
--- a/composite-builds/build-deps/google-java-format/build.gradle.kts
+++ b/composite-builds/build-deps/google-java-format/build.gradle.kts
@@ -16,20 +16,27 @@
*/
plugins {
- id("com.android.library")
- id("com.itsaky.androidide.build")
+ id("com.android.library")
+ id("com.itsaky.androidide.build")
}
android {
- namespace = "com.google.googlejavaformat"
+ namespace = "com.google.googlejavaformat"
}
dependencies {
- implementation(libs.google.guava)
- implementation(libs.google.auto.value.annotations)
- implementation(libs.google.auto.service.annotations)
- implementation(projects.buildDeps.javac)
+ implementation(libs.google.guava)
+ implementation(libs.google.auto.value.annotations)
+ implementation(libs.google.auto.service.annotations)
- annotationProcessor(libs.google.auto.value.ap)
- annotationProcessor(libs.google.auto.service)
-}
\ No newline at end of file
+ // NOT projects.buildDeps.javac (the aggregate): java-compiler must stay resident-only when
+ // this module is consumed by the isolated javac carrier (ADFA-5053) -- see
+ // composite-builds/build-deps/jdk-compiler's identical fix for the full rationale. This
+ // module genuinely runs javac's own parser at runtime (that's how it reformats source), so
+ // jdk-compiler itself stays a real, bundled dependency.
+ implementation(projects.buildDeps.jdkCompiler)
+ compileOnly(projects.buildDeps.javaCompiler)
+
+ annotationProcessor(libs.google.auto.value.ap)
+ annotationProcessor(libs.google.auto.service)
+}
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java
similarity index 86%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java
index 607b300790..28c62d971b 100644
--- a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java
+++ b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java
@@ -101,7 +101,14 @@ public List getJarClassPath(Path file) throws IOException {
}
}
- protected Optional getAttributes(Path file) {
+ // public, not protected: JavacFileManager (openjdk.tools.javac.file, isolated in the javac
+ // carrier per ADFA-5053) calls this directly on a CacheFSInfo instance, which is resident
+ // (loaded by the parent classloader). ART treats the two as different runtime packages
+ // despite the identical package name, since same-package/protected access is resolved by
+ // classloader identity, not just the package string -- protected access across that
+ // boundary throws IllegalAccessError at runtime, caught by on-device testing, not by the
+ // build or unit tests.
+ public Optional getAttributes(Path file) {
return attributeCache.computeIfAbsent(file, this::maybeReadAttributes);
}
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/FSInfo.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/FSInfo.java
similarity index 100%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/FSInfo.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/FSInfo.java
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java
similarity index 90%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java
index 280228518a..c89ab7ce7c 100644
--- a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java
+++ b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/file/RelativePath.java
@@ -102,7 +102,10 @@ public String getPath() {
*/
public static class RelativeDirectory extends RelativePath {
- static RelativeDirectory forPackage(CharSequence packageName) {
+ // public, not package-private: JavacFileManager/JRTIndex (isolated in the javac carrier
+ // per ADFA-5053) call this on RelativeDirectory, which is resident -- same
+ // cross-classloader access rationale as CacheFSInfo.getAttributes above.
+ public static RelativeDirectory forPackage(CharSequence packageName) {
return new RelativeDirectory(packageName.toString().replace('.', '/'));
}
@@ -158,7 +161,10 @@ public String toString() {
* Internally, the file separator is always '/'. It never ends in '/'.
*/
public static class RelativeFile extends RelativePath {
- static RelativeFile forClass(CharSequence className, JavaFileObject.Kind kind) {
+ // public, not package-private: JavacFileManager (isolated in the javac carrier per
+ // ADFA-5053) calls this on RelativeFile, which is resident -- same cross-classloader
+ // access rationale as CacheFSInfo.getAttributes above.
+ public static RelativeFile forClass(CharSequence className, JavaFileObject.Kind kind) {
return new RelativeFile(className.toString().replace('.', '/') + kind.extension);
}
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/Assert.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/Assert.java
similarity index 100%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/Assert.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/Assert.java
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/Context.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/Context.java
similarity index 100%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/Context.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/Context.java
diff --git a/composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/PlatformUtils.java b/composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/PlatformUtils.java
similarity index 100%
rename from composite-builds/build-deps/jdk-compiler/src/main/java/openjdk/tools/javac/util/PlatformUtils.java
rename to composite-builds/build-deps/java-compiler/src/main/java/openjdk/tools/javac/util/PlatformUtils.java
diff --git a/composite-builds/build-deps/javapoet/build.gradle.kts b/composite-builds/build-deps/javapoet/build.gradle.kts
index fe99059f36..f93a542f06 100644
--- a/composite-builds/build-deps/javapoet/build.gradle.kts
+++ b/composite-builds/build-deps/javapoet/build.gradle.kts
@@ -16,9 +16,13 @@
*/
plugins {
- kotlin("jvm")
+ kotlin("jvm")
}
dependencies {
- api(projects.buildDeps.javaCompiler)
-}
\ No newline at end of file
+ // javapoet itself stays fully resident (see lsp/java-compiler-impl/build.gradle.kts) --
+ // templates-api/templates-impl (the "New Project" wizard) need it unconditionally, unlike
+ // javac. So unlike jdk-compiler's identical-looking dependency, this one stays `api`: there's
+ // no isolated consumer to duplicate java-compiler's classes into.
+ api(projects.buildDeps.javaCompiler)
+}
diff --git a/composite-builds/build-deps/jdk-compiler/build.gradle.kts b/composite-builds/build-deps/jdk-compiler/build.gradle.kts
index 20a207ffa3..9fd528954c 100644
--- a/composite-builds/build-deps/jdk-compiler/build.gradle.kts
+++ b/composite-builds/build-deps/jdk-compiler/build.gradle.kts
@@ -30,5 +30,11 @@ tasks.withType().configureEach {
}
dependencies {
- api(projects.buildDeps.javaCompiler)
+ // compileOnly, not api: java-compiler must stay resident-only when jdk-compiler is consumed
+ // by the isolated javac carrier (ADFA-5053) -- an `api` dependency here would propagate to
+ // every consumer's runtime/packaging classpath regardless of how *they* declare their own
+ // dependency on this module, duplicating CacheFSInfo/Context/etc. into the carrier dex. The
+ // `javac` aggregate module (composite-builds/build-deps/javac) still `api`s both modules
+ // itself for its own (resident-only) consumers.
+ compileOnly(projects.buildDeps.javaCompiler)
}
\ No newline at end of file
diff --git a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md
new file mode 100644
index 0000000000..d34bda2096
--- /dev/null
+++ b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md
@@ -0,0 +1,66 @@
+# 0012. Lazy-load the embedded javac fork via a carrier APK + DexClassLoader
+
+- **Status:** Proposed
+- **Date:** 2026-08-06
+- **Deciders:** Code On The Go team
+
+> **Note:** every "ADR 0011" / `KotlinCompilerLoader` reference below describes ADFA-5010's
+> concurrent, sibling application of this same pattern to the Kotlin Analysis API (branch
+> `task/ADFA-5010-lazy-load-kotlin-compiler`), not an already-merged precedent this branch can
+> point to -- as of this change, `docs/adr/` has no `0011-*.md` file and `stage` has no
+> `KotlinCompilerLoader`. The real, existing precedent both tickets extend is
+> `plugin-manager/.../PluginLoader.kt`'s lazy-`DexClassLoader` pattern. Land ADFA-5010 first, or
+> adjust these cross-references, before merging this ADR to avoid dangling links/claims.
+
+## Context
+
+Per ADFA-4549's release-build DEX analysis, the vendored `javac` fork (`openjdk.tools.javac.**`, `composite-builds/build-deps/jdk-compiler`) was the second-largest single DEX contributor after the Kotlin Analysis API ([ADR 0011](0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md)): ~2,238 classes, ~3.7MB. Like the Analysis API, it's not a build-time compiler — real Gradle builds run out-of-process via the Tooling API ([ADR 0002](0002-on-device-builds-via-gradle-tooling-api.md)) and never touch it. What's embedded backs purely in-process, live-editing features (completion, diagnostics, navigation, signature help, code actions, formatting) in the Java language server, and — like the Analysis API before ADR 0011 — it was always resident and constructed eagerly: `JavaLanguageServer.setupWithProject()` referenced `JavaCompilerService.NO_MODULE_COMPILER`/`SourceFileManager`, triggering real `Context`/`JavacFileManager` construction plus a full `android.jar` scan, on every project open regardless of language (ADFA-5052 fixed the eager-construction half of this independently; see Decision below for how this ticket builds on it).
+
+**Harder than ADR 0011's Kotlin case, for one specific reason.** The Kotlin Analysis API was a single downloaded jar with one clear consumer. javac is vendored source, substituted via `composite-builds/build-deps` ([ADR 0003](0003-vendored-forked-desktop-toolchain.md)), and — the complication that shaped most of this decision — `subprojects/projects` (the foundational, always-resident project-model module used by *every* project regardless of language) directly imports real `openjdk.tools.javac.file.*` types via a handful of small wrapper classes, for live classpath-jar indexing (`ModuleProject.indexClasspaths()`, called for every module of every project). That's genuinely load-bearing, unlike the analogous `lsp/jvm-symbol-index` dependency ADR 0011 found and removed as dead code.
+
+Investigation found this coupling narrower than it first looked: none of `CacheFSInfo`/`FSInfo`/`Context`/`Assert`/`PlatformUtils`/`RelativePath` (the file/attribute-caching leaf classes `subprojects/projects` actually needs, via its own `subprojects/javac-fs` wrapper) import anything from javac's parser/`Attr`/`Resolve`/`Symtab`/`Types`/codegen — the ~400-file heavy fork proper. The dependency is confirmed one-directional (fork → these leaf classes, via `ReusableContext.kt`; never the reverse).
+
+**Explicitly out of scope:** `jdt` (6 files, 44KB, used only by `xml-utils` for an unrelated signature-parsing utility). `jaxp` (~2,172 classes, ~4.9MB — the single largest remaining DEX chunk) was investigated as a candidate for the same treatment and rejected: its actual load-bearing path is `AndroidModule.readResources()`/`extractPackageName()` parsing every dependency AAR's `AndroidManifest.xml` during project sync to populate `R.id`/`R.string` completion consumed by *all* languages, not just XML files — not gateable behind a single-language trigger the way javac and Kotlin are. jaxp's dex weight is also ~85% dead code kept alive only by an overbroad `-keep class jaxp.** { *; }` rule, not genuine reachability — a ProGuard-scoping fix, tracked separately, not a lazy-load ticket.
+
+## Decision
+
+**Isolate the javac fork and everything that directly references it behind a `DexClassLoader` boundary, loaded lazily on first real `.java`-file interaction, mirroring ADR 0011 exactly** — reusing ADFA-5052's already-built lazy-trigger machinery (`JavaLanguageServer`'s `CompilerLifecycle` state machine) rather than rebuilding it: this decision only changes *what* that trigger constructs (a `DexClassLoader`-loaded session instead of a resident `JavaCompilerService`), not *when* it fires.
+
+**The vendored composite build needed a small, mechanical relocation first**, to resolve the `subprojects/projects` coupling above: the six leaf classes (`CacheFSInfo`, `FSInfo`, `Context`, `Assert`, `PlatformUtils`, `RelativePath`, package names unchanged) moved from `composite-builds/build-deps/jdk-compiler` into `composite-builds/build-deps/java-compiler` — an existing, separate composite-build sub-module that already held the self-contained `jdkx.*`/`com.itsaky.androidide.zipfs2.*` API surface with zero javac-fork dependency, and which `jdk-compiler` already depended on (so the move can't introduce a cycle). This let a new small resident module, `subprojects/javac-fs`, depend on `java-compiler` alone rather than the full fork — without it, isolating the rest of `jdk-compiler` would have either duplicated these classes (a resident copy plus a carrier-dex copy) or stranded them where `subprojects/projects` couldn't reach them.
+
+- **`lsp/java-api`** — the bridge: `IJavaCompilerSession`, `IJavaCompilerSessionFactory`. Resident. Exposes the LSP operations directly (`complete`/`findReferences`/`findDefinition`/`expandSelection`/`signatureHelp`/`analyze`/`onContentChange`/`formatCode`/`handleCompletionFailure`/`resetProject`/`registerCodeActions`/`unregisterCodeActions`/`findSourceFilePath`) rather than a `getCompiler(): JavaCompilerService` accessor, since `JavaCompilerService` itself isn't a resident type — confirmed no caller outside `JavaLanguageServer` used the old accessor before removing it.
+- **`lsp/java-compiler-impl`** — the isolated payload: essentially all of `lsp/java`'s previous `compiler/`, `providers/` (except `providers/snippet`), `actions/`, `edits/`, `parser/`, `rewrite/`, `utils/` (except `AnalyzeTimer`), `visitors/`, plus `JavaCompilerProvider`. Implements the bridge against the real `JavaCompilerService`/`JavaCompilerProvider`. Depends `implementation` on `subprojects/javac-services` (now fs-free) and `libs.composite.jdkCompiler` (the fork alone, *not* the `javac` aggregate — see Consequences); resident types it needs (`lsp/java`, `lsp/api`, `lsp/models`, `subprojects/projects`, `subprojects/javac-fs`, `java-compiler`) are `compileOnly`.
+- **`subprojects/java-compiler-carrier`** — a `com.android.application` module (`isMinifyEnabled = false`, matching ADR 0011's "ship intact, don't shrink" call) whose only purpose is a real, D8-dexed APK from `lsp/java-compiler-impl`. Never installed; `app`'s build copies its output to `app/src/main/assets/data/common/java-compiler-carrier.apk` (`copyJavaCompilerCarrierToAssets`, wired into `preBuild`).
+- **`lsp/java`** (resident) — `JavaLanguageServer` becomes a thin wrapper, keeping ADFA-5052's `CompilerLifecycle`/`compilerLifecycleLock`/`ensureProjectReset()` unchanged in shape. `JavaCompilerLoader` (new, mirrors `KotlinCompilerLoader`) extracts the carrier APK and loads it via `DexClassLoader(apkPath, optimizedDir, null, parent)` inside `ensureProjectReset()`'s existing gate.
+
+**Two genuinely resident-vs-isolated judgment calls, decided by what each library actually does at runtime, not just what it's used for:**
+- **google-java-format moved with javac into the isolated module**, even though its only resident-side touchpoint (`JavaServerSettings`' formatter options) looks like plain config. It uses javac's own parser internally to reformat source, so it needs the real fork at runtime — `JavaServerSettings` now exposes only a plain code-style `int`; the two isolated call sites (`CodeFormatProvider`, `OrganizeImportsAction`) build the real `JavaFormatterOptions` themselves.
+- **javapoet stayed fully resident**, despite being used by an isolated-module file (`JavaPoetUtils.kt`'s code-generation actions), because it's *also* needed unconditionally by `templates-api`/`templates-impl` (the "New Project" wizard — a resident, javac-unrelated feature) and has zero dependency on the heavy fork itself (only on `java-compiler`'s lightweight `jdkx` model types). `lsp/java-compiler-impl` sees it via `compileOnly`.
+
+**Duplicate-class-identity bugs found only by dex-inspecting the actual built carrier, not by reading the Gradle config** — the same failure mode ADR 0011 calls out. `jdk-compiler`'s own `build.gradle.kts` had `api(projects.buildDeps.javaCompiler)` (an `api` dependency propagates to every consumer's runtime classpath regardless of how *they* declare their own dependency on `jdk-compiler`, so no consumer-side `compileOnly` could stop it); `google-java-format`'s composite-build file had the identical pattern and needed the identical fix, since it's bundled into the isolated module alongside javac (see below). `javapoet`'s composite-build file has the same-looking `api(projects.buildDeps.javaCompiler)` line but was deliberately left as `api`, not changed to `compileOnly` — javapoet itself stays fully resident (see below), so there's no isolated consumer for it to leak into. A separate, real leak — `app/build.gradle.kts`'s own stray direct `implementation(projects.subprojects.javacServices)`, with zero actual source usage in `app/` — turned out to be the same class of leftover ADR 0011 found and removed for `kotlin-analysis-api`.
+
+**A second, distinct hazard class found only by an on-device run, not by the build, unit tests, or dex inspection: cross-classloader `protected`/package-private access.** ART resolves same-package and `protected` member access by classloader identity, not just the package name string — two classes named `openjdk.tools.javac.file.X` and `openjdk.tools.javac.file.Y` are *not* considered the same runtime package if `X` is resident (parent classloader) and `Y` is isolated (carrier's `DexClassLoader`). `JavacFileManager` (isolated) calling `CacheFSInfo.getAttributes` (resident, `protected`) — both nominally in the same `openjdk.tools.javac.file` package — threw `IllegalAccessError` at the first real `.java`-file interaction on a physical device, with no signal at build time or in any unit test. Two more call sites (`RelativeFile.forClass`, `RelativeDirectory.forPackage`, both package-private) had the identical problem, found by then auditing the rest of the fork for other cross-boundary access to the six resident leaf classes. All three were widened to `public`. Anyone adding a new resident leaf class to this boundary needs to grep the fork for cross-references to it and make sure every accessed member is `public`, not just correctly deduplicated at the class level -- **except** where the isolated class *extends* the resident one (e.g. `ReusableContext extends Context`) and accesses a `protected` member via an implicit/`this`-typed reference: that's protected access via inheritance, governed by JVMS 5.4.4's other `protected` bullet, which has no runtime-package/classloader-identity condition and doesn't need widening.
+
+## Consequences
+
+**Positive**
+- The javac fork is gone from `app`'s own `classes*.dex` entirely — confirmed via `dexdump` class-descriptor listings against both a real release build and the carrier's own dex (not just Gradle's `checkDuplicateClasses`, which only catches conflicts within one module's own build graph): `NBAttr`/`ReusableCompiler`/`JavaCompilerService`/`JavaCompilerSessionImpl`/google-java-format's `Formatter` appear exactly once, in the carrier's dex, never `app`'s; the resident leaf classes (`CacheFSInfo`/`Context`/`Assert`/`PlatformUtils`/`CacheFSInfoSingleton`/JavaPoet) appear exactly once, in `app`'s dex, never the carrier's.
+- Projects that never open a `.java` file never pay the classload or `JavaCompilerService`/`SourceFileManager` construction cost — building on, not duplicating, ADFA-5052's existing lazy-trigger fix.
+- `app/proguard-rules.pro`'s `-keep class openjdk.** { *; }` is removed — nothing in `app`'s own classpath needs it anymore (the carrier doesn't run R8 at all, so it doesn't need it either). `jdkx.**`'s keep rule stays: unlike Kotlin's Analysis API, some `jdkx`/`java-compiler` classes remain genuinely resident (`javac-fs`, javapoet), so this rule isn't a pure no-op the way the removed one was.
+
+**Negative / costs**
+- First `.java`-file interaction in a session now pays a one-time synchronous latency spike (asset extraction on first run + `DexClassLoader` construction + `JavaCompilerService`/`SourceFileManager` bootstrap) on top of ADFA-5052's own deferred-reset cost.
+- A third resident/isolated classloader boundary to reason about (after Kotlin's and the plugin system's). The same rule as ADR 0011 applies and now has two worked examples of getting it wrong: an `api` dependency anywhere in a vendored composite build's *own* `build.gradle.kts` propagates to every consumer's runtime classpath regardless of how the consumer declares its dependency — `compileOnly` has to be applied at the source of the leak, not just where it's consumed. A second, distinct rule this ADR adds: every resident member the isolated fork calls across the boundary must be `public` — `protected`/package-private access throws `IllegalAccessError` at runtime even when both classes share a package name, since ART checks classloader identity, not just the package string, and this has no build-time or unit-test signal at all.
+- The debugger's breakpoint/stack-frame source-path resolution (`JavaDebugAdapter`/`ModelUtils.asLspLocation`) took on a narrow, real dependency on `JavaCompilerProvider`/`SourceFileObject` that the isolation boundary can't ignore; it now resolves through `IJavaCompilerSession.findSourceFilePath` (returning a plain path, not the isolated `SourceFileObject` type) instead, and degrades to filename-only when no session exists yet.
+
+## Alternatives considered
+
+Same as ADR 0011 — Android Dynamic Feature modules (rejected: this app installs outside the Play Store), reflectively patching `PathClassLoader.pathList` (rejected: relies on progressively-locked-down non-SDK internals), and the brotli-compressed installer-zip pipeline (rejected: that framework is for hundreds-of-MB optional downloads; this is tens of MB that belongs in the base APK unconditionally).
+
+## Related
+
+- [ADR 0011](0011-lazy-load-kotlin-analysis-api-via-dexclassloader.md) — the Kotlin precedent this mirrors structurally; also the origin of the `api`-vs-`compileOnly` duplicate-class-identity rule this decision found two more violations of.
+- [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 vendoring/substitution mechanism this decision's composite-build relocation operates within.
+- ADFA-5052 — the eager-construction fix this decision builds directly on top of, reusing its `CompilerLifecycle` state machine as the DexClassLoader trigger point.
+- `plugin-manager/.../PluginLoader.kt` — the existing `DexClassLoader` pattern both this and ADR 0011 extend.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 9bb6db0c4a..25e16421ee 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 |
+| [0012](0012-lazy-load-javac-via-dexclassloader.md) | Lazy-load the embedded javac fork via a carrier APK + DexClassLoader | Proposed |
diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
index 55b7562d7d..4124b0512e 100644
--- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
+++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
@@ -60,7 +60,6 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent
import com.itsaky.androidide.flashbar.Flashbar
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.api.ILanguageServer
-import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.DefinitionParams
import com.itsaky.androidide.lsp.models.DefinitionResult
@@ -95,6 +94,7 @@ import io.github.rosemoe.sora.widget.IDEEditorSearcher
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion
import io.github.rosemoe.sora.widget.component.EditorBuiltinComponent
import io.github.rosemoe.sora.widget.component.EditorTextActionWindow
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -1357,12 +1357,17 @@ open class IDEEditor
invokeOnCompletion { err -> logError(err, action) }
}
+ private fun isCancelled(err: Throwable?): Boolean {
+ err ?: return false
+ return err is CancellationException || isCancelled(err.cause)
+ }
+
private fun logError(
err: Throwable?,
action: String,
) {
err ?: return
- if (CancelChecker.isCancelled(err)) {
+ if (isCancelled(err)) {
log.warn("{} has been cancelled", action)
} else {
log.error("{} failed", action)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9c4e15649b..897fb631c7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -113,9 +113,11 @@ composite-constants = { module = "com.itsaky.androidide.build:constants" }
composite-desugaringCore = { module = "com.itsaky.androidide.build:desugaring-core" }
composite-fuzzysearch = { module = "com.itsaky.androidide.build:fuzzysearch" }
composite-googleJavaFormat = { module = "com.itsaky.androidide.build:google-java-format" }
+composite-javaCompiler = { module = "com.itsaky.androidide.build:java-compiler" }
composite-javac = { module = "com.itsaky.androidide.build:javac" }
composite-javapoet = { module = "com.itsaky.androidide.build:javapoet" }
composite-jaxp = { module = "com.itsaky.androidide.build:jaxp" }
+composite-jdkCompiler = { module = "com.itsaky.androidide.build:jdk-compiler" }
composite-jdt = { module = "com.itsaky.androidide.build:jdt" }
composite-layoutlibApi = { module = "com.itsaky.androidide.build:layoutlib-api" }
composite-treeview = { module = "com.itsaky.androidide.build:treeview" }
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..b5fbf9f0dd 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 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/java-api/build.gradle.kts b/lsp/java-api/build.gradle.kts
new file mode 100644
index 0000000000..3d4b783594
--- /dev/null
+++ b/lsp/java-api/build.gradle.kts
@@ -0,0 +1,38 @@
+/*
+ * 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 .
+ */
+
+import com.itsaky.androidide.build.config.BuildConfig
+
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ namespace = "${BuildConfig.PACKAGE_NAME}.lsp.java.api"
+}
+
+dependencies {
+ api(projects.lsp.api)
+ api(projects.lsp.models)
+ api(projects.shared)
+ api(projects.subprojects.projects)
+ api(projects.eventbusEvents)
+
+ implementation(projects.common)
+ implementation(libs.common.kotlin)
+}
diff --git a/lsp/java-api/src/main/AndroidManifest.xml b/lsp/java-api/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..91bff55d11
--- /dev/null
+++ b/lsp/java-api/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
diff --git a/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSession.kt b/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSession.kt
new file mode 100644
index 0000000000..80c783ddd9
--- /dev/null
+++ b/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSession.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.java.api
+
+import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
+import com.itsaky.androidide.lsp.models.CodeFormatResult
+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.ExpandSelectionParams
+import com.itsaky.androidide.lsp.models.FormatCodeParams
+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.models.Range
+import com.itsaky.androidide.projects.api.ModuleProject
+import com.itsaky.androidide.projects.api.Workspace
+import java.nio.file.Path
+
+/**
+ * Bridge to the isolated javac session, loaded lazily via `DexClassLoader` on the first
+ * real `.java`-file interaction (see `JavaCompilerLoader`) instead of being always resident
+ * in the main app dex. Exposes the LSP operations directly rather than a `getCompiler()`
+ * accessor, since the underlying `JavaCompilerService`/`Provider` types live entirely on
+ * the isolated side of the classloader boundary.
+ */
+interface IJavaCompilerSession : AutoCloseable {
+ /**
+ * Runs the deferred javac project reset for [workspace]: destroys the no-module and
+ * per-module compilers, clears file-manager and R.jar caches, and re-caches classpath
+ * locations for every submodule. Mirrors what `setupWithProject` used to do eagerly.
+ */
+ fun resetProject(workspace: Workspace)
+
+ /** Registers the Java code-actions menu. Deferred here 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()
+
+ fun complete(params: CompletionParams): CompletionResult
+
+ suspend fun findReferences(params: ReferenceParams): ReferenceResult
+
+ suspend fun findDefinition(params: DefinitionParams): DefinitionResult
+
+ suspend fun expandSelection(params: ExpandSelectionParams): Range
+
+ suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp
+
+ suspend fun analyze(file: Path): DiagnosticResult
+
+ fun onContentChange(event: DocumentChangeEvent)
+
+ /** Clears the diagnostics-analysis timestamp tracked for [file] when it's closed. */
+ fun onFileClosed(file: Path)
+
+ fun formatCode(params: FormatCodeParams?): CodeFormatResult
+
+ /**
+ * Handles a completion failure: distinguishes a genuine cancellation (checked here, not on
+ * the resident side, since the thrown exception type is only classloader-identity-safe to
+ * check from the same side that threw it) from a real error, destroying per-module compilers
+ * in the latter case so the next request rebuilds them. Always returns `true` (failure was
+ * handled), matching `ILanguageServer.handleFailure`'s contract.
+ */
+ fun handleCompletionFailure(error: Throwable?): Boolean
+
+ /**
+ * Resolves the source file path for [className] within [module], for the JDWP debugger's
+ * breakpoint/stack-frame-to-source mapping (`ModelUtils.asLspLocation`). Returns the raw
+ * path rather than a `SourceFileObject`, since that type lives only on this side of the
+ * classloader boundary.
+ */
+ fun findSourceFilePath(
+ module: ModuleProject,
+ className: String,
+ ): String?
+}
diff --git a/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSessionFactory.kt b/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSessionFactory.kt
new file mode 100644
index 0000000000..dd2889da62
--- /dev/null
+++ b/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSessionFactory.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.java.api
+
+import com.itsaky.androidide.projects.api.Workspace
+
+/**
+ * Entry point loaded by name (reflection) from the carrier APK's `DexClassLoader` -- see
+ * `JavaCompilerLoader`. Implemented by `JavaCompilerSessionFactoryImpl` in the isolated
+ * `lsp:java-compiler-impl` module, which must expose a public no-arg constructor for this
+ * to work.
+ */
+interface IJavaCompilerSessionFactory {
+ fun create(workspace: Workspace): IJavaCompilerSession
+}
diff --git a/lsp/java-compiler-impl/build.gradle.kts b/lsp/java-compiler-impl/build.gradle.kts
new file mode 100644
index 0000000000..77f7c52f06
--- /dev/null
+++ b/lsp/java-compiler-impl/build.gradle.kts
@@ -0,0 +1,116 @@
+/*
+ * 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 .
+ */
+
+import com.itsaky.androidide.build.config.BuildConfig
+
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+ id("kotlin-kapt")
+}
+
+android {
+ namespace = "${BuildConfig.PACKAGE_NAME}.lsp.java.impl"
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+}
+
+kapt {
+ arguments {
+ arg("eventBusIndex", "${BuildConfig.PACKAGE_NAME}.events.LspJavaImplEventsIndex")
+ }
+}
+
+dependencies {
+ kapt(projects.annotationProcessors)
+
+ // Resident (bundled in the main app dex via editor/editor-api/lsp:java/etc.) -- like the
+ // appcompat/material block below, `implementation` here would duplicate these, including
+ // their native .so payloads, into the isolated carrier dex alongside the identical resident
+ // copies, breaking type identity across the DexClassLoader boundary (see docs/adr/0012).
+ // The carrier's DexClassLoader resolves them from its parent (the resident classloader)
+ // instead.
+ compileOnly(libs.androidide.ts)
+ compileOnly(libs.androidide.ts.java)
+ implementation(platform(libs.sora.bom))
+ compileOnly(libs.common.editor)
+ implementation(libs.common.javaparser)
+ compileOnly(libs.androidx.annotation)
+ compileOnly(libs.google.guava)
+ compileOnly(libs.google.gson)
+ compileOnly(libs.androidx.core.ktx)
+ compileOnly(libs.common.kotlin)
+ // Resident (bundled via common's `api`) -- needed to translate CancelAbort (isolated-only,
+ // never classloader-identity-safe to check from resident code) into CancellationException
+ // (this same, single resident copy, safe to check from either side) before it crosses back
+ // out of JavaCompilerSessionImpl. compileOnly for the same reason as the block above.
+ compileOnly(libs.common.kotlin.coroutines.core)
+
+ // The actual javac fork -- this is the payload this module exists to isolate. NOT
+ // libs.composite.javac (the aggregate): that also pulls in java-compiler, which must stay
+ // resident-only (see docs/adr/0012) -- duplicating it here would break type identity
+ // across the DexClassLoader boundary for CacheFSInfo/Context/etc.
+ implementation(libs.composite.jdkCompiler)
+ implementation(projects.subprojects.javacServices)
+
+ // google-java-format uses javac's own parser internally, so -- like javac itself -- it's
+ // heavy and isolated, not resident: JavaServerSettings (resident) exposes only a plain
+ // code-style int, and CodeFormatProvider builds the real JavaFormatterOptions here.
+ implementation(libs.composite.googleJavaFormat)
+
+ // Resident (java-compiler: jdkx.*/zipfs2.* + the relocated fs-adjacent leaf classes) --
+ // this module's own source (SourceFileObject, etc.) references these types directly.
+ compileOnly(libs.composite.javaCompiler)
+
+ // javapoet (used by JavaPoetUtils.kt's code-generation actions) is lightweight and stays
+ // fully resident -- templates-api/templates-impl need it unconditionally for the "New
+ // Project" wizard, unrelated to javac. compileOnly here avoids duplicating it into the
+ // carrier alongside that resident copy.
+ compileOnly(libs.composite.javapoet)
+
+ // Resident modules, visible at compile time but never bundled into this module's own
+ // output -- `implementation` here would duplicate their classes into the isolated
+ // carrier dex alongside the identical resident copies, breaking type identity across
+ // the DexClassLoader boundary (see docs/adr/0012).
+ compileOnly(libs.androidx.appcompat)
+ compileOnly(libs.google.material)
+ compileOnly(projects.actions)
+ compileOnly(projects.common)
+ compileOnly(projects.editorApi)
+ compileOnly(projects.resources)
+ compileOnly(projects.idetooltips)
+ compileOnly(projects.lsp.api)
+ compileOnly(projects.lsp.java)
+ compileOnly(projects.lsp.javaApi)
+ compileOnly(projects.lsp.jvmSymbolIndex)
+ compileOnly(projects.subprojects.javacFs)
+ compileOnly(projects.subprojects.projects)
+
+ testImplementation(projects.testing.lsp)
+ // The moved tests construct/drive a resident JavaLanguageServer directly, and reference
+ // java-compiler types (jdkx.tools.Diagnostic, etc.) directly too; compileOnly (main
+ // sourceset) doesn't extend to the test compile classpath, so these need their own entries.
+ testImplementation(projects.lsp.java)
+ testImplementation(libs.composite.javaCompiler)
+ // JavaSelectionProviderTest references sora-editor's Content directly; same compileOnly
+ // test-classpath gap as above.
+ testImplementation(libs.common.editor)
+}
diff --git a/lsp/java-compiler-impl/src/main/AndroidManifest.xml b/lsp/java-compiler-impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..91bff55d11
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/AndroidManifest.xml
@@ -0,0 +1,18 @@
+
+
+
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt
similarity index 86%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt
index 82e42d24c5..5dfcaef5c9 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt
@@ -23,6 +23,8 @@ import java.util.concurrent.CancellationException
* Thrown when a compilation process is cancelled.
* @author Akash Yadav
*/
-class CompilationCancellationException @JvmOverloads constructor(
- override val cause: Throwable? = null
-) : CancellationException()
+class CompilationCancellationException
+ @JvmOverloads
+ constructor(
+ override val cause: Throwable? = null,
+ ) : CancellationException()
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java
new file mode 100644
index 0000000000..2fd3987fef
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java
@@ -0,0 +1,101 @@
+/*
+ * 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.java;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
+import com.itsaky.androidide.projects.api.ModuleProject;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides {@link JavaCompilerService} instances for different {@link ModuleProject}s.
+ *
+ * @author Akash Yadav
+ */
+public class JavaCompilerProvider {
+ private static final Logger logger = LoggerFactory.getLogger(JavaCompilerProvider.class);
+ private static JavaCompilerProvider sInstance;
+
+ @NonNull
+ public static JavaCompilerService get(ModuleProject module) {
+ return JavaCompilerProvider.getInstance().forModule(module);
+ }
+
+ public static JavaCompilerProvider getInstance() {
+ if (sInstance == null) {
+ sInstance = new JavaCompilerProvider();
+ }
+
+ return sInstance;
+ }
+
+ private final Map mCompilers = new ConcurrentHashMap<>();
+
+ private JavaCompilerProvider() {}
+
+ // TODO This currently destroys all the compiler instances
+ // We must have a method to destroy only the required instance in
+ // JavaLanguageServer.handleFailure(LSPFailure)
+ public synchronized void destroy() {
+ for (final JavaCompilerService compiler : mCompilers.values()) {
+ compiler.destroy();
+ }
+ mCompilers.clear();
+ }
+
+ /**
+ * Iterate over all available {@link JavaCompilerService} instances to perform given {@code action} function and return the result of the function.
+ *
+ * @param action
+ * The function to consume the {@link JavaCompilerService} instances and produce a result. The function can return {@code null} to indicate that no result was produced for the provided element. If the function returns a non-null value, the iteration is stopped and the non-null result is returned. If the function returns {@code null} for all elements, then {@code null} is returned.
+ * @return The result of the action.
+ * @param
+ * The type of the result produced by the given function.
+ */
+ @Nullable
+ public synchronized T find(Function action) {
+ logger.debug("find from {} compiler services", mCompilers.size());
+ for (JavaCompilerService service : mCompilers.values()) {
+ final var result = action.apply(service);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ public synchronized JavaCompilerService forModule(ModuleProject module) {
+ // A module instance is set to the compiler only in case the project is initialized or
+ // this method was called with other module instance.
+ final JavaCompilerService cached = mCompilers.get(module);
+ if (cached != null && cached.getModule() != null) {
+ return cached;
+ }
+
+ final JavaCompilerService newInstance = new JavaCompilerService(module);
+ mCompilers.put(module, newInstance);
+
+ return newInstance;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt
new file mode 100644
index 0000000000..69aab41d03
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.java.actions
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.ActionItem
+import com.itsaky.androidide.actions.EditorActionItem
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.lsp.api.ILanguageClient
+import com.itsaky.androidide.lsp.api.ILanguageServerRegistry
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.JavaLanguageServer
+import com.itsaky.androidide.lsp.java.R
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.rewrite.Rewrite
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.utils.DocumentUtils
+import com.itsaky.androidide.utils.ILogger
+import com.itsaky.androidide.utils.flashError
+import java.io.File
+
+/**
+ * Base class for java code actions
+ *
+ * @author Akash Yadav
+ */
+abstract class BaseJavaCodeAction : EditorActionItem {
+ override var visible: Boolean = true
+ override var enabled: Boolean = true
+ override var icon: Drawable? = null
+ override var requiresUIThread: Boolean = false
+ override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS
+
+ protected abstract val titleTextRes: Int
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+ if (
+ !data.hasRequiredData(Context::class.java, JavaLanguageServer::class.java, File::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ if (titleTextRes != -1) {
+ label = data[Context::class.java]!!.getString(titleTextRes)
+ }
+
+ val file = data.requireFile()
+ val isJava = DocumentUtils.isJavaFile(file.toPath())
+ val module = IProjectManager.getInstance().findModuleForFile(file, false)
+
+ visible = isJava
+ enabled = isJava && module != null
+ }
+
+ fun performCodeAction(
+ data: ActionData,
+ result: Rewrite,
+ ) {
+ val compiler = data.requireCompiler()
+
+ val actions =
+ try {
+ result.asCodeActions(compiler, label)
+ } catch (e: Exception) {
+ flashError(e.cause?.message ?: e.message)
+ ILogger.ROOT.error(e.cause?.message ?: e.message, e)
+ return
+ }
+
+ if (actions == null) {
+ onPerformCodeActionFailed(data)
+ return
+ }
+
+ data.getLanguageClient()?.performCodeAction(actions)
+ }
+
+ protected open fun onPerformCodeActionFailed(data: ActionData) {
+ flashError(R.string.msg_codeaction_failed)
+ }
+
+ protected fun ActionData.requireLanguageServer(): JavaLanguageServer =
+ ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID)
+ as JavaLanguageServer
+
+ protected fun ActionData.getLanguageClient(): ILanguageClient? = requireLanguageServer().client
+
+ protected fun ActionData.requireCompiler(): JavaCompilerService {
+ val module = IProjectManager.getInstance().findModuleForFile(requireFile(), false)
+ requireNotNull(module) {
+ "Cannot get compiler instance. Unable to find module for file: ${requireFile().name}"
+ }
+ return JavaCompilerProvider.get(module)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/FieldBasedAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/FieldBasedAction.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/FieldBasedAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/FieldBasedAction.kt
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt
similarity index 65%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt
index 2f5fd243b0..785844152d 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt
@@ -1,55 +1,54 @@
-/*
- * 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.java.actions.common
-
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.hasRequiredData
-import com.itsaky.androidide.actions.markInvisible
-import com.itsaky.androidide.editor.api.ILspEditor
-import com.itsaky.androidide.idetooltips.TooltipTag
-import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
-import com.itsaky.androidide.resources.R
-import io.github.rosemoe.sora.widget.CodeEditor
-import java.io.File
-
-/**
- * Action that allows the user to find references to a variable, field, method or class.
- *
- * @author Akash Yadav
- */
-class FindReferencesAction : BaseJavaCodeAction() {
-
- override val titleTextRes: Int = R.string.action_find_references
- override val id: String = "ide.editor.lsp.java.findReferences"
- override var label: String = ""
- override var requiresUIThread: Boolean = true
- override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIND_REFS
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
-
- if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
- markInvisible()
- return
- }
- }
-
- override suspend fun execAction(data: ActionData): Any {
- val editor = data[CodeEditor::class.java]!!
- return (editor as? ILspEditor)?.findReferences() ?: false
- }
-}
+/*
+ * 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.java.actions.common
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.editor.api.ILspEditor
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.resources.R
+import io.github.rosemoe.sora.widget.CodeEditor
+import java.io.File
+
+/**
+ * Action that allows the user to find references to a variable, field, method or class.
+ *
+ * @author Akash Yadav
+ */
+class FindReferencesAction : BaseJavaCodeAction() {
+ override val titleTextRes: Int = R.string.action_find_references
+ override val id: String = "ide.editor.lsp.java.findReferences"
+ override var label: String = ""
+ override var requiresUIThread: Boolean = true
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIND_REFS
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val editor = data[CodeEditor::class.java]!!
+ return (editor as? ILspEditor)?.findReferences() ?: false
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt
similarity index 65%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt
index 90c6d7a8bf..236b76141f 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt
@@ -1,56 +1,55 @@
-/*
- * 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.java.actions.common
-
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.hasRequiredData
-import com.itsaky.androidide.actions.markInvisible
-import com.itsaky.androidide.editor.api.ILspEditor
-import com.itsaky.androidide.idetooltips.TooltipTag
-import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
-import com.itsaky.androidide.resources.R
-import io.github.rosemoe.sora.widget.CodeEditor
-import java.io.File
-
-/**
- * Action that allows the user to navigate to the definition of a variable, field, method, class,
- * etc.
- *
- * @author Akash Yadav
- */
-class GoToDefinitionAction : BaseJavaCodeAction() {
-
- override val titleTextRes: Int = R.string.action_goto_definition
- override val id: String = "ide.editor.lsp.java.gotoDefinition"
- override var label: String = ""
- override var requiresUIThread: Boolean = true
- override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_GOTO_DEF
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
-
- if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
- markInvisible()
- return
- }
- }
-
- override suspend fun execAction(data: ActionData): Any {
- val editor = data[CodeEditor::class.java]!!
- return (editor as? ILspEditor)?.findDefinition() ?: false
- }
-}
+/*
+ * 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.java.actions.common
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.editor.api.ILspEditor
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.resources.R
+import io.github.rosemoe.sora.widget.CodeEditor
+import java.io.File
+
+/**
+ * Action that allows the user to navigate to the definition of a variable, field, method, class,
+ * etc.
+ *
+ * @author Akash Yadav
+ */
+class GoToDefinitionAction : BaseJavaCodeAction() {
+ override val titleTextRes: Int = R.string.action_goto_definition
+ override val id: String = "ide.editor.lsp.java.gotoDefinition"
+ override var label: String = ""
+ override var requiresUIThread: Boolean = true
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_GOTO_DEF
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible || !data.hasRequiredData(CodeEditor::class.java, File::class.java)) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val editor = data[CodeEditor::class.java]!!
+ return (editor as? ILspEditor)?.findDefinition() ?: false
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt
new file mode 100644
index 0000000000..39178856c2
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt
@@ -0,0 +1,90 @@
+package com.itsaky.androidide.lsp.java.actions.common
+
+import com.google.googlejavaformat.java.FormatterException
+import com.google.googlejavaformat.java.ImportOrderer
+import com.google.googlejavaformat.java.JavaFormatterOptions
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireEditor
+import com.itsaky.androidide.editor.api.IEditor
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.JavaLanguageServer
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.JavaServerSettings
+import com.itsaky.androidide.resources.R.string
+import io.github.rosemoe.sora.widget.CodeEditor
+import org.slf4j.LoggerFactory
+
+class OrganizeImportsAction : BaseJavaCodeAction() {
+ override val id: String = "lsp_java_organizeImports"
+ override var label: String = ""
+ override val titleTextRes: Int = string.action_organize_imports
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_ORGANIZE_IMPORTS
+
+ companion object {
+ private val log = LoggerFactory.getLogger(OrganizeImportsAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+ if (!visible) {
+ return
+ }
+
+ if (!data.hasRequiredData(CodeEditor::class.java)) {
+ markInvisible()
+ return
+ }
+
+ visible = true
+ enabled = true
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val watch =
+ com.itsaky.androidide.utils
+ .StopWatch("Organize imports")
+ return try {
+ val editor = data.requireEditor()
+ val content = editor.text
+ val server = data[JavaLanguageServer::class.java]
+ val settings = server!!.settings as JavaServerSettings
+ val style =
+ if (settings.codeStyle == JavaServerSettings.CODE_STYLE_AOSP) {
+ JavaFormatterOptions.Style.AOSP
+ } else {
+ JavaFormatterOptions.Style.GOOGLE
+ }
+ val output = ImportOrderer.reorderImports(content.toString(), style)
+ watch.log()
+ output
+ } catch (e: FormatterException) {
+ log.error("Failed to reorder imports", e)
+ false
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ super.postExec(data, result)
+ if (result is String) {
+ if (result.isNotEmpty()) {
+ val editor = data.requireEditor()
+ val cursor = editor.cursor.left()
+
+ editor.text.apply {
+ val endLine = getLine(lineCount - 1)
+ replace(0, 0, lineCount - 1, endLine.length + endLine.lineSeparator.length, result)
+ }
+
+ (editor as? IEditor?)?.also {
+ it.setSelectionAround(cursor)
+ editor.ensureSelectionVisible()
+ }
+ }
+ }
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt
new file mode 100644
index 0000000000..03ab2f7f72
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt
@@ -0,0 +1,65 @@
+package com.itsaky.androidide.lsp.java.actions.common
+
+import com.google.googlejavaformat.java.FormatterException
+import com.google.googlejavaformat.java.RemoveUnusedImports
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireEditor
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.resources.R.string
+import io.github.rosemoe.sora.widget.CodeEditor
+import org.slf4j.LoggerFactory
+
+class RemoveUnusedImportsAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.removeUnusedImports"
+ override var label: String = ""
+ override val titleTextRes: Int = string.action_remove_unused_imports
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_UNUSED_IMPORTS
+
+ companion object {
+ private val log = LoggerFactory.getLogger(RemoveUnusedImportsAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+ if (!visible) {
+ return
+ }
+
+ if (!data.hasRequiredData(CodeEditor::class.java)) {
+ markInvisible()
+ return
+ }
+
+ visible = true
+ enabled = true
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val watch =
+ com.itsaky.androidide.utils
+ .StopWatch("Remove unused imports")
+ return try {
+ val editor = data.requireEditor()
+ val content = editor.text
+ val output = RemoveUnusedImports.removeUnusedImports(content.toString())
+ watch.log()
+ output
+ } catch (e: FormatterException) {
+ log.error("Failed to remove unused imports", e)
+ false
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result is String && result.isNotEmpty()) {
+ val editor = data.requireEditor()
+ editor.setText(result)
+ }
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt
new file mode 100644
index 0000000000..73c342d3cf
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.google.common.collect.Iterables.toArray
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.newDialogBuilder
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.AddImport
+import com.itsaky.androidide.lsp.java.rewrite.Rewrite
+import com.itsaky.androidide.lsp.models.CodeActionItem
+import com.itsaky.androidide.lsp.models.DiagnosticItem
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import jdkx.tools.Diagnostic
+import jdkx.tools.JavaFileObject
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class AddImportAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.addImport"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.NOT_IMPORTED.id
+
+ override val titleTextRes: Int = R.string.action_import_classes
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
+
+ companion object {
+ private val log = LoggerFactory.getLogger(AddImportAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible || !data.hasRequiredData(DiagnosticItem::class.java)) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data.get(DiagnosticItem::class.java)!!
+ if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
+ markInvisible()
+ return
+ }
+
+ val file = data.requireFile()
+ val module =
+ IProjectManager.getInstance().findModuleForFile(file, false)
+ ?: run {
+ markInvisible()
+ return
+ }
+
+ val compiler = JavaCompilerProvider.get(module)
+
+ @Suppress("UNCHECKED_CAST")
+ val jcDiagnostic =
+ JavaDiagnosticUtils.asJCDiagnostic(diagnostic.extra as Diagnostic)
+ if (jcDiagnostic == null) {
+ markInvisible()
+ return
+ }
+
+ val found =
+ jcDiagnostic.args[1]?.toString()?.let { compiler.findQualifiedNames(it, true).isNotEmpty() }
+ ?: false
+
+ visible = found
+ enabled = found
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ @Suppress("UNCHECKED_CAST")
+ val diagnostic =
+ JavaDiagnosticUtils.asUnwrapper(
+ data.get(DiagnosticItem::class.java)!!.extra as Diagnostic,
+ )!!
+ val file = data.requireFile()
+ val module =
+ IProjectManager.getInstance().findModuleForFile(file, false)
+ ?: run {
+ markInvisible()
+ return Any()
+ }
+
+ val compiler = JavaCompilerProvider.get(module)
+
+ val titles = mutableListOf()
+ val rewrites = mutableListOf()
+ val simpleName = diagnostic.d.args[1]
+ for (name in compiler.publicTopLevelTypes()) {
+ var klass = name
+ if (klass.contains('/')) {
+ klass = klass.replace('/', '.')
+ }
+
+ if (!klass.endsWith(".$simpleName")) {
+ continue
+ }
+
+ titles.add(klass)
+ rewrites.add(AddImport(data.requirePath(), klass))
+ }
+
+ if (rewrites.isEmpty()) {
+ return false
+ }
+
+ return Pair(titles, rewrites)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is Pair<*, *>) {
+ return
+ }
+
+ val file = data.requireFile()
+ val module =
+ IProjectManager.getInstance().findModuleForFile(file, false)
+ ?: run {
+ markInvisible()
+ return
+ }
+
+ val compiler = JavaCompilerProvider.get(module)
+ val client = data.getLanguageClient() ?: return
+ val actions = mutableListOf()
+ val titles = result.first as List
+ val rewrites = result.second as List
+
+ for (index in rewrites.indices) {
+ val name = titles[index]
+ val rewrite = rewrites[index]
+ rewrite.asCodeActions(compiler, name)?.let { actions.add(it) }
+ }
+
+ when (actions.size) {
+ 0 -> {
+ log.warn("No rewrites found. Cannot perform action")
+ }
+
+ 1 -> {
+ client.performCodeAction(actions[0])
+ }
+
+ else -> {
+ val builder = newDialogBuilder(data)
+ builder.setTitle(label)
+ builder.setItems(toArray(titles, String::class.java)) { d, w ->
+ d.dismiss()
+ client.performCodeAction(actions[w])
+ }
+ builder.show()
+ }
+ }
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt
new file mode 100644
index 0000000000..d40f32a3f2
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.AddException
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
+import com.itsaky.androidide.lsp.models.DiagnosticItem
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class AddThrowsAction : BaseJavaCodeAction() {
+ override val id = "ide.editor.lsp.java.diagnostics.addThrows"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.NOT_THROWN.id
+
+ override val titleTextRes: Int = R.string.action_add_throws
+
+ companion object {
+ private val log = LoggerFactory.getLogger(AddThrowsAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible || !data.hasRequiredData(DiagnosticItem::class.java)) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+ return compiler.compile(file).get { task ->
+ val needsThrow = CodeActionUtils.findMethod(task, diagnostic.range)
+ val exceptionName = CodeActionUtils.extractExceptionName(diagnostic.message)
+ return@get AddException(
+ needsThrow.className,
+ needsThrow.methodName,
+ needsThrow.erasedParameterTypes,
+ exceptionName,
+ )
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is AddException) {
+ log.warn("Unable to add 'throws' expression")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt
new file mode 100644
index 0000000000..ceb79db253
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.requireContext
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.R
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.utils.positionForImports
+import com.itsaky.androidide.lsp.models.CodeActionItem
+import com.itsaky.androidide.lsp.models.CodeActionKind
+import com.itsaky.androidide.lsp.models.DocumentChange
+import com.itsaky.androidide.lsp.models.TextEdit
+import com.itsaky.androidide.models.Range
+import com.itsaky.androidide.utils.DialogUtils
+import com.itsaky.androidide.utils.flashInfo
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+
+/**
+ * Analyzes the source file for unresolved names and tries to import all of them at once.
+ *
+ * @author Akash Yadav
+ */
+class AutoFixImportsAction : BaseJavaCodeAction() {
+ override val titleTextRes: Int = R.string.title_fix_imports
+ override val id: String = "ide.editor.lsp.java.diagnostics.autoFixImports"
+ override var label: String = ""
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
+
+ companion object {
+ private val log = LoggerFactory.getLogger(AutoFixImportsAction::class.java)
+ }
+
+ override suspend fun execAction(data: ActionData): Result {
+ val path = data.requirePath()
+ val compiler = data.requireCompiler()
+ return compiler.compile(path).get { task ->
+ val classes = mutableMapOf>()
+
+ // find all unresolved simple names
+ unresolvedNames(path, task).forEach { simpleName ->
+
+ // if we have already looked for this simple name
+ // we do not need to look it up again
+ if (classes[simpleName] != null) return@forEach
+
+ // find classes with those names
+ compiler.findQualifiedNames(simpleName).let { names ->
+
+ // if we find classes with that specific simple name, map them to the simple name
+ if (names.isNotEmpty()) {
+ classes[simpleName] = names
+ }
+ }
+ }
+
+ // return the result
+ Result(getFileImports(task, path), classes)
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is Result) {
+ log.error("Invalid result returned from execAction: {}", result)
+ return
+ }
+
+ if (result.classes.isEmpty()) {
+ flashInfo(R.string.msg_no_unresolved_classes)
+ return
+ }
+
+ // if there are multiple classes with same simple name
+ // ask the user to choose the appropriate class
+ if (result.classes.any { it.value.size > 1 }) {
+ finalizeClassNames(data, result)
+ } else {
+ performEdits(data, result)
+ }
+ }
+
+ private fun finalizeClassNames(
+ data: ActionData,
+ result: Result,
+ ) {
+ var e: Map.Entry>? = null
+ for (entry in result.classes) {
+ if (entry.value.size > 1) {
+ e = entry
+ break
+ }
+ }
+
+ if (e == null) {
+ performEdits(data, result)
+ return
+ }
+
+ val context = data.requireContext()
+ DialogUtils
+ .newMaterialDialogBuilder(context)
+ .setCancelable(true)
+ .setItems(e.value.toTypedArray()) { dialog, which ->
+ dialog.dismiss()
+ result.classes[e.key] = listOf(e.value[which])
+
+ // once the user decides which class to import for this simple name,
+ // call this method again to see if there any other simple names with multiple options
+ finalizeClassNames(data, result)
+ }.setTitle(context.getString(R.string.title_class_chooser, e.key))
+ .show()
+ }
+
+ private fun performEdits(
+ data: ActionData,
+ result: Result,
+ ) {
+ val path = data.requirePath()
+ val compiler = data.requireCompiler()
+ val client =
+ data.getLanguageClient()
+ ?: run {
+ log.warn("No language client found. Cannot perform edits.")
+ return
+ }
+
+ val classes = result.classes.mapNotNull { it.value.firstOrNull() }
+
+ if (classes.isEmpty()) {
+ flashInfo(R.string.msg_no_unresolved_classes)
+ return
+ }
+
+ val insertText = StringBuilder()
+ if (result.fileImports.isEmpty() && classes.isNotEmpty()) {
+ // if there are no file imports, the new imports will be added just after the package
+ // declaration. To avoid this, add a new line before the imports
+ insertText.append("\n")
+ }
+
+ for (klass in classes) {
+ insertText.append("import $klass;\n")
+ }
+
+ val position = compiler.compile(path).get { positionForImports(classes[0], it) }
+
+ val change = DocumentChange()
+ change.file = path
+ change.edits = listOf(TextEdit(Range.pointRange(position), insertText.toString()))
+
+ val action = CodeActionItem()
+ action.title = data.requireContext().getString(R.string.title_fix_imports)
+ action.kind = CodeActionKind.QuickFix
+ action.changes = listOf(change)
+ client.performCodeAction(action)
+ }
+
+ /**
+ * Walks through the diagnostics of the compilation task, looks for [DiagnosticCode.NOT_IMPORTED]
+ * errors and returns a list of simple names of all not imported classes.
+ */
+ private fun unresolvedNames(
+ file: Path,
+ task: CompileTask,
+ ): List {
+ val names = mutableListOf()
+ var docContents: CharSequence? = null
+ val diagnostics =
+ task.diagnostics.filter {
+ it.source.toUri() == file.toUri() && it.code == DiagnosticCode.NOT_IMPORTED.id
+ }
+ for (diagnostic in diagnostics) {
+ val content =
+ try {
+ docContents ?: diagnostic.source.getCharContent(true).also { docContents = it }
+ } catch (e: Exception) {
+ log.error("Failed to get contents of file {}", file, e)
+ continue
+ }
+
+ val name =
+ content.subSequence(diagnostic.startPosition.toInt(), diagnostic.endPosition.toInt())
+ names.add(name.toString())
+ }
+ return names
+ }
+
+ private fun getFileImports(
+ task: CompileTask,
+ file: Path,
+ ): Set =
+ task
+ .root(file)
+ .imports
+ .map {
+ it.qualifiedIdentifier
+ }.map { it.toString() }
+ .toSet()
+
+ inner class Result(
+ val fileImports: Set,
+ val classes: MutableMap>,
+ )
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt
similarity index 50%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt
index 75181aeac8..6350f8bebc 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt
@@ -1,84 +1,84 @@
-/*
- * 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.java.actions.diagnostics
-
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.hasRequiredData
-import com.itsaky.androidide.actions.markInvisible
-import com.itsaky.androidide.actions.requireFile
-import com.itsaky.androidide.actions.requirePath
-import com.itsaky.androidide.lsp.java.JavaCompilerProvider
-import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
-import com.itsaky.androidide.lsp.java.models.DiagnosticCode
-import com.itsaky.androidide.lsp.java.rewrite.CreateMissingMethod
-import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
-import com.itsaky.androidide.projects.IProjectManager
-import com.itsaky.androidide.resources.R
-import org.slf4j.LoggerFactory
-
-/** @author Akash Yadav */
-class CreateMissingMethodAction : BaseJavaCodeAction() {
-
- override val id: String = "ide.editor.lsp.java.diagnostics.createMissingMethod"
- override var label: String = ""
- private val diagnosticCode = DiagnosticCode.MISSING_METHOD.id
-
- override val titleTextRes: Int = R.string.action_create_missing_method
-
- companion object {
-
- private val log = LoggerFactory.getLogger(CreateMissingMethodAction::class.java)
- }
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
-
- if (
- !visible ||
- !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
- ) {
- markInvisible()
- return
- }
-
- val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
- if (diagnosticCode != diagnostic.code) {
- markInvisible()
- return
- }
- }
-
- override suspend fun execAction(data: ActionData): Any {
- val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
- val compiler =
- JavaCompilerProvider.get(
- IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
- val file = data.requirePath()
- return compiler.compile(file).get {
- CreateMissingMethod(file, findPosition(it, diagnostic.range.start))
- }
- }
-
- override fun postExec(data: ActionData, result: Any) {
- if (result !is CreateMissingMethod) {
- log.warn("Unable to create missing method")
- return
- }
-
- performCodeAction(data, result)
- }
-}
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.CreateMissingMethod
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class CreateMissingMethodAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.createMissingMethod"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.MISSING_METHOD.id
+
+ override val titleTextRes: Int = R.string.action_create_missing_method
+
+ companion object {
+ private val log = LoggerFactory.getLogger(CreateMissingMethodAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (
+ !visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+ return compiler.compile(file).get {
+ CreateMissingMethod(file, findPosition(it, diagnostic.range.start))
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is CreateMissingMethod) {
+ log.warn("Unable to create missing method")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt
similarity index 52%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt
index 62bf0b0f33..086b9566da 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt
@@ -1,89 +1,89 @@
-/*
- * 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.java.actions.diagnostics
-
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.hasRequiredData
-import com.itsaky.androidide.actions.markInvisible
-import com.itsaky.androidide.actions.requireFile
-import com.itsaky.androidide.actions.requirePath
-import com.itsaky.androidide.idetooltips.TooltipTag
-import com.itsaky.androidide.lsp.java.JavaCompilerProvider
-import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
-import com.itsaky.androidide.lsp.java.models.DiagnosticCode
-import com.itsaky.androidide.lsp.java.rewrite.ConvertFieldToBlock
-import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
-import com.itsaky.androidide.lsp.models.DiagnosticItem
-import com.itsaky.androidide.projects.IProjectManager
-import com.itsaky.androidide.resources.R
-import org.slf4j.LoggerFactory
-
-/** @author Akash Yadav */
-class FieldToBlockAction : BaseJavaCodeAction() {
-
- override val id: String = "ide.editor.lsp.java.diagnostics.fieldToBlock"
- override var label: String = ""
- private val diagnosticCode = DiagnosticCode.UNUSED_FIELD.id
- override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
-
- override val titleTextRes: Int = R.string.action_convert_to_block
-
- companion object {
-
- private val log = LoggerFactory.getLogger(FieldToBlockAction::class.java)
- }
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
-
- if (!visible) {
- return
- }
-
- if (!data.hasRequiredData(DiagnosticItem::class.java)) {
- markInvisible()
- return
- }
-
- val diagnostic = data.get(DiagnosticItem::class.java)!!
- if (diagnosticCode != diagnostic.code) {
- markInvisible()
- return
- }
- }
-
- override suspend fun execAction(data: ActionData): Any {
- val compiler =
- JavaCompilerProvider.get(
- IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
- val diagnostic = data[DiagnosticItem::class.java]!!
- val file = data.requirePath()
-
- return compiler.compile(file).get {
- ConvertFieldToBlock(file, findPosition(it, diagnostic.range.start))
- }
- }
-
- override fun postExec(data: ActionData, result: Any) {
- if (result !is ConvertFieldToBlock) {
- log.warn("Unable to convert field to block")
- return
- }
-
- performCodeAction(data, result)
- }
-}
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.ConvertFieldToBlock
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
+import com.itsaky.androidide.lsp.models.DiagnosticItem
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class FieldToBlockAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.fieldToBlock"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNUSED_FIELD.id
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
+
+ override val titleTextRes: Int = R.string.action_convert_to_block
+
+ companion object {
+ private val log = LoggerFactory.getLogger(FieldToBlockAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible) {
+ return
+ }
+
+ if (!data.hasRequiredData(DiagnosticItem::class.java)) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data.get(DiagnosticItem::class.java)!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val diagnostic = data[DiagnosticItem::class.java]!!
+ val file = data.requirePath()
+
+ return compiler.compile(file).get {
+ ConvertFieldToBlock(file, findPosition(it, diagnostic.range.start))
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is ConvertFieldToBlock) {
+ log.warn("Unable to convert field to block")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt
new file mode 100644
index 0000000000..3c98c55f95
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.ImplementAbstractMethods
+import com.itsaky.androidide.resources.R
+import jdkx.tools.Diagnostic
+import jdkx.tools.JavaFileObject
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class ImplementAbstractMethodsAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.implementAbstractMethods"
+ override var label: String = ""
+ private var diagnosticCode = DiagnosticCode.DOES_NOT_OVERRIDE_ABSTRACT.id
+
+ override val titleTextRes: Int = R.string.action_implement_abstract_methods
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_OVERRIDE_SUPER
+
+ companion object {
+ private val log = LoggerFactory.getLogger(ImplementAbstractMethodsAction::class.java)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible) {
+ return
+ }
+
+ if (!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
+ if (diagnosticCode != diagnostic.code || diagnostic.extra !is Diagnostic<*>) {
+ markInvisible()
+ return
+ }
+
+ JavaDiagnosticUtils.asJCDiagnostic(diagnostic.extra as Diagnostic)
+ ?: run {
+ markInvisible()
+ return
+ }
+
+ visible = true
+ enabled = true
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic =
+ JavaDiagnosticUtils.asJCDiagnostic(
+ data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!.extra as Diagnostic,
+ )
+ return ImplementAbstractMethods(diagnostic!!)
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is ImplementAbstractMethods) {
+ log.warn("Unable to perform action. Invalid result from execAction(..)")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt
similarity index 51%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt
index 221e5354e7..09da5a5ad7 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt
@@ -1,84 +1,84 @@
-/*
- * 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.java.actions.diagnostics
-
-import com.itsaky.androidide.actions.ActionData
-import com.itsaky.androidide.actions.hasRequiredData
-import com.itsaky.androidide.actions.markInvisible
-import com.itsaky.androidide.actions.requireFile
-import com.itsaky.androidide.actions.requirePath
-import com.itsaky.androidide.lsp.java.JavaCompilerProvider
-import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
-import com.itsaky.androidide.lsp.java.models.DiagnosticCode
-import com.itsaky.androidide.lsp.java.rewrite.RemoveClass
-import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
-import com.itsaky.androidide.projects.IProjectManager
-import com.itsaky.androidide.resources.R
-import org.slf4j.LoggerFactory
-
-/** @author Akash Yadav */
-class RemoveClassAction : BaseJavaCodeAction() {
-
- override val id: String = "ide.editor.lsp.java.diagnostics.removeClass"
- override var label: String = ""
- private val diagnosticCode = DiagnosticCode.UNUSED_CLASS.id
-
- override val titleTextRes: Int = R.string.action_remove_class
-
- companion object {
-
- private val log = LoggerFactory.getLogger(RemoveClassAction::class.java)
- }
-
- override fun prepare(data: ActionData) {
- super.prepare(data)
-
- if (!visible || !data.hasRequiredData(
- com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
- ) {
- markInvisible()
- return
- }
-
- val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
- if (diagnosticCode != diagnostic.code) {
- markInvisible()
- return
- }
- }
-
- override suspend fun execAction(data: ActionData): Any {
- val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
- val compiler =
- JavaCompilerProvider.get(
- IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
- val file = data.requirePath()
-
- return compiler.compile(file).get {
- RemoveClass(file, findPosition(it, diagnostic.range.start))
- }
- }
-
- override fun postExec(data: ActionData, result: Any) {
- if (result !is RemoveClass) {
- log.warn("Unable to remove class")
- return
- }
-
- performCodeAction(data, result)
- }
-}
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.RemoveClass
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class RemoveClassAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.removeClass"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNUSED_CLASS.id
+
+ override val titleTextRes: Int = R.string.action_remove_class
+
+ companion object {
+ private val log = LoggerFactory.getLogger(RemoveClassAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+
+ return compiler.compile(file).get {
+ RemoveClass(file, findPosition(it, diagnostic.range.start))
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is RemoveClass) {
+ log.warn("Unable to remove class")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt
new file mode 100644
index 0000000000..1500fb5068
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.RemoveMethod
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findMethod
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class RemoveMethodAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.removeMethod"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNUSED_METHOD.id
+
+ override val titleTextRes: Int = R.string.action_remove_method
+
+ companion object {
+ private val log = LoggerFactory.getLogger(RemoveMethodAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+
+ return compiler.compile(file).get {
+ val unusedMethod = findMethod(it, diagnostic.range)
+ RemoveMethod(
+ unusedMethod.className,
+ unusedMethod.methodName,
+ unusedMethod.erasedParameterTypes,
+ )
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is RemoveMethod) {
+ log.warn("Unable to remove method")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt
new file mode 100644
index 0000000000..acfbd5a64a
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.RemoveException
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class RemoveUnusedThrowsAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.removeUnusedThrows"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNUSED_THROWS.id
+
+ override val titleTextRes: Int = R.string.action_remove_unused_throws
+
+ companion object {
+ private val log = LoggerFactory.getLogger(RemoveUnusedThrowsAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (
+ !visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val d = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+ return compiler.compile(file).get { task ->
+ val notThrown = CodeActionUtils.extractNotThrownExceptionName(d.message)
+ val methodWithExtraThrow = CodeActionUtils.findMethod(task, d.range)
+
+ return@get RemoveException(
+ methodWithExtraThrow.className,
+ methodWithExtraThrow.methodName,
+ methodWithExtraThrow.erasedParameterTypes,
+ notThrown,
+ )
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is RemoveException) {
+ log.warn("Unable to remove unused throws")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt
new file mode 100644
index 0000000000..5d03260274
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.AddSuppressWarningAnnotation
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class SuppressUncheckedWarningAction : BaseJavaCodeAction() {
+ override val id = "ide.editor.lsp.java.diagnostics.suppressUncheckedWarning"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNCHECKED.id
+
+ override val titleTextRes: Int = R.string.action_suppress_unchecked_warning
+
+ companion object {
+ private val log = LoggerFactory.getLogger(SuppressUncheckedWarningAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+ return compiler.compile(file).get { task ->
+ val warnedMethod = CodeActionUtils.findMethod(task, diagnostic.range)
+ return@get AddSuppressWarningAnnotation(
+ warnedMethod.className,
+ warnedMethod.methodName,
+ warnedMethod.erasedParameterTypes,
+ )
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is AddSuppressWarningAnnotation) {
+ log.warn("Unable to suppress 'unchecked' warning")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt
new file mode 100644
index 0000000000..c87a107b62
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.java.actions.diagnostics
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.ConvertVariableToStatement
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils.findPosition
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class VariableToStatementAction : BaseJavaCodeAction() {
+ override val id: String = "ide.editor.lsp.java.diagnostics.variableToStatement"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.UNUSED_LOCAL.id
+
+ override val titleTextRes: Int = R.string.action_convert_to_statement
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
+
+ companion object {
+ private val log = LoggerFactory.getLogger(VariableToStatementAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (!visible) {
+ return
+ }
+
+ if (!data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+
+ visible = true
+ enabled = true
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val path = data.requirePath()
+
+ return compiler.compile(path).get {
+ ConvertVariableToStatement(path, findPosition(it, diagnostic.range.start))
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is ConvertVariableToStatement) {
+ log.warn("Unable to convert variable to statement")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateConstructorAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateConstructorAction.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateConstructorAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateConstructorAction.kt
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt
new file mode 100644
index 0000000000..9c533c8b52
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.java.actions.generators
+
+import com.itsaky.androidide.actions.ActionData
+import com.itsaky.androidide.actions.hasRequiredData
+import com.itsaky.androidide.actions.markInvisible
+import com.itsaky.androidide.actions.requireFile
+import com.itsaky.androidide.actions.requirePath
+import com.itsaky.androidide.idetooltips.TooltipTag
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.rewrite.GenerateRecordConstructor
+import com.itsaky.androidide.lsp.java.utils.CodeActionUtils
+import com.itsaky.androidide.projects.IProjectManager
+import com.itsaky.androidide.resources.R
+import org.slf4j.LoggerFactory
+
+/** @author Akash Yadav */
+class GenerateMissingConstructorAction : BaseJavaCodeAction() {
+ override val id = "ide.editor.lsp.java.generator.missingConstructor"
+ override var label: String = ""
+ private val diagnosticCode = DiagnosticCode.MISSING_CONSTRUCTOR.id
+ override val titleTextRes: Int = R.string.action_generate_missing_constructor
+ override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_GEN_CONSTRUCTOR
+
+ companion object {
+ private val log = LoggerFactory.getLogger(GenerateMissingConstructorAction::class.java)
+ }
+
+ override fun prepare(data: ActionData) {
+ super.prepare(data)
+
+ if (
+ !visible ||
+ !data.hasRequiredData(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)
+ ) {
+ markInvisible()
+ return
+ }
+
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ if (diagnosticCode != diagnostic.code) {
+ markInvisible()
+ return
+ }
+ }
+
+ override suspend fun execAction(data: ActionData): Any {
+ val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
+ val compiler =
+ JavaCompilerProvider.get(IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
+ val file = data.requirePath()
+ return compiler.compile(file).get { task ->
+ val needsConstructor =
+ CodeActionUtils.findClassNeedingConstructor(task, diagnostic.range) ?: return@get false
+ return@get GenerateRecordConstructor(needsConstructor)
+ }
+ }
+
+ override fun postExec(
+ data: ActionData,
+ result: Any,
+ ) {
+ if (result !is GenerateRecordConstructor) {
+ log.warn("Unable to generate constructor")
+ return
+ }
+
+ performCodeAction(data, result)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateSettersAndGettersAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateSettersAndGettersAction.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateSettersAndGettersAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateSettersAndGettersAction.kt
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateToStringMethodAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateToStringMethodAction.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateToStringMethodAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateToStringMethodAction.kt
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/OverrideSuperclassMethodsAction.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/OverrideSuperclassMethodsAction.kt
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/OverrideSuperclassMethodsAction.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/OverrideSuperclassMethodsAction.kt
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt
similarity index 75%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt
index 04f32e1ee8..f86965afc9 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt
@@ -19,7 +19,7 @@ package com.itsaky.androidide.lsp.java.compiler
import openjdk.source.tree.CompilationUnitTree
import openjdk.tools.javac.api.JavacTaskImpl
-import java.util.function.*
+import java.util.function.Consumer
/**
* A compilation task processor process the [JavacTaskImpl]. Usually, a processor decides what files
@@ -28,11 +28,13 @@ import java.util.function.*
* @author Akash Yadav
*/
fun interface CompilationTaskProcessor {
-
- /**
- * Process the given [JavacTaskImpl]. The processor is responsible for parsing and analyzing the
- * task. For each parsed [CompilationUnitTree], [processCompilationUnit] must be called.
- */
- @Throws(Throwable::class)
- fun process(task: JavacTaskImpl, processCompilationUnit: Consumer)
+ /**
+* Process the given [JavacTaskImpl]. The processor is responsible for parsing and analyzing the
+* task. For each parsed [CompilationUnitTree], [processCompilationUnit] must be called.
+*/
+ @Throws(Throwable::class)
+ fun process(
+ task: JavacTaskImpl,
+ processCompilationUnit: Consumer,
+ )
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java
new file mode 100644
index 0000000000..c7fe74b6ab
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java
@@ -0,0 +1,73 @@
+/*
+ * 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.java.compiler;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
+import java.nio.file.Path;
+import java.util.List;
+import jdkx.tools.Diagnostic;
+import jdkx.tools.JavaFileObject;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.tools.javac.api.JavacTaskImpl;
+
+public class CompileTask implements AutoCloseable {
+
+ public final JavacTaskImpl task;
+ public final List roots;
+ public final List> diagnostics;
+ public final CompileBatch compileBatch;
+ public final DiagnosticListenerImpl diagnosticListener;
+
+ public CompileTask(
+ @NonNull CompileBatch compileBatch, List> diagnostics) {
+ this.compileBatch = compileBatch;
+ this.task = compileBatch.task;
+ this.roots = compileBatch.roots;
+ this.diagnostics = diagnostics;
+ this.diagnosticListener = compileBatch.diagnosticListener;
+ }
+
+ @Override
+ public void close() {}
+
+ public CompilationUnitTree root() {
+ if (roots.size() != 1) {
+ throw new RuntimeException("No compilation units found. Roots: " + roots.size());
+ }
+ return roots.get(0);
+ }
+
+ public CompilationUnitTree root(JavaFileObject file) {
+ for (CompilationUnitTree root : roots) {
+ if (root.getSourceFile().toUri().equals(file.toUri())) {
+ return root;
+ }
+ }
+ throw new RuntimeException("Compilation unit not found");
+ }
+
+ public CompilationUnitTree root(Path file) {
+ for (CompilationUnitTree root : roots) {
+ if (root.getSourceFile().toUri().equals(file.toUri())) {
+ return root;
+ }
+ }
+ throw new RuntimeException("Compilation unit not found");
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java
similarity index 54%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java
index db23b4ee77..58920adca9 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java
@@ -30,37 +30,37 @@
import jdkx.tools.JavaFileObject;
public interface CompilerProvider {
- Path NOT_FOUND = Paths.get("");
+ Path NOT_FOUND = Paths.get("");
- TreeSet publicTopLevelTypes();
+ default SynchronizedTask compile(Collection extends JavaFileObject> sources) {
+ return compile(new CompilationRequest(sources));
+ }
- TreeSet packagePrivateTopLevelTypes(String packageName);
+ SynchronizedTask compile(CompilationRequest request);
- Optional findAnywhere(String className);
+ default SynchronizedTask compile(Path... files) {
+ return compile(Arrays.stream(files).map(SourceFileObject::new).collect(Collectors.toList()));
+ }
- Path findTypeDeclaration(String className);
+ Optional findAnywhere(String className);
- Path[] findTypeReferences(String className);
+ Path[] findMemberReferences(String className, String memberName);
- Path[] findMemberReferences(String className, String memberName);
+ default List findQualifiedNames(String simpleName) {
+ return findQualifiedNames(simpleName, false);
+ }
- default List findQualifiedNames(String simpleName) {
- return findQualifiedNames(simpleName, false);
- }
+ List findQualifiedNames(String simpleName, boolean onlyOne);
- List findQualifiedNames(String simpleName, boolean onlyOne);
+ Path findTypeDeclaration(String className);
- ParseTask parse(Path file);
+ Path[] findTypeReferences(String className);
- ParseTask parse(JavaFileObject file);
+ TreeSet packagePrivateTopLevelTypes(String packageName);
- default SynchronizedTask compile(Path... files) {
- return compile(Arrays.stream(files).map(SourceFileObject::new).collect(Collectors.toList()));
- }
+ ParseTask parse(JavaFileObject file);
- default SynchronizedTask compile(Collection extends JavaFileObject> sources) {
- return compile(new CompilationRequest(sources));
- }
+ ParseTask parse(Path file);
- SynchronizedTask compile(CompilationRequest request);
+ TreeSet publicTopLevelTypes();
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt
similarity index 76%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt
index bdd340baee..81a7561ccf 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt
@@ -28,20 +28,22 @@ import java.util.function.Consumer
* @author Akash Yadav
*/
class DefaultCompilationTaskProcessor : CompilationTaskProcessor {
+ override fun process(
+ task: JavacTaskImpl,
+ processCompilationUnit: Consumer,
+ ) {
+ val watch = StopWatch("Process compilation task")
+ val trees = task.parse()
+ watch.lapFromLast("Parsed treees")
- override fun process(task: JavacTaskImpl, processCompilationUnit: Consumer) {
- val watch = StopWatch("Process compilation task")
- val trees = task.parse()
- watch.lapFromLast("Parsed treees")
+ trees.forEach(processCompilationUnit::accept)
+ watch.lapFromLast("Processed trees")
- trees.forEach(processCompilationUnit::accept)
- watch.lapFromLast("Processed trees")
-
// val entered = JavacTaskUtil.enterTrees(task, trees)
// watch.lapFromLast("Entered trees")
//
// val analyzed = JavacTaskUtil.analyze(task, entered)
- task.analyze()
- watch.lapFromLast("Analyzed all trees")
- }
+ task.analyze()
+ watch.lapFromLast("Analyzed all trees")
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt
similarity index 86%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt
index ca821b9cc0..f1fdc7c597 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt
@@ -27,10 +27,8 @@ import com.itsaky.androidide.javac.services.compiler.ReusableContext
* @author Akash Yadav
*/
class JCReusableCompiler : ReusableCompiler() {
-
- override fun onCreateContext(): ReusableContext {
- return super.onCreateContext().also {
- JavaCompilerImpl.preRegister(context = it, replace = true)
- }
- }
+ override fun onCreateContext(): ReusableContext =
+ super.onCreateContext().also {
+ JavaCompilerImpl.preRegister(context = it, replace = true)
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt
similarity index 68%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt
index 11d8ad1c45..b7fe68513b 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt
@@ -28,27 +28,28 @@ import openjdk.tools.javac.util.Context
* @property completionInfo Information about the completion
* @author Akash Yadav
*/
-class JavaCompilerConfig(context: Context) {
- init {
- context.put(compilerConfigKey, this)
- }
-
- var files: Collection? = null
- var completionInfo: CompletionInfo? = null
-
- companion object {
-
- @JvmField val compilerConfigKey = Context.Key()
-
- @JvmStatic
- fun instance(context: Context): JavaCompilerConfig {
- var instance = context.get(compilerConfigKey)
- if (instance == null) {
- instance = JavaCompilerConfig(context)
- }
- return instance
- }
- }
+class JavaCompilerConfig(
+ context: Context,
+) {
+ init {
+ context.put(compilerConfigKey, this)
+ }
+
+ var files: Collection? = null
+ var completionInfo: CompletionInfo? = null
+
+ companion object {
+ @JvmField val compilerConfigKey = Context.Key()
+
+ @JvmStatic
+ fun instance(context: Context): JavaCompilerConfig {
+ var instance = context.get(compilerConfigKey)
+ if (instance == null) {
+ instance = JavaCompilerConfig(context)
+ }
+ return instance
+ }
+ }
}
/**
@@ -57,4 +58,6 @@ class JavaCompilerConfig(context: Context) {
* @property cursor The cursor position for the completion.
* @author Akash Yadav
*/
-data class CompletionInfo(val cursor: Position)
+data class CompletionInfo(
+ val cursor: Position,
+)
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt
new file mode 100644
index 0000000000..4e086453ca
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.java.compiler
+
+import com.itsaky.androidide.javac.services.compiler.ReusableContext
+import com.itsaky.androidide.javac.services.compiler.ReusableJavaCompiler
+import com.itsaky.androidide.lsp.java.parser.ts.TSJavaParser
+import com.itsaky.androidide.lsp.java.parser.ts.TSMethodPruner.prune
+import com.itsaky.androidide.projects.FileManager
+import com.itsaky.androidide.utils.VMUtils
+import com.itsaky.androidide.utils.withStopWatch
+import jdkx.tools.JavaFileObject
+import jdkx.tools.JavaFileObject.Kind.SOURCE
+import openjdk.tools.javac.api.ClientCodeWrapper
+import openjdk.tools.javac.tree.JCTree.JCCompilationUnit
+import openjdk.tools.javac.util.Context
+import kotlin.io.path.name
+
+class JavaCompilerImpl(
+ context: Context?,
+) : ReusableJavaCompiler(context) {
+ override fun parse(
+ filename: JavaFileObject?,
+ content: CharSequence?,
+ ): JCCompilationUnit {
+ if (VMUtils.isJvm) {
+ return super.parse(filename, content)
+ }
+
+ val file = ClientCodeWrapper.instance(context).unwrap(filename)
+ val compilerConfig = JavaCompilerConfig.instance(context)
+
+ // Preconditions
+ if (
+ content == null ||
+ compilerConfig.files == null ||
+ filename?.kind != SOURCE ||
+ compilerConfig.files?.contains(file) == false
+ ) {
+ return super.parse(filename, content)
+ }
+
+ // If the file is NOT being parsed for a completion request,
+ // we should not prune method bodies of active documents
+ if (compilerConfig.completionInfo == null && FileManager.isActive(filename.toUri())) {
+ return super.parse(filename, content)
+ }
+
+ val pruned =
+ withStopWatch("${if (file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch ->
+ val contentBuilder = StringBuilder(content)
+
+ // TSJavaParser.parse() returns a result owned by its own LRU cache (see
+ // TSParseCache), which closes the tree on eviction -- closing it here too,
+ // e.g. via .use{}, would double-close it and use-after-free it on the next
+ // cache hit for this file.
+ val parseResult = TSJavaParser.parse(file)
+
+ prune(
+ contentBuilder,
+ parseResult.tree,
+ compilerConfig.completionInfo?.cursor?.index ?: -1,
+ )
+
+ watch.log()
+
+ contentBuilder
+ }
+
+ return super.parse(filename, pruned)
+ }
+
+ companion object {
+ @JvmStatic
+ fun preRegister(
+ context: ReusableContext,
+ replace: Boolean = false,
+ ) {
+ if (replace) {
+ context.drop(compilerKey)
+ }
+ context.put(compilerKey, Context.Factory { JavaCompilerImpl(it) })
+ }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerService.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerService.java
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerService.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerService.java
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionFactoryImpl.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionFactoryImpl.kt
new file mode 100644
index 0000000000..295684a083
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionFactoryImpl.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.java.compiler
+
+import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession
+import com.itsaky.androidide.lsp.java.api.IJavaCompilerSessionFactory
+import com.itsaky.androidide.projects.api.Workspace
+
+/**
+ * Loaded by name (reflection) via `JavaCompilerLoader` -- must keep a public no-arg
+ * constructor. Does not reset the project itself: `JavaLanguageServer.ensureProjectReset()`
+ * always calls `resetProject` right after getting a session, whether newly created or
+ * already existing (e.g. after a project switch), so resetting here too would be redundant.
+ */
+class JavaCompilerSessionFactoryImpl : IJavaCompilerSessionFactory {
+ override fun create(workspace: Workspace): IJavaCompilerSession = JavaCompilerSessionImpl()
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionImpl.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionImpl.kt
new file mode 100644
index 0000000000..3c48189621
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionImpl.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.java.compiler
+
+import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
+import com.itsaky.androidide.javac.services.CancelAbort
+import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider
+import com.itsaky.androidide.lsp.internal.model.CachedCompletion
+import com.itsaky.androidide.lsp.java.JavaCompilerProvider
+import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu
+import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession
+import com.itsaky.androidide.lsp.java.models.JavaServerSettings
+import com.itsaky.androidide.lsp.java.providers.CodeFormatProvider
+import com.itsaky.androidide.lsp.java.providers.CompletionProvider
+import com.itsaky.androidide.lsp.java.providers.DefinitionProvider
+import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider
+import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider
+import com.itsaky.androidide.lsp.java.providers.ReferenceProvider
+import com.itsaky.androidide.lsp.java.providers.SignatureProvider
+import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
+import com.itsaky.androidide.lsp.models.CodeFormatResult
+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.ExpandSelectionParams
+import com.itsaky.androidide.lsp.models.FormatCodeParams
+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.IProjectManager.Companion.getInstance
+import com.itsaky.androidide.projects.ProjectManagerImpl
+import com.itsaky.androidide.projects.api.ModuleProject
+import com.itsaky.androidide.projects.api.Workspace
+import jdkx.tools.JavaFileObject
+import kotlinx.coroutines.CancellationException
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+import java.util.Objects
+
+/**
+ * Implements [IJavaCompilerSession] on the isolated side of the DexClassLoader boundary --
+ * everything [JavaLanguageServer][com.itsaky.androidide.lsp.java.JavaLanguageServer]'s
+ * complete/findReferences/findDefinition/expandSelection/signatureHelp/analyze/onContentChange
+ * bodies used to do directly, before javac moved out of the main dex (ADFA-5053).
+ */
+class JavaCompilerSessionImpl : IJavaCompilerSession {
+ private val completionProvider = CompletionProvider()
+ private val diagnosticProvider = JavaDiagnosticProvider()
+ private var cachedCompletion: CachedCompletion = CachedCompletion.EMPTY
+
+ private val settings get() = JavaServerSettings.getInstance()
+
+ override fun resetProject(workspace: Workspace) {
+ JavaCompilerService.NO_MODULE_COMPILER.destroy()
+ SourceFileManager.clearCache()
+
+ // Clear cached JAR file system for R.jar. Using the cached instance will result in
+ // completions not being updated for updated resources.
+ // TODO Clearing caches for JAR files ending with '/R.jar' is probably not a good idea --
+ // maybe this could be improved by using data from the AndroidModule project model.
+ CachingJarFileSystemProvider.clearCachesForPaths { path: String -> path.endsWith("/R.jar") }
+
+ JavaCompilerProvider.getInstance().destroy()
+
+ for (subModule in workspace.subProjects) {
+ if (subModule !is ModuleProject || subModule.path == workspace.rootProject.path) {
+ continue
+ }
+ SourceFileManager.forModule(subModule)
+ }
+ }
+
+ override fun registerCodeActions() {
+ LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu)
+ }
+
+ override fun unregisterCodeActions() {
+ LSPEditorActions.ensureActionsMenuUnregistered(JavaCodeActionsMenu)
+ }
+
+ override fun close() {
+ // Mirrors resetProject(): NO_MODULE_COMPILER accumulates state via onContentChange()'s
+ // unconditional onDocumentChange() calls too, and each session now owns its own
+ // discardable DexClassLoader -- nothing can reach back to release it once replaced.
+ JavaCompilerService.NO_MODULE_COMPILER.destroy()
+ JavaCompilerProvider.getInstance().destroy()
+ SourceFileManager.clearCache()
+ }
+
+ private fun getCompiler(file: Path?): JavaCompilerService {
+ val module =
+ ProjectManagerImpl.getInstance().findModuleForFile(file ?: return JavaCompilerService.NO_MODULE_COMPILER)
+ ?: return JavaCompilerService.NO_MODULE_COMPILER
+ return JavaCompilerProvider.get(module)
+ }
+
+ override fun complete(params: CompletionParams): CompletionResult {
+ val compiler = getCompiler(params.file)
+ if (!completionProvider.canComplete(params.file)) {
+ return CompletionResult.EMPTY
+ }
+
+ if (diagnosticProvider.isAnalyzing()) {
+ log.warn("Cancelling source code analysis due to completion request")
+ diagnosticProvider.cancel()
+ }
+
+ completionProvider.reset(
+ compiler,
+ settings,
+ cachedCompletion,
+ ) { updated: CachedCompletion ->
+ Objects.requireNonNull(updated)
+ cachedCompletion = updated
+ }
+
+ return completionProvider.complete(params)
+ }
+
+ override suspend fun findReferences(params: ReferenceParams): ReferenceResult =
+ translatingCancelAbort {
+ val compiler = getCompiler(params.file)
+ ReferenceProvider(compiler, params.cancelChecker).findReferences(params)
+ }
+
+ override suspend fun findDefinition(params: DefinitionParams): DefinitionResult =
+ translatingCancelAbort {
+ val compiler = getCompiler(params.file)
+ DefinitionProvider(compiler, settings, params.cancelChecker).findDefinition(params)
+ }
+
+ override suspend fun expandSelection(params: ExpandSelectionParams): Range =
+ translatingCancelAbort {
+ val compiler = getCompiler(params.file)
+ JavaSelectionProvider(compiler).expandSelection(params)
+ }
+
+ override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp =
+ translatingCancelAbort {
+ val compiler = getCompiler(params.file)
+ SignatureProvider(compiler, params.cancelChecker).signatureHelp(params)
+ }
+
+ override suspend fun analyze(file: Path): DiagnosticResult = translatingCancelAbort { diagnosticProvider.analyze(file) }
+
+ /**
+ * javac's cancellation signal, [CancelAbort], is thrown deep inside the isolated fork
+ * (`NBAttr`/`NBParserFactory`/`NBEnter`/`NBMemberEnter` via `CancelService.abortIfCanceled`)
+ * and is only classloader-identity-safe to recognize on this, the side that threw it --
+ * resident callers (`JavaLanguageServer`, `IDEEditor`) can't `is CancelAbort` it, and
+ * previously silently lost that recognition entirely when it crossed back out unwrapped
+ * (they'd log a routine cancellation as a real failure). Translating it here, into
+ * [CancellationException] -- a single resident copy safe to check from either side, per
+ * `compileOnly(libs.common.kotlin.coroutines.core)` in this module's build file -- restores
+ * that recognition across the boundary.
+ */
+ private inline fun translatingCancelAbort(block: () -> T): T =
+ try {
+ block()
+ } catch (e: CancelAbort) {
+ throw CancellationException("javac operation was cancelled", e)
+ }
+
+ override fun onContentChange(event: DocumentChangeEvent) {
+ // TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
+ JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event)
+ val module = getInstance().findModuleForFile(event.changedFile)
+ if (module != null) {
+ JavaCompilerProvider.get(module).onDocumentChange(event)
+ }
+ }
+
+ override fun formatCode(params: FormatCodeParams?): CodeFormatResult = CodeFormatProvider(settings).format(params)
+
+ override fun handleCompletionFailure(error: Throwable?): Boolean {
+ if (isCancelled(error)) {
+ return true
+ }
+ JavaCompilerProvider.getInstance().destroy()
+ return true
+ }
+
+ override fun onFileClosed(file: Path) {
+ diagnosticProvider.clearTimestamp(file)
+ }
+
+ override fun findSourceFilePath(
+ module: ModuleProject,
+ className: String,
+ ): String? {
+ val fo = JavaCompilerProvider.get(module).findAnywhere(className).orElse(null) ?: return null
+ if (fo.kind != JavaFileObject.Kind.SOURCE || fo !is SourceFileObject) {
+ return null
+ }
+ return fo.name
+ }
+
+ companion object {
+ private val log = LoggerFactory.getLogger(JavaCompilerSessionImpl::class.java)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileManager.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileManager.java
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileManager.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileManager.java
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java
new file mode 100644
index 0000000000..e0e68d1f6a
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java
@@ -0,0 +1,186 @@
+/*
+ * 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.java.compiler;
+
+import com.google.common.base.MoreObjects;
+import com.itsaky.androidide.projects.FileManager;
+import com.itsaky.androidide.utils.DocumentUtils;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.file.Path;
+import java.time.Instant;
+import java.util.Objects;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.NestingKind;
+import jdkx.tools.JavaFileObject;
+
+public class SourceFileObject implements JavaFileObject {
+ private static Kind kindFromExtension(String name) {
+ for (Kind candidate : Kind.values()) {
+ if (name.endsWith(candidate.extension)) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ // toRealPath() resolves symlinks the same way Files.isSameFile() effectively does; falls back
+ // to a plain absolute+normalized path if the file doesn't exist yet (toRealPath requires it).
+ private static Path resolveCanonicalPath(Path path) {
+ try {
+ return path.toRealPath();
+ } catch (IOException e) {
+ return path.toAbsolutePath().normalize();
+ }
+ }
+
+ /** path is the absolute path to this file on disk */
+ final Path path;
+ /**
+ * Identity key for equals()/hashCode(), separate from {@link #path}: the two paths compared in equals() may be textually different (a symlink, or a relative vs. canonicalized form) yet refer to the same file, so both sides need to hash the same normalized value. Resolved once here rather than via a live {@code Files.isSameFile} check per comparison, which has no hash of its own to key off of.
+ */
+ private final Path canonicalPath;
+
+ /** contents is the text in this file, or null if we should use the text in FileStore */
+ String contents;
+
+ /** if contents is set, the modified time of contents */
+ Instant modified;
+
+ public SourceFileObject(Path path) {
+ this(path, null, Instant.EPOCH);
+ }
+
+ public SourceFileObject(Path path, String contents, Instant modified) {
+ if (!DocumentUtils.isJavaFile(path))
+ throw new RuntimeException(path + " is not a java source");
+ this.path = path;
+ this.canonicalPath = resolveCanonicalPath(path);
+ this.contents = contents;
+ this.modified = modified;
+ }
+
+ @Override
+ public boolean delete() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SourceFileObject)) {
+ return false;
+ }
+ final SourceFileObject that = (SourceFileObject) o;
+ return Objects.equals(this.canonicalPath, that.canonicalPath)
+ && Objects.equals(contents, that.contents)
+ && Objects.equals(modified, that.modified);
+ }
+
+ @Override
+ public Modifier getAccessLevel() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ if (contents != null) {
+ return contents;
+ }
+ return FileManager.INSTANCE.getDocumentContents(this.path);
+ }
+
+ @Override
+ public Kind getKind() {
+ String name = path.getFileName().toString();
+ return kindFromExtension(name);
+ }
+
+ @Override
+ public long getLastModified() {
+ if (contents != null) {
+ return modified.toEpochMilli();
+ }
+ return FileManager.INSTANCE.getLastModified(this.path).toEpochMilli();
+ }
+
+ @Override
+ public String getName() {
+ return path.toString();
+ }
+
+ @Override
+ public NestingKind getNestingKind() {
+ return null;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(canonicalPath, contents, modified);
+ }
+
+ @Override
+ public boolean isNameCompatible(String simpleName, Kind kind) {
+ return path.getFileName().toString().equals(simpleName + kind.extension);
+ }
+
+ @Override
+ public InputStream openInputStream() {
+ if (contents != null) {
+ byte[] bytes = contents.getBytes();
+ return new ByteArrayInputStream(bytes);
+ }
+ return FileManager.INSTANCE.getInputStream(path);
+ }
+
+ @Override
+ public OutputStream openOutputStream() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Reader openReader(boolean ignoreEncodingErrors) {
+ if (contents != null) {
+ return new StringReader(contents);
+ }
+ return FileManager.INSTANCE.getReader(path);
+ }
+
+ @Override
+ public Writer openWriter() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this).add("path", this.path.toString()).toString();
+ }
+
+ @Override
+ public URI toUri() {
+ return this.path.toAbsolutePath().toUri();
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt
new file mode 100644
index 0000000000..4e3d26bd95
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.java.compiler
+
+import com.itsaky.androidide.lsp.java.CompilationCancellationException
+import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
+import org.slf4j.LoggerFactory
+import java.util.concurrent.Semaphore
+
+class SynchronizedTask {
+ @Volatile
+ @PublishedApi
+ internal var isCompiling = false
+
+ @PublishedApi
+ internal val semaphore = Semaphore(1)
+
+ @PublishedApi
+ internal var task: CompileTask? = null
+ private set
+
+ companion object {
+ @PublishedApi
+ internal val log = LoggerFactory.getLogger(SynchronizedTask::class.java)
+ }
+
+ inline fun run(crossinline taskConsumer: (CompileTask) -> Unit) {
+ try {
+ semaphore.acquire()
+ } catch (e: InterruptedException) {
+ throw CompilationCancellationException(e)
+ }
+ try {
+ taskConsumer(task!!)
+ } catch (err: Throwable) {
+ if (!isCancelled(err)) {
+ log.error("An error occurred while working with compilation task", err)
+ }
+ throw err
+ } finally {
+ semaphore.release()
+ }
+ }
+
+ inline fun get(crossinline action: (CompileTask) -> T): T {
+ try {
+ semaphore.acquire()
+ } catch (e: InterruptedException) {
+ throw CompilationCancellationException(e)
+ }
+ return try {
+ action(task!!)
+ } catch (err: Throwable) {
+ if (!isCancelled(err)) {
+ log.error("An error occurred while working with compilation task", err)
+ }
+ throw err
+ } finally {
+ semaphore.release()
+ }
+ }
+
+ fun post(action: Runnable) = post { action.run() }
+
+ inline fun post(action: () -> Unit) {
+ try {
+ semaphore.acquire()
+ } catch (e: InterruptedException) {
+ throw CompilationCancellationException(e)
+ }
+ isCompiling = true
+ try {
+ if (task != null) {
+ task!!.close()
+ }
+ action()
+ } catch (err: Throwable) {
+ if (!isCancelled(err)) {
+ log.error("An error occurred", err)
+ }
+ throw err
+ } finally {
+ semaphore.release()
+ isCompiling = false
+ }
+ }
+
+ fun setTask(task: CompileTask?) {
+ this.task = task
+ }
+
+ @get:Synchronized
+ val isBusy: Boolean
+ get() = isCompiling || semaphore.availablePermits() == 0
+
+/**
+* **FOR INTERNAL USE ONLY!**
+*/
+ fun logStats() {
+ log.warn(
+ "[SynchronizedTask] isCompiling={} queuedLength={}",
+ isCompiling,
+ semaphore.queueLength,
+ )
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt
similarity index 86%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt
index 387d546cb4..d466a35601 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt
@@ -30,19 +30,21 @@ import java.nio.file.Path
*
* @author Akash Yadav
*/
-abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditHandler() {
-
+abstract class AdvancedJavaEditHandler(
+ protected val file: Path,
+) : BaseJavaEditHandler() {
override fun performEdits(
item: CompletionItem,
editor: CodeEditor,
text: Content,
line: Int,
column: Int,
- index: Int
+ index: Int,
) {
- val compiler = JavaCompilerProvider.get(
- IProjectManager.getInstance().findModuleForFile(file, false) ?: return
- )
+ val compiler =
+ JavaCompilerProvider.get(
+ IProjectManager.getInstance().findModuleForFile(file, false) ?: return,
+ )
performEdits(compiler, editor, item)
executeCommand(editor, item.command)
@@ -59,6 +61,6 @@ abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditH
abstract fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
- completionItem: CompletionItem
+ completionItem: CompletionItem,
)
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt
similarity index 94%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt
index 4f12c45336..ef93f68954 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt
@@ -29,8 +29,10 @@ import io.github.rosemoe.sora.widget.CodeEditor
* @author Akash Yadav
*/
open class BaseJavaEditHandler : 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/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt
similarity index 92%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt
index d78a8e673d..47b88d5cd9 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt
@@ -32,12 +32,14 @@ import java.nio.file.Path
* @param file The file in which this edit will be performed.
* @author Akash Yadav
*/
-class ClassImportEditHandler(val imports: Set, file: Path) : AdvancedJavaEditHandler(file) {
-
+class ClassImportEditHandler(
+ val imports: Set,
+ file: Path,
+) : AdvancedJavaEditHandler(file) {
override fun performEdits(
compiler: JavaCompilerService,
editor: CodeEditor,
- completionItem: CompletionItem
+ completionItem: CompletionItem,
) {
val data = completionItem.data as? ClassCompletionData ?: return
val className = data.className
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt
new file mode 100644
index 0000000000..d5c7e9ac8f
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.java.edits
+
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.utils.EditHelper
+import io.github.rosemoe.sora.widget.CodeEditor
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+
+/**
+ * Imports multiple classes at once.
+ *
+ * @param classes The fully qualified classnames to import.
+ * @param imported The current imports of the given file.
+ * @author Akash Yadav
+ */
+class MultipleClassImportEditHandler(
+ private val classes: Set,
+ private val imported: Set,
+ file: Path,
+) : AdvancedJavaEditHandler(file) {
+ companion object {
+ private val log = LoggerFactory.getLogger(MultipleClassImportEditHandler::class.java)
+ }
+
+ override fun performEdits(
+ compiler: JavaCompilerService,
+ editor: CodeEditor,
+ completionItem: com.itsaky.androidide.lsp.models.CompletionItem,
+ ) {
+ val edits = mutableListOf()
+ for (className in classes) {
+ try {
+ edits.addAll(EditHelper.addImportIfNeeded(compiler, file, imported, className))
+ } catch (err: Throwable) {
+ log.error("Unable to compute edits to perform import for class: {}", className)
+ }
+ }
+
+ // Each edit's position is computed independently against the same pre-edit AST, but
+ // RewriteHelper applies them in sequence against the same live buffer -- an earlier
+ // insertion shifts every line after it, invalidating a later edit's pre-computed
+ // position. Applying bottom-to-top instead avoids that: inserting at a lower line never
+ // shifts anything above it, so every not-yet-applied edit's position stays valid.
+ val orderedEdits = edits.sortedByDescending { it.range.start }
+ com.itsaky.androidide.lsp.util.RewriteHelper
+ .performEdits(orderedEdits, editor)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt
similarity index 81%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt
index e50cf4ef32..7cb011d8e2 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt
@@ -31,11 +31,11 @@ import java.util.function.Consumer
* @author Akash Yadav
*/
data class CompilationRequest
-@JvmOverloads
-constructor(
- @JvmField val sources: Collection,
- @JvmField val partialRequest: PartialReparseRequest? = null,
- @JvmField
- val compilationTaskProcessor: CompilationTaskProcessor = DefaultCompilationTaskProcessor(),
- @JvmField var configureContext: Consumer? = null
-)
+ @JvmOverloads
+ constructor(
+ @JvmField val sources: Collection,
+ @JvmField val partialRequest: PartialReparseRequest? = null,
+ @JvmField
+ val compilationTaskProcessor: CompilationTaskProcessor = DefaultCompilationTaskProcessor(),
+ @JvmField var configureContext: Consumer? = null,
+ )
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt
new file mode 100644
index 0000000000..f642a7fc58
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.java.models
+
+/**
+ * Diagnostic codes are unique IDs for java diagnostic types.
+ *
+ * @author Akash Yadav
+ */
+enum class DiagnosticCode(
+ val id: String,
+) {
+ // -------- Warnings generated by the IDE --------------
+
+/** Unused method parameter. */
+ UNUSED_PARAM("ide.java.unused.param"),
+
+/** Unused local variable. */
+ UNUSED_LOCAL("ide.java.unused.local"),
+
+/** Unused field. */
+ UNUSED_FIELD("ide.java.unused.field"),
+
+/** Unused method. */
+ UNUSED_METHOD("ide.java.unused.method"),
+
+/** Unused class. */
+ UNUSED_CLASS("ide.java.unused.class"),
+
+/** Exception not thrown in method body. */
+ UNUSED_THROWS("ide.java.unused.throws"),
+
+/** Unknown unused element. */
+ UNUSED_OTHER("ide.java.unused.other"),
+
+/** A block with no statements i.e. an empty block */
+ EMPTY_BLOCK("ide.java.empty.block"),
+
+// ------------ Compiler warnings and errors ------------
+ UNCHECKED("compiler.warn.unchecked.call.mbr.of.raw.type"),
+ DOES_NOT_OVERRIDE_ABSTRACT("compiler.err.does.not.override.abstract"),
+ NOT_IMPORTED("compiler.err.cant.resolve.location"),
+ NOT_THROWN("compiler.err.unreported.exception.need.to.catch.or.throw"),
+ MISSING_CONSTRUCTOR("compiler.err.var.not.initialized.in.default.constructor"),
+ MISSING_METHOD("compiler.err.cant.resolve.location.args"),
+ ;
+
+ companion object {
+ @JvmStatic
+ fun forId(id: String): DiagnosticCode = values().first { id == it.id }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt
similarity index 61%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt
index af0ea85203..9aa9668a15 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt
@@ -33,45 +33,42 @@ import com.itsaky.androidide.lsp.models.TextEdit
* @author Akash Yadav
*/
class JavaCompletionItem(
- label: String,
- detail: String,
- insertText: String?,
- insertTextFormat: InsertTextFormat?,
- sortText: String?,
- command: Command?,
- kind: CompletionItemKind,
- matchLevel: MatchLevel,
- additionalTextEdits: List?,
- data: ICompletionData?,
-
- // Override the default edit handler
- editHandler: IEditHandler = BaseJavaEditHandler()
-) :
- CompletionItem(
- label,
- detail,
- insertText,
- insertTextFormat,
- sortText,
- command,
- kind,
- matchLevel,
- additionalTextEdits,
- data,
- editHandler
- ) {
-
- constructor() :
- this(
- "", // label
- "", // detail
- null, // insertText
- null, // insertTextFormat
- null, // sortText
- null, // command
- CompletionItemKind.NONE, // kind
- MatchLevel.NO_MATCH, // match level
- ArrayList(), // additionalEdits
- null // data
- )
+ label: String,
+ detail: String,
+ insertText: String?,
+ insertTextFormat: InsertTextFormat?,
+ sortText: String?,
+ command: Command?,
+ kind: CompletionItemKind,
+ matchLevel: MatchLevel,
+ additionalTextEdits: List?,
+ data: ICompletionData?,
+ // Override the default edit handler
+ editHandler: IEditHandler = BaseJavaEditHandler(),
+) : CompletionItem(
+ label,
+ detail,
+ insertText,
+ insertTextFormat,
+ sortText,
+ command,
+ kind,
+ matchLevel,
+ additionalTextEdits,
+ data,
+ editHandler,
+ ) {
+ constructor() :
+ this(
+ "", // label
+ "", // detail
+ null, // insertText
+ null, // insertTextFormat
+ null, // sortText
+ null, // command
+ CompletionItemKind.NONE, // kind
+ MatchLevel.NO_MATCH, // match level
+ ArrayList(), // additionalEdits
+ null, // data
+ )
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt
similarity index 90%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt
index 8c66b9ab8a..874ebbad9f 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt
@@ -23,4 +23,7 @@ package com.itsaky.androidide.lsp.java.models
* @param cursor The position of the cursor (1-based).
* @author Akash Yadav
*/
-data class PartialReparseRequest(@JvmField val cursor: Long, @JvmField val contents: String)
+data class PartialReparseRequest(
+ @JvmField val cursor: Long,
+ @JvmField val contents: String,
+)
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt
similarity index 82%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt
index 714f4914a5..5b806c5ad2 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt
@@ -26,12 +26,11 @@ import jdkx.tools.JavaFileObject
* @author Akash Yadav
*/
interface IJavaParser : AutoCloseable {
-
- /**
- * Parses the contents of the given [JavaFileObject].
- *
- * @param file The [JavaFileObject] to parse.
- * @return The result of the parse.
- */
- fun parse(file: JavaFileObject): T
+ /**
+ * Parses the contents of the given [JavaFileObject].
+ *
+ * @param file The [JavaFileObject] to parse.
+ * @return The result of the parse.
+ */
+ fun parse(file: JavaFileObject): T
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java
similarity index 82%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java
index 86830877c1..ac46694cc4 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java
@@ -21,11 +21,11 @@
import openjdk.source.util.JavacTask;
public class ParseTask {
- public final JavacTask task;
- public final CompilationUnitTree root;
+ public final JavacTask task;
+ public final CompilationUnitTree root;
- public ParseTask(JavacTask task, CompilationUnitTree root) {
- this.task = task;
- this.root = root;
- }
-}
\ No newline at end of file
+ public ParseTask(JavacTask task, CompilationUnitTree root) {
+ this.task = task;
+ this.root = root;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java
new file mode 100644
index 0000000000..9cf963496b
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java
@@ -0,0 +1,243 @@
+/*
+ * 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.java.parser;
+
+import com.itsaky.androidide.lsp.java.compiler.SourceFileManager;
+import com.itsaky.androidide.lsp.java.compiler.SourceFileObject;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.projects.IProjectManager;
+import com.itsaky.androidide.projects.api.ModuleProject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jdkx.tools.JavaCompiler;
+import jdkx.tools.JavaFileObject;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.MemberSelectTree;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.JavacTask;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+import openjdk.tools.javac.api.JavacTool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Parser {
+
+ private static final JavaCompiler COMPILER = JavacTool.create();
+ private static SourceFileManager FILE_MANAGER = SourceFileManager.NO_MODULE;
+ private static final Logger LOG = LoggerFactory.getLogger(Parser.class);
+ private static Parser cachedParse;
+ private static long cachedModified = -1;
+
+ public static Parser parseFile(Path file) {
+ return parseJavaFileObject(new SourceFileObject(file));
+ }
+
+ public static Parser parseJavaFileObject(JavaFileObject file) {
+ if (needsParse(file)) {
+ loadParse(file);
+ }
+ return cachedParse;
+ }
+
+ public static Range range(JavacTask task, CharSequence contents, TreePath path) {
+ // Find start position
+ Trees trees = Trees.instance(task);
+ SourcePositions pos = trees.getSourcePositions();
+ CompilationUnitTree root = path.getCompilationUnit();
+ LineMap lines = root.getLineMap();
+ int start = (int) pos.getStartPosition(root, path.getLeaf());
+ int end = (int) pos.getEndPosition(root, path.getLeaf());
+
+ // If start is -1, give up
+ if (start == -1) {
+ LOG.warn("Couldn't locate `{}`", path.getLeaf());
+ return Range.NONE;
+ }
+ // If end is bad, guess based on start
+ if (end == -1) {
+ end = start + path.getLeaf().toString().length();
+ }
+
+ if (path.getLeaf() instanceof ClassTree) {
+ ClassTree cls = (ClassTree) path.getLeaf();
+
+ // If class has annotations, skip over them
+ if (!cls.getModifiers().getAnnotations().isEmpty()) {
+ start = (int) pos.getEndPosition(root, cls.getModifiers());
+ }
+
+ // Find position of class name
+ String name = cls.getSimpleName().toString();
+ start = indexOf(contents, name, start);
+ if (start == -1) {
+ LOG.warn("Couldn't find identifier `{}` in `{}`", name, path.getLeaf());
+ return Range.NONE;
+ }
+ end = start + name.length();
+ }
+ if (path.getLeaf() instanceof MethodTree) {
+ MethodTree method = (MethodTree) path.getLeaf();
+
+ // If method has annotations, skip over them
+ if (!method.getModifiers().getAnnotations().isEmpty()) {
+ start = (int) pos.getEndPosition(root, method.getModifiers());
+ }
+
+ // Find position of method name
+ String name = method.getName().toString();
+ if (name.equals("")) {
+ name = className(path);
+ }
+ start = indexOf(contents, name, start);
+ if (start == -1) {
+ LOG.warn("Couldn't find identifier `{}` in `{}`", name, path.getLeaf());
+ return Range.NONE;
+ }
+ end = start + name.length();
+ }
+ if (path.getLeaf() instanceof VariableTree) {
+ VariableTree field = (VariableTree) path.getLeaf();
+
+ // If field has annotations, skip over them
+ if (!field.getModifiers().getAnnotations().isEmpty()) {
+ start = (int) pos.getEndPosition(root, field.getModifiers());
+ }
+
+ // Find position of method name
+ String name = field.getName().toString();
+ start = indexOf(contents, name, start);
+ if (start == -1) {
+ LOG.warn("Couldn't find identifier `{}` in `{}`", name, path.getLeaf());
+ return Range.NONE;
+ }
+ end = start + name.length();
+ }
+ if (path.getLeaf() instanceof MemberSelectTree) {
+ MemberSelectTree member = (MemberSelectTree) path.getLeaf();
+ String name = member.getIdentifier().toString();
+ start = indexOf(contents, name, start);
+ if (start == -1) {
+ LOG.warn("Couldn't find identifier `{}` in `{}`", name, path.getLeaf());
+ return Range.NONE;
+ }
+ end = start + name.length();
+ }
+ int startLine = (int) lines.getLineNumber(start);
+ int startCol = (int) lines.getColumnNumber(start);
+ int endLine = (int) lines.getLineNumber(end);
+ int endCol = (int) lines.getColumnNumber(end);
+
+ return new Range(
+ new Position(startLine - 1, startCol - 1), new Position(endLine - 1, endCol - 1));
+ }
+
+ static String className(TreePath t) {
+ while (t != null) {
+ if (t.getLeaf() instanceof ClassTree) {
+ ClassTree cls = (ClassTree) t.getLeaf();
+ return cls.getSimpleName().toString();
+ }
+ t = t.getParentPath();
+ }
+ return "";
+ }
+
+ private static void ignoreError(jdkx.tools.Diagnostic extends JavaFileObject> __) {
+ // Too noisy, this only comes up in parse tasks which tend to be less important
+ // LOG.warning(err.getMessage(Locale.getDefault()));
+ }
+
+ private static int indexOf(CharSequence contents, String name, int start) {
+ Matcher matcher = Pattern.compile("\\b" + name + "\\b").matcher(contents);
+ if (matcher.find(start)) {
+ return matcher.start();
+ }
+ return -1;
+ }
+
+ private static void loadParse(JavaFileObject file) {
+ cachedParse = new Parser(file);
+ cachedModified = file.getLastModified();
+ }
+
+ private static boolean needsParse(JavaFileObject file) {
+ if (cachedParse == null) {
+ return true;
+ }
+ if (!cachedParse.file.equals(file)) {
+ return true;
+ }
+
+ return file.getLastModified() > cachedModified;
+ }
+
+ /**
+ * Create a task that compiles a single file
+ */
+ private static JavacTask singleFileTask(JavaFileObject file) {
+ final ModuleProject module = IProjectManager.getInstance()
+ .findModuleForFile(Paths.get(file.toUri()));
+ if (module != null) {
+ FILE_MANAGER = SourceFileManager.forModule(module);
+ }
+
+ return (JavacTask) COMPILER.getTask(
+ null,
+ FILE_MANAGER,
+ Parser::ignoreError,
+ Collections.emptyList(),
+ Collections.emptyList(),
+ Collections.singletonList(file));
+ }
+
+ public final JavaFileObject file;
+
+ public final String contents;
+
+ public final JavacTask task;
+
+ public final CompilationUnitTree root;
+
+ public final Trees trees;
+
+ private Parser(JavaFileObject file) {
+ this.file = file;
+ try {
+ this.contents = file.getCharContent(false).toString();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ this.task = singleFileTask(file);
+ try {
+ this.root = task.parse().iterator().next();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ this.trees = Trees.instance(task);
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt
new file mode 100644
index 0000000000..27bfc170c2
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.java.parser.ts
+
+import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent
+import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
+import com.itsaky.androidide.lsp.java.parser.IJavaParser
+import com.itsaky.androidide.treesitter.TSParser
+import com.itsaky.androidide.treesitter.java.TSLanguageJava
+import com.itsaky.androidide.utils.StopWatch
+import jdkx.tools.JavaFileObject
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.slf4j.LoggerFactory
+
+/**
+ * [IJavaParser] which uses tree sitter to parse source files.
+ *
+ * @author Akash Yadav
+ */
+object TSJavaParser : IJavaParser {
+ private val cache = TSParseCache(15) // cache 15 results at max
+
+ private var isClosed = false
+ private val parser = TSParser.create().also { it.language = TSLanguageJava.getInstance() }
+ get() {
+ check(!isClosed) { "${javaClass.simpleName} instance has been closed" }
+ return field
+ }
+
+ private val log = LoggerFactory.getLogger(TSJavaParser::class.java)
+
+ init {
+ EventBus.getDefault().register(this)
+ }
+
+ @Subscribe(threadMode = ThreadMode.ASYNC)
+ fun onFileDeleted(event: FileDeletionEvent) {
+ synchronized(this.cache) {
+ this.cache.remove(
+ event.file
+ .toPath()
+ .toAbsolutePath()
+ .toUri(),
+ )
+ }
+ }
+
+ @Subscribe(threadMode = ThreadMode.ASYNC)
+ fun onFileRenamed(event: FileRenameEvent) {
+ synchronized(this.cache) {
+ val existing =
+ this.cache.remove(
+ event.file
+ .toPath()
+ .toAbsolutePath()
+ .toUri(),
+ )
+ if (existing != null) {
+ this.cache.put(
+ event.newFile
+ .toPath()
+ .toAbsolutePath()
+ .toUri(),
+ existing,
+ )
+ }
+ }
+ }
+
+ override fun parse(file: JavaFileObject): TSParseResult {
+ check(file.kind == JavaFileObject.Kind.SOURCE) { "File must a source file object" }
+
+ synchronized(this.cache) {
+ val result = this.cache[file.toUri()]
+ if (result != null) {
+ if (result.fileModified == file.lastModified) {
+ // cache hit and cache modified == file modified
+ log.info("Using cached parse tree")
+ return result
+ }
+ // cache hit, but cache modified != file modified
+ // need to reparse
+ }
+ }
+
+ parser.reset()
+ val watch = StopWatch("[TreeSitter] Parsing")
+ val content = file.getCharContent(false).toString()
+ if (parser.isParsing) {
+ parser.requestCancellationAndWait()
+ }
+ val parseTree = parser.parseString(content)
+ watch.log()
+
+ val result = TSParseResult(file, parseTree)
+
+ synchronized(this.cache) { this.cache.put(result.uri, result) }
+
+ return result
+ }
+
+ override fun close() {
+ synchronized(this.cache) { this.cache.evictAll() }
+ parser.close()
+ EventBus.getDefault().unregister(this)
+ isClosed = true
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt
new file mode 100644
index 0000000000..208dc866ac
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.java.parser.ts
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryCursor
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.java.TSLanguageJava
+
+/**
+ * Helper class to prune method bodies in Java source code using.
+ *
+ * @author Akash Yadav
+ */
+object TSMethodPruner {
+ private const val METHOD_BODIES_QUERY = "(method_declaration body: (block) @method.body)"
+
+ fun prune(
+ content: StringBuilder,
+ tree: TSTree,
+ cursor: Int,
+ ) {
+ val root = tree.rootNode
+ TSQuery.create(TSLanguageJava.getInstance(), METHOD_BODIES_QUERY).use { query ->
+ check(query.canAccess()) { "Invalid method bodies query" }
+ TSQueryCursor.create().use { queryCursor ->
+ queryCursor.exec(query, root)
+
+ var match: TSQueryMatch? = queryCursor.nextMatch()
+ while (match != null) {
+ val capture = match.captures[0]
+ val start = capture.node.startByte / 2
+ val end = capture.node.endByte / 2
+
+ if (cursor in start until end) {
+ // cursor is located in this method, so do not prune
+ match = queryCursor.nextMatch()
+ continue
+ }
+
+ // +1 and -1 to avoid removing the curly braces from the body
+ eraseRegion(content, start + 1, end - 1)
+ match = queryCursor.nextMatch()
+ }
+ }
+ }
+ }
+
+ private fun eraseRegion(
+ content: StringBuilder,
+ start: Int,
+ end: Int,
+ ) {
+ for (i in start until end) {
+ if (!content[i].isWhitespace()) {
+ content.setCharAt(i, ' ')
+ }
+ }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt
similarity index 76%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt
index bc09de9837..f3897e2d27 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt
@@ -25,15 +25,16 @@ import java.net.URI
*
* @author Akash Yadav
*/
-class TSParseCache(maxSize: Int) : LruCache(maxSize) {
-
- override fun entryRemoved(
- evicted: Boolean,
- key: URI,
- oldValue: TSParseResult,
- newValue: TSParseResult?
- ) {
- // Release the tree instance
- oldValue.tree.close()
- }
+class TSParseCache(
+ maxSize: Int,
+) : LruCache(maxSize) {
+ override fun entryRemoved(
+ evicted: Boolean,
+ key: URI,
+ oldValue: TSParseResult,
+ newValue: TSParseResult?,
+ ) {
+ // Release the tree instance
+ oldValue.tree.close()
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt
similarity index 82%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt
index 870d494b0d..de9ff182a3 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt
@@ -26,11 +26,14 @@ import java.net.URI
*
* @author Akash Yadav
*/
-class TSParseResult(file: JavaFileObject, val tree: TSTree) : AutoCloseable {
- val uri: URI = file.toUri()
- val fileModified: Long = file.lastModified
+class TSParseResult(
+ file: JavaFileObject,
+ val tree: TSTree,
+) : AutoCloseable {
+ val uri: URI = file.toUri()
+ val fileModified: Long = file.lastModified
- override fun close() {
- tree.close()
- }
+ override fun close() {
+ tree.close()
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt
similarity index 97%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt
index 84950ef873..3b47c7b205 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt
@@ -32,9 +32,8 @@ import java.nio.file.Path
abstract class BaseJavaServiceProvider(
protected val file: Path,
protected val compiler: JavaCompilerService,
- protected val settings: IServerSettings
+ protected val settings: IServerSettings,
) {
-
/** Abort the completion if cancelled. */
fun abortCompletionIfCancelled() {
ProgressManager.abortIfCancelled()
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt
similarity index 88%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt
index acdd03aa2a..ed39071d06 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt
@@ -24,5 +24,6 @@ import com.itsaky.androidide.progress.ICancelChecker
*
* @author Akash Yadav
*/
-abstract class CancelableServiceProvider(cancelChecker: ICancelChecker) :
- ICancelChecker by cancelChecker
\ No newline at end of file
+abstract class CancelableServiceProvider(
+ cancelChecker: ICancelChecker,
+) : ICancelChecker by cancelChecker
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java
new file mode 100644
index 0000000000..0d7b746de6
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java
@@ -0,0 +1,115 @@
+/*
+ * 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.java.providers;
+
+import static com.google.common.collect.Range.closedOpen;
+
+import androidx.annotation.NonNull;
+import com.google.common.collect.ImmutableList;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import com.google.googlejavaformat.java.JavaFormatterOptions;
+import com.google.googlejavaformat.java.Replacement;
+import com.itsaky.androidide.lsp.api.IServerSettings;
+import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
+import com.itsaky.androidide.lsp.models.CodeFormatResult;
+import com.itsaky.androidide.lsp.models.FormatCodeParams;
+import com.itsaky.androidide.lsp.models.IndexedTextEdit;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.utils.StopWatch;
+import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Formats Java code using Google Java Format.
+ *
+ * @author Akash Yadav
+ */
+public class CodeFormatProvider {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CodeFormatProvider.class);
+
+ private final JavaServerSettings settings;
+
+ public CodeFormatProvider(IServerSettings settings) {
+ assert settings instanceof JavaServerSettings;
+ this.settings = (JavaServerSettings) settings;
+ }
+
+ public CodeFormatResult format(FormatCodeParams params) {
+ try {
+ final StopWatch watch = new StopWatch("Code formatting");
+ final String content = params.getContent().toString();
+ final JavaFormatterOptions.Style style = settings.getCodeStyle() == JavaServerSettings.CODE_STYLE_AOSP
+ ? JavaFormatterOptions.Style.AOSP
+ : JavaFormatterOptions.Style.GOOGLE;
+ final Formatter formatter = new Formatter(JavaFormatterOptions.builder().formatJavadoc(true).style(style).build());
+
+ if (params.getRange() == Range.NONE) {
+ String formatted;
+ try {
+ formatted = formatter.formatSource(content);
+ } catch (FormatterException e) {
+ e.printStackTrace();
+ formatted = content;
+ }
+ return CodeFormatResult.forWholeContent(content, formatted);
+ }
+
+ final Collection> ranges = getCharRanges(content, params.getRange());
+
+ final ImmutableList replacements = formatter.getFormatReplacements(content, ranges);
+
+ watch.log();
+ return createResult(replacements);
+ } catch (Throwable e) {
+ LOG.error("Failed to format code.", e);
+ return CodeFormatResult.NONE;
+ }
+ }
+
+ private CodeFormatResult createResult(final ImmutableList replacements) {
+ final CodeFormatResult result = new CodeFormatResult(true);
+ for (final Replacement replacement : replacements) {
+ final com.google.common.collect.Range range = replacement.getReplaceRange();
+ final IndexedTextEdit edit = new IndexedTextEdit();
+ edit.setNewText(replacement.getReplacementString());
+ edit.setStart(range.lowerEndpoint());
+ edit.setEnd(range.upperEndpoint());
+ result.getIndexedTextEdits().add(edit);
+ }
+ return result;
+ }
+
+ @NonNull
+ private Collection> getCharRanges(
+ final String content, @NonNull final Range range) {
+
+ int start, end;
+ if (range == Range.NONE) {
+ start = 0;
+ end = content.length();
+ } else {
+ start = range.getStart().requireIndex();
+ end = range.getEnd().requireIndex();
+ }
+
+ return ImmutableList.of(closedOpen(start, end));
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CompletionProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CompletionProvider.java
similarity index 100%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CompletionProvider.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CompletionProvider.java
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java
new file mode 100644
index 0000000000..c376059208
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java
@@ -0,0 +1,133 @@
+/*
+ * 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.java.providers;
+
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.api.IServerSettings;
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.providers.definition.ErroneousDefinitionProvider;
+import com.itsaky.androidide.lsp.java.providers.definition.IJavaDefinitionProvider;
+import com.itsaky.androidide.lsp.java.providers.definition.LocalDefinitionProvider;
+import com.itsaky.androidide.lsp.java.providers.definition.RemoteDefinitionProvider;
+import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
+import com.itsaky.androidide.lsp.models.DefinitionParams;
+import com.itsaky.androidide.lsp.models.DefinitionResult;
+import com.itsaky.androidide.models.Location;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.progress.ICancelChecker;
+import com.itsaky.androidide.utils.DocumentUtils;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.TypeKind;
+import jdkx.tools.JavaFileObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefinitionProvider extends CancelableServiceProvider {
+
+ public static final List NOT_SUPPORTED = Collections.emptyList();
+ private static final Logger LOG = LoggerFactory.getLogger(DefinitionProvider.class);
+ private final JavaCompilerService compiler;
+ private final IServerSettings settings;
+ private Path file;
+ private Position position;
+ private int line, column;
+
+ public DefinitionProvider(JavaCompilerService compiler, IServerSettings settings,
+ ICancelChecker cancelChecker) {
+ super(cancelChecker);
+ this.compiler = compiler;
+ this.settings = settings;
+ }
+
+ public List findDefinition() {
+ abortIfCancelled();
+ final SynchronizedTask compile = compiler.compile(file);
+ abortIfCancelled();
+ final Element element = compile.get(task -> NavigationHelper.findElement(task, file, line, column, this));
+
+ if (element == null) {
+ LOG.error("Cannot find element at line: {} and column: {}", line, column);
+ return NOT_SUPPORTED;
+ }
+
+ IJavaDefinitionProvider provider = null;
+
+ if (element.asType().getKind() == TypeKind.ERROR) {
+ provider = new ErroneousDefinitionProvider(position, file, compiler, settings, this);
+ } else if (NavigationHelper.isLocal(element)) {
+ provider = new LocalDefinitionProvider(position, file, compiler, settings, this);
+ }
+
+ if (provider == null) {
+ final String className = className(element);
+ if (TextUtils.isEmpty(className)) {
+ LOG.error("No class name found for element: {}", element);
+ return NOT_SUPPORTED;
+ }
+
+ final Optional optional = compiler.findAnywhere(className);
+ if (!optional.isPresent()) {
+ LOG.error("Cannot find source file for class: {}", className);
+ return NOT_SUPPORTED;
+ }
+
+ final JavaFileObject jfo = optional.get();
+ if (DocumentUtils.isSameFile(Paths.get(jfo.toUri()), file)) {
+ provider = new LocalDefinitionProvider(position, file, compiler, settings, this);
+ } else {
+ provider = new RemoteDefinitionProvider(position, file, compiler, settings, this).setOtherFile(jfo);
+ }
+ }
+
+ return provider.findDefinition(element);
+ }
+
+ @NonNull
+ public DefinitionResult findDefinition(@NonNull DefinitionParams params) {
+ this.file = params.getFile();
+
+ // 1-based line and column index
+ this.line = params.getPosition().getLine() + 1;
+ this.column = params.getPosition().getColumn() + 1;
+ this.position = new Position(this.line, this.column);
+ final List locations = findDefinition();
+
+ LOG.debug("Found {} definitions...", locations.size());
+ return new DefinitionResult(locations);
+ }
+
+ private String className(Element element) {
+ while (element != null) {
+ abortIfCancelled();
+ if (element instanceof TypeElement) {
+ TypeElement type = (TypeElement) element;
+ return type.getQualifiedName().toString();
+ }
+ element = element.getEnclosingElement();
+ }
+ return "";
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt
new file mode 100644
index 0000000000..6ad6c2c3e1
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt
@@ -0,0 +1,346 @@
+/*
+ * 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.java.providers
+
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode.EMPTY_BLOCK
+import com.itsaky.androidide.lsp.java.models.DiagnosticCode.UNUSED_THROWS
+import com.itsaky.androidide.lsp.java.visitors.DiagnosticVisitor
+import com.itsaky.androidide.lsp.models.DiagnosticItem
+import com.itsaky.androidide.lsp.models.DiagnosticSeverity
+import com.itsaky.androidide.lsp.models.DiagnosticSeverity.WARNING
+import com.itsaky.androidide.models.Range
+import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled
+import com.itsaky.androidide.projects.FileManager
+import com.itsaky.androidide.utils.DocumentUtils.isSameFile
+import jdkx.lang.model.element.Element
+import jdkx.tools.Diagnostic
+import jdkx.tools.JavaFileObject
+import openjdk.source.tree.BlockTree
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.LineMap
+import openjdk.source.tree.MethodTree
+import openjdk.source.tree.VariableTree
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.Locale
+import java.util.regex.Pattern
+
+/**
+ * Finds errors and warnings from a compilation task.
+ *
+ * @author Akash Yadav
+ */
+object DiagnosticsProvider {
+ /**
+ * Finds diagnostics from the given task (only the diagnostics for the given file). The task
+ * should be a valid task.
+ *
+ * As the file might be too long, the diagnostics list must be sorted so we can quickly binary
+ * search the list when needed.
+ *
+ * @param task The compilation task to get diagnostics from.
+ * @param file The file of which the diagnostics must be extracted.
+ * @return The list of diagnostics retrieved from the task. Never null.
+ */
+ @JvmStatic
+ fun findDiagnostics(
+ task: CompileTask,
+ file: Path?,
+ ): List {
+ val result = mutableListOf()
+ var root: CompilationUnitTree? = null
+ for (tree in task.roots) {
+ abortIfCancelled()
+ val path = Paths.get(tree.sourceFile.toUri())
+ if (isSameFile(path, file!!)) {
+ root = tree
+ break
+ }
+ }
+
+ abortIfCancelled()
+
+ if (root == null) {
+ // CompilationUnitTree for the file was not found
+ // Can't do anything...
+ return result
+ }
+
+ addCompilerErrors(task, root, result)
+ abortIfCancelled()
+ addDiagnosticsByVisiting(task, root, result)
+ abortIfCancelled()
+ return result
+ }
+
+ private fun addDiagnosticsByVisiting(
+ task: CompileTask,
+ root: CompilationUnitTree,
+ result: MutableList,
+ ) {
+ val notThrown = mutableMapOf()
+ val scanner = DiagnosticVisitor(task.task)
+ scanner.scan(root, notThrown)
+ for (unusedEl in scanner.notUsed()) {
+ warnUnused(task, unusedEl)?.also { result.add(it) }
+ }
+
+ for (location in notThrown.keys) {
+ result.add(warnNotThrown(task, notThrown[location], location!!))
+ }
+
+ for (path in scanner.emptyBlocks.keys) {
+ result.add(warnEmptyBlock(task, path, scanner.emptyBlocks[path]!!))
+ }
+ }
+
+ private fun warnEmptyBlock(
+ task: CompileTask,
+ path: TreePath,
+ name: String,
+ ): DiagnosticItem {
+ val trees = Trees.instance(task.task)
+ val thisTree = path.leaf
+ val code = EMPTY_BLOCK
+
+ val root = task.root()
+ val lines = task.root().lineMap
+ val positions = trees.sourcePositions
+ val start = positions.getStartPosition(root, thisTree)
+ val end = positions.getEndPosition(root, thisTree)
+ return DiagnosticItem(
+ source = "",
+ code = code.id,
+ message = "'$name' statement has empty body",
+ severity = WARNING,
+ range =
+ Range(getPosition(start, lines), getPosition(end, lines)).apply {
+ this.start.index = start.toInt()
+ this.end.index = end.toInt()
+ },
+ )
+ }
+
+ private fun addCompilerErrors(
+ task: CompileTask,
+ root: CompilationUnitTree,
+ result: MutableList,
+ ) {
+ for (diagnostic in task.diagnostics) {
+ if (diagnostic.source == null || diagnostic.source!!.toUri() != root.sourceFile.toUri()) {
+ continue
+ }
+ if (diagnostic.startPosition == -1L || diagnostic.endPosition == -1L) {
+ continue
+ }
+ result.add(asDiagnosticItem(diagnostic, root.lineMap))
+ }
+ }
+
+ private fun warnNotThrown(
+ task: CompileTask,
+ name: String?,
+ path: TreePath,
+ ): DiagnosticItem {
+ val trees = Trees.instance(task.task)
+ val pos = trees.sourcePositions
+ val root = path.compilationUnit
+ val lines = root.lineMap
+ val start = pos.getStartPosition(root, path.leaf)
+ val end = pos.getEndPosition(root, path.leaf)
+ return DiagnosticItem(
+ message = String.format("'%s' is not thrown in the body of the method", name),
+ range =
+ Range(getPosition(start, lines), getPosition(end, lines)).apply {
+ this.start.index = start.toInt()
+ this.end.index = end.toInt()
+ },
+ code = UNUSED_THROWS.id,
+ severity = DiagnosticSeverity.INFO,
+ source = "",
+ )
+ }
+
+ private fun warnUnused(
+ task: CompileTask,
+ unusedEl: Element,
+ ): DiagnosticItem? {
+ val trees = Trees.instance(task.task)
+ val path = trees.getPath(unusedEl) ?: throw RuntimeException("$unusedEl has no path")
+ val root = path.compilationUnit
+ val leaf = path.leaf
+ val pos = trees.sourcePositions
+ var start = pos.getStartPosition(root, leaf).toInt()
+ var end = pos.getEndPosition(root, leaf).toInt()
+
+ if (leaf is VariableTree) {
+ val offset = pos.getEndPosition(root, leaf.type).toInt()
+ if (offset != -1) {
+ start = offset
+ }
+ }
+
+ val file = Paths.get(root.sourceFile.toUri())
+ val contents = FileManager.getDocumentContents(file)
+ var name = unusedEl.simpleName
+ if (name.contentEquals("")) {
+ name = unusedEl.enclosingElement.simpleName
+ }
+
+ val region =
+ try {
+ contents.subSequence(start, end)
+ } catch (err: IndexOutOfBoundsException) {
+ // might happen if the file contents were changed after the file was compiled for analysis
+ return null
+ }
+
+ val matcher = Pattern.compile("\\b$name\\b").matcher(region)
+ if (matcher.find()) {
+ start += matcher.start()
+ end = start + name.length
+ }
+
+ val message = String.format("'%s' is not used", name)
+ val code: DiagnosticCode
+ val severity: DiagnosticSeverity
+ when (leaf) {
+ is VariableTree -> {
+ when (path.parentPath.leaf) {
+ is MethodTree -> {
+ code = DiagnosticCode.UNUSED_PARAM
+ severity = DiagnosticSeverity.HINT
+ }
+
+ is BlockTree -> {
+ code = DiagnosticCode.UNUSED_LOCAL
+ severity = DiagnosticSeverity.INFO
+ }
+
+ is ClassTree -> {
+ code = DiagnosticCode.UNUSED_FIELD
+ severity = DiagnosticSeverity.INFO
+ }
+
+ else -> {
+ code = DiagnosticCode.UNUSED_OTHER
+ severity = DiagnosticSeverity.HINT
+ }
+ }
+ }
+
+ is MethodTree -> {
+ code = DiagnosticCode.UNUSED_METHOD
+ severity = DiagnosticSeverity.INFO
+ }
+
+ is ClassTree -> {
+ code = DiagnosticCode.UNUSED_CLASS
+ severity = DiagnosticSeverity.INFO
+ }
+
+ else -> {
+ code = DiagnosticCode.UNUSED_OTHER
+ severity = DiagnosticSeverity.INFO
+ }
+ }
+
+ return asDiagnosticItem(severity, code.id, message, start.toLong(), end.toLong(), root)
+ }
+
+ private fun asDiagnosticItem(
+ severity: DiagnosticSeverity,
+ code: String,
+ message: String,
+ start: Long,
+ end: Long,
+ root: CompilationUnitTree,
+ ): DiagnosticItem =
+ DiagnosticItem(
+ message = message,
+ code = code,
+ severity = severity,
+ range =
+ Range(getPosition(start, root.lineMap), getPosition(end, root.lineMap)).apply {
+ this.start.index = start.toInt()
+ this.end.index = end.toInt()
+ },
+ source = "",
+ )
+
+ private fun asDiagnosticItem(
+ diagnostic: Diagnostic,
+ lines: LineMap,
+ ): DiagnosticItem {
+ abortIfCancelled()
+ val result =
+ DiagnosticItem(
+ range = getDiagnosticRange(diagnostic, lines),
+ severity = severityFor(diagnostic.kind),
+ code = diagnostic.code,
+ message = diagnostic.getMessage(Locale.getDefault()),
+ source = "",
+ )
+ result.range.start.index = diagnostic.startPosition.toInt()
+ result.range.end.index = diagnostic.endPosition.toInt()
+ result.extra = diagnostic
+ return result
+ }
+
+ private fun getDiagnosticRange(
+ diagnostic: Diagnostic,
+ lines: LineMap,
+ ): Range {
+ abortIfCancelled()
+ val start = getPosition(diagnostic.startPosition, lines)
+ val end = getPosition(diagnostic.endPosition, lines)
+ return Range(start, end)
+ }
+
+ private fun getPosition(
+ position: Long,
+ lines: LineMap,
+ ): com.itsaky.androidide.models.Position {
+ abortIfCancelled()
+ // decrement the numbers
+ // to convert 1-based indexes to 0-based
+ val line = (lines.getLineNumber(position) - 1).toInt()
+ val column = (lines.getColumnNumber(position) - 1).toInt()
+ return com.itsaky.androidide.models
+ .Position(line, column)
+ }
+
+ private fun severityFor(kind: Diagnostic.Kind): DiagnosticSeverity =
+ when (kind) {
+ Diagnostic.Kind.ERROR -> DiagnosticSeverity.ERROR
+
+ Diagnostic.Kind.WARNING,
+ Diagnostic.Kind.MANDATORY_WARNING,
+ -> WARNING
+
+ Diagnostic.Kind.NOTE -> DiagnosticSeverity.INFO
+
+ Diagnostic.Kind.OTHER -> DiagnosticSeverity.HINT
+
+ else -> DiagnosticSeverity.HINT
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt
similarity index 86%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt
index 78671f4d67..b1241d73bf 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt
@@ -36,21 +36,19 @@ import java.util.concurrent.atomic.AtomicBoolean
* @author Akash Yadav
*/
class JavaDiagnosticProvider {
-
private val analyzeTimestamps = mutableMapOf()
private var cachedDiagnostics = DiagnosticResult.NO_UPDATE
private var analyzing = AtomicBoolean(false)
private var analyzingThread: AnalyzingThread? = null
companion object {
-
private val log = LoggerFactory.getLogger(JavaDiagnosticProvider::class.java)
}
fun analyze(file: Path): DiagnosticResult {
-
- val module = IProjectManager.getInstance().findModuleForFile(file, false)
- ?: return DiagnosticResult.NO_UPDATE
+ val module =
+ IProjectManager.getInstance().findModuleForFile(file, false)
+ ?: return DiagnosticResult.NO_UPDATE
val compiler = JavaCompilerService(module)
abortIfCancelled()
@@ -75,20 +73,19 @@ class JavaDiagnosticProvider {
analyzing.set(true)
- val analyzingThread = AnalyzingThread(compiler, file).also {
- analyzingThread = it
- it.start()
- it.join()
- }
+ val analyzingThread =
+ AnalyzingThread(compiler, file).also {
+ analyzingThread = it
+ it.start()
+ it.join()
+ }
return analyzingThread.result.also {
this.analyzingThread = null
}
}
- fun isAnalyzing(): Boolean {
- return this.analyzing.get()
- }
+ fun isAnalyzing(): Boolean = this.analyzing.get()
fun cancel() {
this.analyzingThread?.cancel()
@@ -98,7 +95,10 @@ class JavaDiagnosticProvider {
analyzeTimestamps.remove(file)
}
- private fun doAnalyze(file: Path, task: CompileTask): DiagnosticResult {
+ private fun doAnalyze(
+ file: Path,
+ task: CompileTask,
+ ): DiagnosticResult {
val result =
if (!isTaskValid(task)) {
// Do not use Collections.emptyList ()
@@ -106,13 +106,14 @@ class JavaDiagnosticProvider {
// throws exception when trying to access.
log.info("Using cached diagnostics")
cachedDiagnostics
- } else
+ } else {
DiagnosticResult(
file,
findDiagnostics(task, file).sortedBy {
it.range
- }
+ },
)
+ }
return result.also {
log.info("Analyze file completed. Found {} diagnostic items", result.diagnostics.size)
}
@@ -123,9 +124,10 @@ class JavaDiagnosticProvider {
return task?.task != null && task.roots != null && task.roots.size > 0
}
- inner class AnalyzingThread(val compiler: JavaCompilerService, val file: Path) :
- Thread("JavaAnalyzerThread") {
-
+ inner class AnalyzingThread(
+ val compiler: JavaCompilerService,
+ val file: Path,
+ ) : Thread("JavaAnalyzerThread") {
var result: DiagnosticResult = DiagnosticResult.NO_UPDATE
fun cancel() {
@@ -146,11 +148,10 @@ class JavaDiagnosticProvider {
} finally {
compiler.destroy()
analyzing.set(false)
+ }.also {
+ cachedDiagnostics = it
+ analyzeTimestamps[file] = Instant.now()
}
- .also {
- cachedDiagnostics = it
- analyzeTimestamps[file] = Instant.now()
- }
}
}
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java
similarity index 57%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java
index aa80b2a453..e534477b94 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java
@@ -33,30 +33,30 @@
*/
public class JavaSelectionProvider {
- private static final Logger LOG = LoggerFactory.getLogger(JavaSelectionProvider.class);
- private final CompilerProvider compiler;
-
- public JavaSelectionProvider(CompilerProvider compiler) {
- this.compiler = compiler;
- }
-
- @NonNull
- public Range expandSelection(@NonNull ExpandSelectionParams params) {
- return compiler
- .compile(params.getFile())
- .get(
- task -> {
- final CompilationUnitTree root = task.root(params.getFile());
- final FindBiggerRange rangeFinder = new FindBiggerRange(task.task, root);
- final Range range = rangeFinder.scan(root, params.getSelection());
-
- if (range != null) {
- LOG.info("Expanding selection to range: {}", range);
- return range;
- }
-
- LOG.debug("Unable to expand selection");
- return params.getSelection();
- });
- }
+ private static final Logger LOG = LoggerFactory.getLogger(JavaSelectionProvider.class);
+ private final CompilerProvider compiler;
+
+ public JavaSelectionProvider(CompilerProvider compiler) {
+ this.compiler = compiler;
+ }
+
+ @NonNull
+ public Range expandSelection(@NonNull ExpandSelectionParams params) {
+ return compiler
+ .compile(params.getFile())
+ .get(
+ task -> {
+ final CompilationUnitTree root = task.root(params.getFile());
+ final FindBiggerRange rangeFinder = new FindBiggerRange(task.task, root);
+ final Range range = rangeFinder.scan(root, params.getSelection());
+
+ if (range != null) {
+ LOG.info("Expanding selection to range: {}", range);
+ return range;
+ }
+
+ LOG.debug("Unable to expand selection");
+ return params.getSelection();
+ });
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java
new file mode 100644
index 0000000000..9b4826b08b
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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.java.providers;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.CancelChecker;
+import com.itsaky.androidide.lsp.java.utils.FindHelper;
+import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
+import com.itsaky.androidide.lsp.java.visitors.FindReferences;
+import com.itsaky.androidide.lsp.models.ReferenceParams;
+import com.itsaky.androidide.lsp.models.ReferenceResult;
+import com.itsaky.androidide.models.Location;
+import com.itsaky.androidide.progress.ICancelChecker;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.TypeElement;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.util.TreePath;
+
+public class ReferenceProvider extends CancelableServiceProvider {
+
+ public static final List NOT_SUPPORTED = Collections.emptyList();
+ private final CompilerProvider compiler;
+ private Path file;
+ private int line, column;
+
+ public ReferenceProvider(CompilerProvider compiler, ICancelChecker checker) {
+ super(checker);
+ this.compiler = compiler;
+ }
+
+ public List find() {
+ abortIfCancelled();
+ final SynchronizedTask synchronizedTask = compiler.compile(file);
+
+ // findTypeReferences and findMemberReferences initiate another compilation task
+ // However, initiating a compilation task while another compilation is in progress will result in a deadlock
+ // Therefore, we return a supplier from the current synchronized task and
+ final Supplier> result = synchronizedTask.get(
+ task -> {
+ abortIfCancelled();
+ Element element = NavigationHelper.findElement(task, file, line, column, this);
+ if (element == null) {
+ return () -> NOT_SUPPORTED;
+ }
+
+ if (NavigationHelper.isLocal(element)) {
+ // findReferences method here uses the compilation task object
+ // however, finding the references lazily using supplier will leak this task
+ final var references = findReferences(task);
+ return () -> references;
+ }
+
+ if (NavigationHelper.isType(element)) {
+ TypeElement type = (TypeElement) element;
+ String className = type.getQualifiedName().toString();
+ return () -> findTypeReferences(className);
+ }
+
+ if (NavigationHelper.isMember(element)) {
+ final var parentClass = (TypeElement) element.getEnclosingElement();
+ final var className = parentClass.getQualifiedName().toString();
+
+ var memberName = element.getSimpleName().toString();
+ if (memberName.equals("")) {
+ memberName = parentClass.getSimpleName().toString();
+ }
+
+ String finalMemberName = memberName;
+ return () -> findMemberReferences(className, finalMemberName);
+ }
+
+ return () -> NOT_SUPPORTED;
+ });
+
+ return result.get();
+ }
+
+ @NonNull
+ public ReferenceResult findReferences(@NonNull ReferenceParams params) {
+ this.file = params.getFile();
+
+ // 1-based line and column indexes
+ this.line = params.getPosition().getLine() + 1;
+ this.column = params.getPosition().getColumn() + 1;
+
+ List locations;
+ try {
+ locations = find();
+ } catch (Exception err) {
+ if (!CancelChecker.isCancelled(err)) {
+ throw err;
+ }
+ locations = new ArrayList<>();
+ }
+
+ return new ReferenceResult(locations);
+ }
+
+ private List findMemberReferences(String className, String memberName) {
+ abortIfCancelled();
+ final var files = compiler.findMemberReferences(className, memberName);
+ if (files.length == 0) {
+ return Collections.emptyList();
+ }
+
+ abortIfCancelled();
+ return compiler.compile(files).get(this::findReferences);
+ }
+
+ private List findReferences(CompileTask task) {
+ abortIfCancelled();
+ Element element = NavigationHelper.findElement(task, file, line, column, this);
+ List paths = new ArrayList<>();
+ for (CompilationUnitTree root : task.roots) {
+ abortIfCancelled();
+ new FindReferences(task.task, element).scan(root, paths);
+ }
+ List locations = new ArrayList<>();
+ for (TreePath p : paths) {
+ abortIfCancelled();
+ locations.add(FindHelper.location(task, p));
+ }
+ return locations;
+ }
+
+ private List findTypeReferences(String className) {
+ abortIfCancelled();
+ Path[] files = compiler.findTypeReferences(className);
+ if (files.length == 0) {
+ return Collections.emptyList();
+ }
+
+ abortIfCancelled();
+ return compiler.compile(files).get(this::findReferences);
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java
new file mode 100644
index 0000000000..7e0d29b2a4
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java
@@ -0,0 +1,388 @@
+/*
+ * 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.java.providers;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.FindHelper;
+import com.itsaky.androidide.lsp.java.utils.MarkdownHelper;
+import com.itsaky.androidide.lsp.java.utils.ScopeHelper;
+import com.itsaky.androidide.lsp.java.utils.ShortTypePrinter;
+import com.itsaky.androidide.lsp.java.visitors.FindInvocationAt;
+import com.itsaky.androidide.lsp.models.ParameterInformation;
+import com.itsaky.androidide.lsp.models.SignatureHelp;
+import com.itsaky.androidide.lsp.models.SignatureHelpParams;
+import com.itsaky.androidide.lsp.models.SignatureInformation;
+import com.itsaky.androidide.progress.ICancelChecker;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringJoiner;
+import java.util.function.Predicate;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.ElementKind;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.element.VariableElement;
+import jdkx.lang.model.type.ArrayType;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.type.ErrorType;
+import jdkx.lang.model.type.PrimitiveType;
+import jdkx.lang.model.type.TypeMirror;
+import jdkx.lang.model.type.TypeVariable;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.ExpressionTree;
+import openjdk.source.tree.IdentifierTree;
+import openjdk.source.tree.MemberSelectTree;
+import openjdk.source.tree.MethodInvocationTree;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.NewClassTree;
+import openjdk.source.tree.Scope;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.DocTrees;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+
+public class SignatureProvider extends CancelableServiceProvider {
+
+ public static final SignatureHelp NOT_SUPPORTED = new SignatureHelp(Collections.emptyList(), -1, -1);
+ private final CompilerProvider compiler;
+
+ public SignatureProvider(CompilerProvider compiler, ICancelChecker cancelChecker) {
+ super(cancelChecker);
+ this.compiler = compiler;
+ }
+
+ @NonNull
+ public SignatureHelp signatureHelp(Path file, int l, int c) {
+
+ // 1-based line and column index
+ final int line = l + 1;
+ final int column = c + 1;
+
+ // TODO prune
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ abortIfCancelled();
+ return synchronizedTask.get(
+ task -> {
+ long cursor = task.root().getLineMap().getPosition(line, column);
+ TreePath path = new FindInvocationAt(task.task, this).scan(task.root(), cursor);
+ if (path == null) {
+ return NOT_SUPPORTED;
+ }
+ if (path.getLeaf() instanceof MethodInvocationTree) {
+ MethodInvocationTree invoke = (MethodInvocationTree) path.getLeaf();
+ List overloads = methodOverloads(task, invoke);
+ List signatures = new ArrayList<>();
+ for (ExecutableElement method : overloads) {
+ SignatureInformation info = info(method);
+ addSourceInfo(task, method, info);
+ addFancyLabel(info);
+ signatures.add(info);
+ }
+ int activeSignature = activeSignature(task, path, invoke.getArguments(), overloads);
+ int activeParameter = activeParameter(task, invoke.getArguments(), cursor);
+ return new SignatureHelp(signatures, activeSignature, activeParameter);
+ }
+ if (path.getLeaf() instanceof NewClassTree) {
+ NewClassTree invoke = (NewClassTree) path.getLeaf();
+ List overloads = constructorOverloads(task, invoke);
+ List signatures = new ArrayList<>();
+ for (ExecutableElement method : overloads) {
+ SignatureInformation info = info(method);
+ addSourceInfo(task, method, info);
+ addFancyLabel(info);
+ signatures.add(info);
+ }
+ int activeSignature = activeSignature(task, path, invoke.getArguments(), overloads);
+ int activeParameter = activeParameter(task, invoke.getArguments(), cursor);
+ return new SignatureHelp(signatures, activeSignature, activeParameter);
+ }
+ return NOT_SUPPORTED;
+ });
+ }
+
+ @NonNull
+ public SignatureHelp signatureHelp(@NonNull SignatureHelpParams params) {
+ return signatureHelp(
+ params.getFile(), params.getPosition().getLine(), params.getPosition().getColumn());
+ }
+
+ private int activeParameter(
+ @NonNull CompileTask task, @NonNull List extends ExpressionTree> arguments, long cursor) {
+ abortIfCancelled();
+ SourcePositions pos = Trees.instance(task.task).getSourcePositions();
+ CompilationUnitTree root = task.root();
+ for (int i = 0; i < arguments.size(); i++) {
+ long end = pos.getEndPosition(root, arguments.get(i));
+ if (cursor <= end) {
+ return i;
+ }
+ }
+ return arguments.size();
+ }
+
+ private int activeSignature(
+ CompileTask task,
+ TreePath invocation,
+ List extends ExpressionTree> arguments,
+ List overloads) {
+ abortIfCancelled();
+ for (int i = 0; i < overloads.size(); i++) {
+ if (isCompatible(task, invocation, arguments, overloads.get(i))) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ private void addFancyLabel(@NonNull SignatureInformation info) {
+ abortIfCancelled();
+ StringJoiner join = new StringJoiner(", ");
+ for (ParameterInformation p : info.getParameters()) {
+ join.add(p.getLabel());
+ }
+ info.setLabel(info.getLabel() + "(" + join + ")");
+ }
+
+ private void addSourceInfo(
+ @NonNull CompileTask task,
+ @NonNull ExecutableElement method,
+ @NonNull SignatureInformation info) {
+ abortIfCancelled();
+ final var type = (TypeElement) method.getEnclosingElement();
+ final var className = type.getQualifiedName().toString();
+ final var methodName = method.getSimpleName().toString();
+ final var erasedParameterTypes = FindHelper.erasedParameterTypes(task, method);
+ final var file = compiler.findAnywhere(className);
+
+ if (!file.isPresent()) {
+ return;
+ }
+
+ final var parse = compiler.parse(file.get());
+ final var source = FindHelper.findMethod(parse, className, methodName, erasedParameterTypes);
+ if (source == null) {
+ return;
+ }
+
+ final var path = Trees.instance(task.task).getPath(parse.root, source);
+ final var docTree = DocTrees.instance(task.task).getDocCommentTree(path);
+
+ if (docTree != null) {
+ info.setDocumentation(MarkdownHelper.asMarkupContent(docTree));
+ }
+
+ info.setParameters(parametersFromSource(source));
+ }
+
+ @NonNull
+ private List constructorOverloads(
+ @NonNull CompileTask task, @NonNull NewClassTree method) {
+ abortIfCancelled();
+ Trees trees = Trees.instance(task.task);
+ TreePath path = trees.getPath(task.root(), method.getIdentifier());
+ Scope scope = trees.getScope(path);
+ TypeElement type = (TypeElement) trees.getElement(path);
+ List list = new ArrayList<>();
+ for (Element member : task.task.getElements().getAllMembers(type)) {
+ if (member.getKind() != ElementKind.CONSTRUCTOR) {
+ continue;
+ }
+ if (!trees.isAccessible(scope, member, (DeclaredType) type.asType())) {
+ continue;
+ }
+ list.add((ExecutableElement) member);
+ }
+ return list;
+ }
+
+ @NonNull
+ private SignatureInformation info(@NonNull ExecutableElement method) {
+ abortIfCancelled();
+ SignatureInformation info = new SignatureInformation();
+ info.setLabel(method.getSimpleName().toString());
+ if (method.getKind() == ElementKind.CONSTRUCTOR) {
+ info.setLabel(method.getEnclosingElement().getSimpleName().toString());
+ }
+ info.setParameters(parameters(method));
+ return info;
+ }
+
+ private boolean isCompatible(
+ CompileTask task,
+ TreePath invocation,
+ List extends ExpressionTree> arguments,
+ ExecutableElement overload) {
+ abortIfCancelled();
+ if (arguments.size() > overload.getParameters().size()) {
+ return false;
+ }
+ for (int i = 0; i < arguments.size(); i++) {
+ ExpressionTree argument = arguments.get(i);
+ TypeMirror argumentType = Trees.instance(task.task).getTypeMirror(new TreePath(invocation, argument));
+ TypeMirror parameterType = overload.getParameters().get(i).asType();
+ if (!isCompatible(task, argumentType, parameterType)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean isCompatible(CompileTask task, TypeMirror argument, TypeMirror parameter) {
+ abortIfCancelled();
+ if (argument instanceof ErrorType) {
+ return true;
+ }
+ if (argument instanceof PrimitiveType) {
+ argument = task.task.getTypes().boxedClass((PrimitiveType) argument).asType();
+ }
+ if (parameter instanceof PrimitiveType) {
+ parameter = task.task.getTypes().boxedClass((PrimitiveType) parameter).asType();
+ }
+ if (argument instanceof ArrayType) {
+ if (!(parameter instanceof ArrayType)) {
+ return false;
+ }
+ ArrayType argumentA = (ArrayType) argument;
+ ArrayType parameterA = (ArrayType) parameter;
+ return isCompatible(task, argumentA.getComponentType(), parameterA.getComponentType());
+ }
+ if (argument instanceof DeclaredType) {
+ if (!(parameter instanceof DeclaredType)) {
+ return false;
+ }
+ argument = task.task.getTypes().erasure(argument);
+ parameter = task.task.getTypes().erasure(parameter);
+ return argument.toString().equals(parameter.toString());
+ }
+ return true;
+ }
+
+ @NonNull
+ private List memberOverloads(
+ @NonNull CompileTask task, @NonNull MemberSelectTree method) {
+ abortIfCancelled();
+ Trees trees = Trees.instance(task.task);
+ TreePath path = trees.getPath(task.root(), method.getExpression());
+ boolean isStatic = trees.getElement(path) instanceof TypeElement;
+ Scope scope = trees.getScope(path);
+ TypeElement type = typeElement(trees.getTypeMirror(path));
+
+ if (type == null) {
+ return Collections.emptyList();
+ }
+
+ List list = new ArrayList<>();
+ for (Element member : task.task.getElements().getAllMembers(type)) {
+ if (member.getKind() != ElementKind.METHOD) {
+ continue;
+ }
+ if (!member.getSimpleName().contentEquals(method.getIdentifier())) {
+ continue;
+ }
+ if (isStatic != member.getModifiers().contains(Modifier.STATIC)) {
+ continue;
+ }
+ if (!trees.isAccessible(scope, member, (DeclaredType) type.asType())) {
+ continue;
+ }
+ list.add((ExecutableElement) member);
+ }
+ return list;
+ }
+
+ private List methodOverloads(
+ CompileTask task, @NonNull MethodInvocationTree method) {
+ abortIfCancelled();
+ if (method.getMethodSelect() instanceof IdentifierTree) {
+ IdentifierTree id = (IdentifierTree) method.getMethodSelect();
+ return scopeOverloads(task, id);
+ }
+ if (method.getMethodSelect() instanceof MemberSelectTree) {
+ MemberSelectTree select = (MemberSelectTree) method.getMethodSelect();
+ return memberOverloads(task, select);
+ }
+ throw new RuntimeException(method.getMethodSelect().toString());
+ }
+
+ @NonNull
+ private ParameterInformation parameter(@NonNull VariableElement p) {
+ abortIfCancelled();
+ ParameterInformation info = new ParameterInformation();
+ info.setLabel(ShortTypePrinter.NO_PACKAGE.print(p.asType()));
+ return info;
+ }
+
+ @NonNull
+ private List parameters(@NonNull ExecutableElement method) {
+ abortIfCancelled();
+ List list = new ArrayList<>();
+ for (VariableElement p : method.getParameters()) {
+ list.add(parameter(p));
+ }
+ return list;
+ }
+
+ @NonNull
+ private List parametersFromSource(MethodTree source) {
+ abortIfCancelled();
+ List list = new ArrayList<>();
+ for (VariableTree p : source.getParameters()) {
+ ParameterInformation info = new ParameterInformation();
+ info.setLabel(p.getType() + " " + p.getName());
+ list.add(info);
+ }
+ return list;
+ }
+
+ @NonNull
+ private List scopeOverloads(@NonNull CompileTask task, IdentifierTree method) {
+ abortIfCancelled();
+ Trees trees = Trees.instance(task.task);
+ TreePath path = trees.getPath(task.root(), method);
+ Scope scope = trees.getScope(path);
+ List list = new ArrayList<>();
+ Predicate filter = name -> method.getName().contentEquals(name);
+ // TODO add static imports
+ for (Element member : ScopeHelper.scopeMembers(task, scope, filter)) {
+ if (member.getKind() == ElementKind.METHOD) {
+ list.add((ExecutableElement) member);
+ }
+ }
+ return list;
+ }
+
+ private TypeElement typeElement(TypeMirror type) {
+ abortIfCancelled();
+ if (type instanceof DeclaredType) {
+ DeclaredType declared = (DeclaredType) type;
+ return (TypeElement) declared.asElement();
+ }
+ if (type instanceof TypeVariable) {
+ TypeVariable variable = (TypeVariable) type;
+ return typeElement(variable.getUpperBound());
+ }
+ return null;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt
new file mode 100644
index 0000000000..ce00b1d929
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.providers.CompletionProvider
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.util.TreePath
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.Objects
+
+/**
+ * Completes class names.
+ *
+ * @author Akash Yadav
+ */
+class ClassNamesCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ val root: CompilationUnitTree,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val list = mutableListOf()
+ val packageName = Objects.toString(root.packageName, "")
+ val uniques: MutableSet = HashSet()
+
+ val file: Path = Paths.get(root.sourceFile.toUri())
+ val imports: Set =
+ root.imports
+ .map { it.qualifiedIdentifier }
+ .mapNotNull { it.toString() }
+ .toSet()
+
+ abortCompletionIfCancelled()
+ for (className in compiler.packagePrivateTopLevelTypes(packageName)) {
+ val matchLevel = matchLevel(className, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(classItem(imports, file, className, matchLevel))
+ uniques.add(className)
+ }
+
+ abortCompletionIfCancelled()
+
+ val topLevelTypes = compiler.publicTopLevelTypes()
+ for (className in topLevelTypes) {
+ val matchLevel = matchLevel(simpleName(className), partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ if (uniques.contains(className)) {
+ continue
+ }
+
+ list.add(classItem(imports, file, className, matchLevel))
+ uniques.add(className)
+ }
+ abortCompletionIfCancelled()
+ for (t in root.typeDecls) {
+ if (t !is ClassTree) {
+ continue
+ }
+ val candidate = if (t.simpleName == null) "" else t.simpleName
+
+ val matchLevel = matchLevel(candidate, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ val name = packageName + "." + t.simpleName
+ list.add(classItem(name, matchLevel))
+
+ if (list.size > CompletionProvider.MAX_COMPLETION_ITEMS) {
+ break
+ }
+ }
+
+ log.info("...found {} class names", list.size)
+
+ return CompletionResult(list)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt
new file mode 100644
index 0000000000..b8d896097c
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt
@@ -0,0 +1,431 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.api.describeSnippet
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler
+import com.itsaky.androidide.lsp.java.models.JavaCompletionItem
+import com.itsaky.androidide.lsp.java.providers.BaseJavaServiceProvider
+import com.itsaky.androidide.lsp.java.utils.EditHelper
+import com.itsaky.androidide.lsp.models.ClassCompletionData
+import com.itsaky.androidide.lsp.models.Command
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionItemKind
+import com.itsaky.androidide.lsp.models.CompletionItemKind.ENUM_MEMBER
+import com.itsaky.androidide.lsp.models.CompletionItemKind.FUNCTION
+import com.itsaky.androidide.lsp.models.CompletionItemKind.KEYWORD
+import com.itsaky.androidide.lsp.models.CompletionItemKind.MODULE
+import com.itsaky.androidide.lsp.models.CompletionItemKind.NONE
+import com.itsaky.androidide.lsp.models.CompletionItemKind.PROPERTY
+import com.itsaky.androidide.lsp.models.CompletionItemKind.VARIABLE
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.FieldCompletionData
+import com.itsaky.androidide.lsp.models.ICompletionData
+import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.models.MethodCompletionData
+import com.itsaky.androidide.lsp.snippets.ISnippet
+import com.itsaky.androidide.preferences.utils.indentationString
+import jdkx.lang.model.element.Element
+import jdkx.lang.model.element.ElementKind.ANNOTATION_TYPE
+import jdkx.lang.model.element.ElementKind.CLASS
+import jdkx.lang.model.element.ElementKind.CONSTRUCTOR
+import jdkx.lang.model.element.ElementKind.ENUM
+import jdkx.lang.model.element.ElementKind.ENUM_CONSTANT
+import jdkx.lang.model.element.ElementKind.EXCEPTION_PARAMETER
+import jdkx.lang.model.element.ElementKind.FIELD
+import jdkx.lang.model.element.ElementKind.INSTANCE_INIT
+import jdkx.lang.model.element.ElementKind.INTERFACE
+import jdkx.lang.model.element.ElementKind.LOCAL_VARIABLE
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ElementKind.OTHER
+import jdkx.lang.model.element.ElementKind.PACKAGE
+import jdkx.lang.model.element.ElementKind.PARAMETER
+import jdkx.lang.model.element.ElementKind.RESOURCE_VARIABLE
+import jdkx.lang.model.element.ElementKind.STATIC_INIT
+import jdkx.lang.model.element.ElementKind.TYPE_PARAMETER
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.TypeElement
+import jdkx.lang.model.element.VariableElement
+import openjdk.source.tree.Tree
+import openjdk.source.util.TreePath
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.nio.file.Path
+
+/**
+ * Completion provider for Java source code.
+ *
+ * @author Akash Yadav
+ */
+abstract class IJavaCompletionProvider(
+ protected val cursor: Long,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : BaseJavaServiceProvider(completingFile, compiler, settings) {
+ protected lateinit var filePackage: String
+ protected lateinit var fileImports: Set
+
+ companion object {
+ @JvmStatic
+ protected val log: Logger = LoggerFactory.getLogger(IJavaCompletionProvider::class.java)
+ }
+
+ open fun complete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val root = task.root(file)
+ filePackage = root.`package`?.packageName?.toString() ?: ""
+ fileImports = root.imports.map { it.qualifiedIdentifier.toString() }.toSet()
+ abortCompletionIfCancelled()
+ return doComplete(task, path, partial, endsWithParen)
+ }
+
+ /**
+ * Provide completions with the given data.
+ *
+ * @param task The compilation task. Subclasses are expected to use this compile task instead of
+ * starting another compilation process.
+ * @param path The [TreePath] defining the [Tree] at the current position.
+ * @param partial The partial identifier.
+ * @param endsWithParen `true` if the statement at cursor ends with a parenthesis. `false`
+ * otherwise.
+ */
+ protected abstract fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult
+
+ protected open fun matchLevel(
+ candidate: CharSequence,
+ partial: CharSequence,
+ ): MatchLevel {
+ abortCompletionIfCancelled()
+ return CompletionItem.matchLevel(candidate.toString(), partial.toString())
+ }
+
+ protected open fun putMethod(
+ method: ExecutableElement,
+ methods: MutableMap>,
+ ) {
+ abortCompletionIfCancelled()
+ val name = method.simpleName.toString()
+ if (!methods.containsKey(name)) {
+ methods[name] = ArrayList()
+ }
+ methods[name]!!.add(method)
+ }
+
+ protected open fun keyword(
+ keyword: String,
+ partial: CharSequence,
+ matchRatio: Int,
+ ): CompletionItem = keyword(keyword, partial, CompletionItem.matchLevel(keyword, partial.toString()))
+
+ protected open fun keyword(
+ keyword: String,
+ partialName: CharSequence,
+ matchLevel: MatchLevel,
+ ): CompletionItem {
+ abortCompletionIfCancelled()
+ val item = JavaCompletionItem()
+ item.ideLabel = keyword
+ item.completionKind = KEYWORD
+ item.detail = "keyword"
+ item.ideSortText = keyword
+ item.matchLevel = matchLevel
+ return item
+ }
+
+ protected open fun method(
+ task: CompileTask,
+ overloads: List,
+ addParens: Boolean,
+ matchLevel: MatchLevel,
+ partial: String,
+ ): CompletionItem {
+ abortCompletionIfCancelled()
+ val first = overloads[0]
+ val item = JavaCompletionItem()
+ item.ideLabel = first.simpleName.toString()
+ item.completionKind = CompletionItemKind.METHOD
+ item.detail = printMethodDetail(first)
+ item.ideSortText = item.ideLabel
+ item.matchLevel = matchLevel
+ item.overrideTypeText = EditHelper.printType(first.returnType)
+ val data = data(task, first, overloads.size)
+ item.data = data
+
+ abortCompletionIfCancelled()
+ if (addParens) {
+ if (overloads.size == 1 && first.parameters.isEmpty()) {
+ item.insertText = first.simpleName.toString() + "()$0"
+ } else {
+ item.insertText = first.simpleName.toString() + "($0)"
+ item.command = Command("Trigger Parameter Hints", Command.TRIGGER_PARAMETER_HINTS)
+ }
+ item.insertTextFormat = SNIPPET // DefaultSnippet
+ item.snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true)
+ }
+ return item
+ }
+
+ protected open fun printMethodDetail(first: ExecutableElement): String {
+ val sb = StringBuilder()
+ sb.append(first.simpleName)
+ sb.append("(")
+ if (first.parameters.isNotEmpty()) {
+ for (index in first.parameters.indices) {
+ val parameter = first.parameters[index]
+ sb.append(EditHelper.printType(parameter.asType()))
+ if (index != first.parameters.lastIndex) {
+ sb.append(", ")
+ }
+ }
+ }
+ sb.append(")")
+ return sb.toString()
+ }
+
+ protected open fun item(
+ task: CompileTask,
+ element: Element,
+ matchLevel: MatchLevel,
+ ): CompletionItem {
+ if (element.kind == METHOD) throw RuntimeException("method")
+
+ abortCompletionIfCancelled()
+ val item = JavaCompletionItem()
+ item.ideLabel = element.simpleName.toString()
+ item.completionKind = kind(element)
+ item.detail = element.toString()
+ item.data = data(task, element, 1)
+ item.ideSortText = item.ideLabel
+ item.matchLevel = matchLevel
+
+ if (element is VariableElement) {
+ if (element.constantValue != null) {
+ item.detail = "Constant: ${element.constantValue}"
+ }
+ item.overrideTypeText = EditHelper.printType(element.asType())
+ }
+
+ return item
+ }
+
+ protected open fun classItem(
+ className: String,
+ matchLevel: MatchLevel,
+ ): CompletionItem = classItem(emptySet(), null, className, matchLevel)
+
+ protected open fun classItem(
+ imports: Set,
+ file: Path?,
+ className: String,
+ matchLevel: MatchLevel,
+ ): CompletionItem {
+ abortCompletionIfCancelled()
+ val item = JavaCompletionItem()
+ item.ideLabel = simpleName(className).toString()
+ item.completionKind = CompletionItemKind.CLASS
+ item.detail = packageName(className).toString()
+ item.ideSortText = item.ideLabel
+ item.matchLevel = matchLevel
+ item.data = ClassCompletionData(className)
+
+ // If file is not provided, we are probably completing an import path
+ item.additionalEditHandler = if (file == null) null else ClassImportEditHandler(imports, file)
+ return item
+ }
+
+ protected open fun simpleName(name: String): CharSequence =
+ if (name.contains(".")) {
+ name.subSequence(name.lastIndexOf('.') + 1, name.length)
+ } else {
+ name
+ }
+
+ private fun packageName(name: CharSequence): CharSequence =
+ if (name.contains(".")) {
+ name.subSequence(0, name.lastIndexOf('.'))
+ } else {
+ name
+ }
+
+ protected open fun packageItem(
+ name: String,
+ matchLevel: MatchLevel,
+ ): CompletionItem {
+ abortCompletionIfCancelled()
+ val simpleName = simpleName(name).toString()
+ var packageName = packageName(name).toString()
+ if (packageName == name) {
+ packageName = " "
+ }
+ return JavaCompletionItem().apply {
+ this.ideLabel = simpleName
+ this.detail = packageName
+ this.insertText = simpleName
+ this.completionKind = MODULE
+ this.ideSortText = name
+ this.matchLevel = matchLevel
+ }
+ }
+
+ protected open fun snippetItem(
+ snippet: ISnippet,
+ matchLevel: MatchLevel,
+ partial: String,
+ indent: Int,
+ ): CompletionItem =
+ JavaCompletionItem().apply {
+ this.ideLabel = snippet.prefix
+ this.detail = snippet.description
+ this.completionKind = CompletionItemKind.SNIPPET
+ this.matchLevel = matchLevel
+ this.ideSortText = "00000${snippet.prefix}"
+ this.snippetDescription = describeSnippet(partial)
+
+ val indentation = indentationString(indent)
+ this.insertTextFormat = SNIPPET
+ this.insertText =
+ snippet.body.joinToString(separator = "\n").also {
+ it.replace("\t", indentationString).replace("\n", "\n$indentation")
+ }
+ }
+
+ protected open fun kind(e: Element): CompletionItemKind {
+ abortCompletionIfCancelled()
+ return when (e.kind) {
+ ANNOTATION_TYPE -> CompletionItemKind.ANNOTATION_TYPE
+
+ CLASS -> CompletionItemKind.CLASS
+
+ CONSTRUCTOR -> CompletionItemKind.CONSTRUCTOR
+
+ ENUM -> CompletionItemKind.ENUM
+
+ ENUM_CONSTANT -> ENUM_MEMBER
+
+ EXCEPTION_PARAMETER,
+ PARAMETER,
+ -> PROPERTY
+
+ FIELD -> CompletionItemKind.FIELD
+
+ STATIC_INIT,
+ INSTANCE_INIT,
+ -> FUNCTION
+
+ INTERFACE -> CompletionItemKind.INTERFACE
+
+ LOCAL_VARIABLE,
+ RESOURCE_VARIABLE,
+ -> VARIABLE
+
+ METHOD -> CompletionItemKind.METHOD
+
+ PACKAGE -> MODULE
+
+ TYPE_PARAMETER -> CompletionItemKind.TYPE_PARAMETER
+
+ OTHER -> NONE
+
+ else -> NONE
+ }
+ }
+
+ protected open fun data(
+ task: CompileTask,
+ element: Element,
+ overloads: Int,
+ ): ICompletionData? {
+ abortCompletionIfCancelled()
+ return when {
+ element is TypeElement -> getClassCompletionData(element)
+ element.kind == FIELD -> getFieldCompletionData(element)
+ element is ExecutableElement -> getMethodCompletionData(task, element, overloads)
+ else -> return null
+ }
+ }
+
+ protected open fun getMethodCompletionData(
+ task: CompileTask,
+ element: ExecutableElement,
+ overloads: Int,
+ ): MethodCompletionData {
+ val types = task.task.types
+ val type = element.enclosingElement as TypeElement
+ val parameterTypes = Array(element.parameters.size) { "" }
+ val erasedParameterTypes = Array(parameterTypes.size) { "" }
+ val plusOverloads = overloads - 1
+
+ for (i in element.parameters.indices) {
+ val p = element.parameters[i].asType()
+ parameterTypes[i] = p.toString()
+ erasedParameterTypes[i] = types.erasure(p).toString()
+ }
+
+ return MethodCompletionData(
+ element.simpleName.toString(),
+ getClassCompletionData(type),
+ parameterTypes.toList(),
+ erasedParameterTypes.toList(),
+ plusOverloads,
+ )
+ }
+
+ protected open fun getFieldCompletionData(element: Element): FieldCompletionData {
+ val field = element as VariableElement
+ val type = field.enclosingElement as TypeElement
+ return FieldCompletionData(field.simpleName.toString(), getClassCompletionData(type))
+ }
+
+ protected open fun getClassCompletionData(element: TypeElement) =
+ ClassCompletionData(
+ element.qualifiedName.toString(),
+ element.enclosingElement.kind != PACKAGE,
+ element.findTopLevelElement().qualifiedName.toString(),
+ )
+
+ protected open fun TypeElement.findTopLevelElement(): TypeElement {
+ if (enclosingElement.kind == PACKAGE) {
+ return this
+ }
+
+ var element: TypeElement? = this
+ while (true) {
+ if (element == null || element.enclosingElement?.kind == PACKAGE) {
+ break
+ }
+
+ element = element.enclosingElement as? TypeElement
+ }
+
+ return element!!
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt
new file mode 100644
index 0000000000..768dee47c9
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import openjdk.source.util.TreePath
+import java.nio.file.Path
+
+/** @author Akash Yadav */
+class IdentifierCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val list = mutableListOf()
+
+ abortCompletionIfCancelled()
+
+ val snippets =
+ SnippetCompletionProvider(cursor, file, compiler, settings)
+ .complete(task, path, partial, endsWithParen)
+ list.addAll(snippets.items)
+
+ val scopeMembers =
+ ScopeCompletionProvider(file, cursor, compiler, settings)
+ .complete(task, path, partial, endsWithParen)
+ list.addAll(scopeMembers.items)
+
+ abortCompletionIfCancelled()
+ val staticImports =
+ StaticImportCompletionProvider(file, cursor, compiler, settings, path.compilationUnit)
+ .complete(task, path, partial, endsWithParen)
+ list.addAll(staticImports.items)
+
+ if (CompletionResult.TRIM_TO_MAX && list.size < CompletionResult.MAX_ITEMS) {
+ val allLower: Boolean = settings.shouldMatchAllLowerCase()
+ if (allLower || (partial.isNotEmpty() && Character.isUpperCase(partial[0]))) {
+ abortCompletionIfCancelled()
+ val classNames =
+ ClassNamesCompletionProvider(file, cursor, compiler, settings, path.compilationUnit)
+ .complete(task, path, partial, endsWithParen)
+ list.addAll(classNames.items)
+ }
+ }
+
+ abortCompletionIfCancelled()
+ val keywords =
+ KeywordCompletionProvider(file, cursor, compiler, settings)
+ .complete(task, path, partial, endsWithParen)
+ list.addAll(keywords.items)
+
+ return CompletionResult(list)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt
new file mode 100644
index 0000000000..4f7a8a8bfb
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt
@@ -0,0 +1,453 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.providers.CompletionProvider.MAX_COMPLETION_ITEMS
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel.CASE_SENSITIVE_EQUAL
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import com.itsaky.androidide.projects.api.ModuleProject
+import com.itsaky.androidide.projects.util.BootClasspathProvider
+import com.itsaky.androidide.utils.ClassTrie
+import com.itsaky.androidide.utils.ClassTrie.Node
+import jdkx.lang.model.element.Element
+import jdkx.lang.model.element.ElementKind
+import jdkx.lang.model.element.ElementKind.ANNOTATION_TYPE
+import jdkx.lang.model.element.ElementKind.CLASS
+import jdkx.lang.model.element.ElementKind.CONSTRUCTOR
+import jdkx.lang.model.element.ElementKind.ENUM
+import jdkx.lang.model.element.ElementKind.ENUM_CONSTANT
+import jdkx.lang.model.element.ElementKind.FIELD
+import jdkx.lang.model.element.ElementKind.INSTANCE_INIT
+import jdkx.lang.model.element.ElementKind.INTERFACE
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ElementKind.STATIC_INIT
+import jdkx.lang.model.element.Modifier.STATIC
+import jdkx.lang.model.element.TypeElement
+import openjdk.source.util.TreePath
+import openjdk.tools.javac.api.JavacTrees
+import openjdk.tools.javac.code.Symbol.MethodSymbol
+import openjdk.tools.javac.model.JavacTypes
+import openjdk.tools.javac.tree.JCTree.JCImport
+import java.nio.file.Path
+
+/**
+ * Provides completions for imports.
+ *
+ * @author Akash Yadav
+ */
+class ImportCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ lateinit var importPath: String
+
+ // TODO add tests for this
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val importTree = path.leaf
+ if (importTree !is JCImport) {
+ return CompletionResult.EMPTY
+ }
+
+ log.info("...complete import for path: {}", importPath)
+
+ val names: MutableSet = HashSet()
+ val list = mutableListOf()
+
+ var pkgName = importPath
+ val incomplete: String
+ if (!pkgName.contains(".")) {
+ pkgName = ""
+ incomplete = importPath
+ } else if (pkgName.endsWith(".")) {
+ pkgName = pkgName.substring(0, pkgName.lastIndex)
+ incomplete = ""
+ } else {
+ incomplete = pkgName.substringAfterLast(delimiter = '.')
+ pkgName = pkgName.substringBeforeLast(delimiter = '.')
+ }
+
+ abortCompletionIfCancelled()
+ run {
+ val match = matchLevel("static", incomplete)
+ if (match != NO_MATCH && !importTree.isStatic && pkgName.isEmpty()) {
+ list.add(keyword("static", incomplete, match))
+ }
+ }
+
+ abortCompletionIfCancelled()
+ val module = compiler.module
+ if (module == null) {
+ legacyImportPathCompletion(partial, names, list)
+ return CompletionResult(list)
+ }
+
+ if (pkgName.isEmpty() || pkgName.isBlank()) {
+ // User is typing first segment of package name
+ // Javac APIs will not work here
+ tryCompleteImport(pkgName, incomplete, list, names, module)
+ return CompletionResult(list)
+ }
+
+ try {
+ val packages = collectPackageNodes(module, pkgName)
+ abortCompletionIfCancelled()
+ if (packages.isNotEmpty()) {
+ for (node in packages) {
+ addDirectChildNodes(node, incomplete, list, names, false)
+ }
+ }
+ } catch (err: RequireMemberCompletionException) {
+ // If pkgName is not an existing package name, check if it is a qualified classname
+ // A user might be trying to acess members of a member class. So, we keep replacing last '.'
+ // until we find a valid qualified name of a class
+ if (completeTypeMembers(task, path, pkgName, incomplete, list)) {
+ return CompletionResult(list)
+ }
+ }
+
+ try {
+ // This maybe reached only in some rare cases
+ tryCompleteImport(pkgName, incomplete, list, names, module)
+ } catch (e: RequireMemberCompletionException) {
+ // User is trying to access members of a class
+ if (completeTypeMembers(task, path, pkgName, incomplete, list)) {
+ return CompletionResult(list)
+ }
+ }
+
+ return CompletionResult(list)
+ }
+
+ private fun completeTypeMembers(
+ task: CompileTask,
+ path: TreePath,
+ pkgName: String,
+ incomplete: String,
+ list: MutableList,
+ ): Boolean {
+ abortCompletionIfCancelled()
+ val elements = task.task.elements
+ var typesForPkg: Set = setOf()
+ val maybeInnerName = StringBuilder(pkgName)
+ while (true) {
+ val types = elements.getAllTypeElements(maybeInnerName)
+ if (types.isNotEmpty()) {
+ typesForPkg = types
+ break
+ }
+
+ if (!maybeInnerName.contains(".")) {
+ break
+ }
+ maybeInnerName.setCharAt(maybeInnerName.lastIndexOf('.'), '$')
+ }
+
+ abortCompletionIfCancelled()
+ if (typesForPkg.isNotEmpty()) {
+ // We found a valid class name
+ // Add the accessible class items
+ for (type in typesForPkg) {
+ val result = completeTypeMembers(task, type, path, incomplete)
+ if (result.isNotEmpty()) {
+ list.addAll(result)
+ }
+ }
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Collects package nodes for [pkgName] in source paths, classpaths and bootclasspaths. If any
+ * segment of [pkgName] is a class, [RequireMemberCompletionException] is thrown to indicate that
+ * class members must be completed.
+ *
+ * @param module The project module
+ * @param pkgName The package name to collect nodes for.
+ */
+ private fun collectPackageNodes(
+ module: ModuleProject,
+ pkgName: String,
+ ): List {
+ abortCompletionIfCancelled()
+ val result = mutableListOf()
+ val fromSource = collectPackageNode(module.compileJavaSourceClasses, pkgName)
+ if (fromSource != null) {
+ result.add(fromSource)
+ }
+
+ abortCompletionIfCancelled()
+ val fromClasspath = collectPackageNode(module.compileClasspathClasses, pkgName)
+ if (fromClasspath != null) {
+ result.add(fromClasspath)
+ }
+
+ BootClasspathProvider.getAllEntries().forEach {
+ abortCompletionIfCancelled()
+ val fromBootclasspath = collectPackageNode(it, pkgName)
+ if (fromBootclasspath != null) {
+ result.add(fromBootclasspath)
+ }
+ }
+ return result
+ }
+
+ /**
+ * Collect package nodes from the [trie] for the given [pkgName]. If any segment of [pkgName] is a
+ * class, [RequireMemberCompletionException] is thrown to indicate that class members must be
+ * completed.
+ *
+ * @param trie The [ClassTrie] to find package names from.
+ * @param pkgName The package name of the package to find node for.
+ * @return The found package name. Or `null` if no package can be found.
+ */
+ private fun collectPackageNode(
+ trie: com.itsaky.androidide.utils.ClassTrie,
+ pkgName: String,
+ ): Node? {
+ val segments = trie.segments(pkgName)
+ var node: Node? = trie.root
+ for (segment in segments) {
+ abortCompletionIfCancelled()
+ if (node == null) {
+ break
+ }
+
+ if (node.isClass) {
+ // If any of the segment in pkgName is a class
+ // We need to complete memebers of a class
+ throw RequireMemberCompletionException()
+ }
+
+ node = node.children[segment]
+ }
+
+ return node
+ }
+
+ private fun completeTypeMembers(
+ task: CompileTask,
+ type: TypeElement,
+ path: TreePath,
+ partial: String,
+ ): MutableList {
+ abortCompletionIfCancelled()
+
+ val list = mutableListOf()
+ val elements = task.task.elements
+ val trees = JavacTrees.instance(task.task.context)
+ val jcTypes = JavacTypes.instance(task.task.context)
+ val scope = trees.getScope(path)
+ val isStatic = (path.leaf as JCImport).isStatic
+ if (!trees.isAccessible(scope, type)) {
+ // Type not accessible
+ return list
+ }
+
+ val members = elements.getAllMembers(type)
+ for (member in members) {
+ abortCompletionIfCancelled()
+ if (
+ member.kind == CONSTRUCTOR || member.kind == STATIC_INIT || member.kind == INSTANCE_INIT
+ ) {
+ continue
+ }
+
+ val match = matchLevel(member.simpleName, partial)
+ if (match == NO_MATCH) {
+ continue
+ }
+
+ if (isType(member)) {
+ list.add(classItem(member.simpleName.toString(), match))
+ continue
+ }
+
+ if (!isStatic) {
+ continue
+ }
+
+ val mods = member.modifiers
+ if (!mods.contains(STATIC)) {
+ continue
+ }
+
+ if (!trees.isAccessible(scope, member, jcTypes.getDeclaredType(type))) {
+ continue
+ }
+
+ if (member.kind == METHOD) {
+ list.add(method(task, listOf(member as MethodSymbol), false, match, partial))
+ continue
+ }
+
+ if (member.kind == FIELD || member.kind == ENUM_CONSTANT) {
+ list.add(item(task, member, match))
+ }
+ }
+
+ return list
+ }
+
+ @Throws(RequireMemberCompletionException::class)
+ private fun tryCompleteImport(
+ pkgName: String,
+ incomplete: String,
+ list: MutableList,
+ names: MutableSet,
+ module: ModuleProject,
+ packageOnly: Boolean = false,
+ ) {
+ abortCompletionIfCancelled()
+ val sourceNode =
+ if (pkgName.isEmpty()) {
+ module.compileJavaSourceClasses.root
+ } else {
+ module.compileJavaSourceClasses.findNode(pkgName)
+ }
+ if (sourceNode != null) {
+ if (sourceNode.isClass) {
+ throw RequireMemberCompletionException()
+ }
+
+ addDirectChildNodes(sourceNode, incomplete, list, names, packageOnly)
+ }
+
+ abortCompletionIfCancelled()
+ val classpathNode =
+ if (pkgName.isEmpty()) {
+ module.compileClasspathClasses.root
+ } else {
+ module.compileClasspathClasses.findNode(pkgName)
+ }
+ if (classpathNode != null) {
+ if (classpathNode.isClass) {
+ throw RequireMemberCompletionException()
+ }
+
+ addDirectChildNodes(classpathNode, incomplete, list, names, packageOnly)
+ }
+
+ BootClasspathProvider.getAllEntries().forEach {
+ abortCompletionIfCancelled()
+ val node =
+ if (pkgName.isEmpty()) {
+ it.root
+ } else {
+ it.findNode(pkgName)
+ }
+ if (node != null) {
+ if (node.isClass) {
+ throw RequireMemberCompletionException()
+ }
+ addDirectChildNodes(node, incomplete, list, names, packageOnly)
+ }
+ }
+ }
+
+ private fun addDirectChildNodes(
+ sourceNode: Node,
+ incomplete: String,
+ list: MutableList,
+ names: MutableSet,
+ packageOnly: Boolean,
+ ) {
+ for (child in sourceNode.children.values) {
+ abortCompletionIfCancelled()
+ val match =
+ if (incomplete.isEmpty()) {
+ CASE_SENSITIVE_EQUAL
+ } else {
+ matchLevel(child.name, incomplete)
+ }
+
+ if (match == NO_MATCH || names.contains(child.name)) {
+ continue
+ }
+
+ if (packageOnly && child.isClass) {
+ continue
+ }
+
+ if (child.isClass) {
+ list.add(classItem(child.qualifiedName, match))
+ } else {
+ list.add(packageItem(child.qualifiedName, match))
+ }
+
+ names.add(child.name)
+ }
+ }
+
+ private fun legacyImportPathCompletion(
+ partial: String,
+ names: MutableSet,
+ list: MutableList,
+ ) {
+ abortCompletionIfCancelled()
+ for (className in compiler.publicTopLevelTypes()) {
+ val matchLevel = matchLevel(className, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ val start = importPath.lastIndexOf('.')
+ var end = className.indexOf('.', importPath.length)
+ if (end == -1) {
+ end = className.length
+ }
+ val segment = className.substring(start + 1, end)
+ if (names.contains(segment)) {
+ continue
+ }
+ names.add(segment)
+ val isClass = end == importPath.length
+ if (isClass) {
+ list.add(classItem(className, matchLevel))
+ } else {
+ list.add(packageItem(segment, matchLevel))
+ }
+
+ if (list.size > MAX_COMPLETION_ITEMS) {
+ break
+ }
+ }
+ }
+
+ internal fun isType(element: Element): Boolean = isType(element.kind)
+
+ internal fun isType(kind: ElementKind): Boolean = kind == ANNOTATION_TYPE || kind == CLASS || kind == INTERFACE || kind == ENUM
+
+ /**
+ * Internal exception to indicate that members of a class must be completed. This is thrown and
+ * caught internally when completing imports.
+ */
+ internal class RequireMemberCompletionException : IllegalStateException()
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt
new file mode 100644
index 0000000000..ce3fb9fed7
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.MethodTree
+import openjdk.source.tree.Tree
+import openjdk.source.util.TreePath
+import java.nio.file.Path
+
+/**
+ * Provides keyword completions.
+ *
+ * @author Akash Yadav
+ */
+class KeywordCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ if (partial.isBlank()) {
+ return CompletionResult.EMPTY
+ }
+
+ val level: Tree = findKeywordLevel(path)
+ var keywords = arrayOf()
+ when (level) {
+ is CompilationUnitTree -> keywords = TOP_LEVEL_KEYWORDS
+ is ClassTree -> keywords = CLASS_BODY_KEYWORDS
+ is MethodTree -> keywords = METHOD_BODY_KEYWORDS
+ }
+
+ abortCompletionIfCancelled()
+ val list = mutableListOf()
+ for (k in keywords) {
+ val matchLevel = matchLevel(k, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(keyword(k, partial, 100))
+ }
+
+ return CompletionResult(list)
+ }
+
+ private fun findKeywordLevel(treePath: TreePath): Tree {
+ var path: TreePath? = treePath
+ while (path != null) {
+ if (path.leaf is CompilationUnitTree || path.leaf is ClassTree || path.leaf is MethodTree) {
+ return path.leaf
+ }
+ path = path.parentPath
+ }
+ throw RuntimeException("empty path")
+ }
+
+ companion object {
+ private val TOP_LEVEL_KEYWORDS =
+ arrayOf(
+ "package",
+ "import",
+ "public",
+ "private",
+ "protected",
+ "abstract",
+ "class",
+ "interface",
+ "@interface",
+ "extends",
+ "implements",
+ )
+ private val CLASS_BODY_KEYWORDS =
+ arrayOf(
+ "public",
+ "private",
+ "protected",
+ "static",
+ "final",
+ "native",
+ "synchronized",
+ "abstract",
+ "default",
+ "class",
+ "interface",
+ "void",
+ "boolean",
+ "int",
+ "long",
+ "float",
+ "double",
+ "true",
+ "false",
+ "null",
+ )
+ private val METHOD_BODY_KEYWORDS =
+ arrayOf(
+ "new",
+ "assert",
+ "try",
+ "catch",
+ "finally",
+ "throw",
+ "return",
+ "break",
+ "case",
+ "continue",
+ "default",
+ "do",
+ "while",
+ "for",
+ "switch",
+ "if",
+ "else",
+ "instanceof",
+ "var",
+ "final",
+ "class",
+ "void",
+ "boolean",
+ "int",
+ "long",
+ "float",
+ "double",
+ "synchronized",
+ "true",
+ "false",
+ "null",
+ )
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt
new file mode 100644
index 0000000000..069c137e67
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt
@@ -0,0 +1,179 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier.STATIC
+import jdkx.lang.model.element.TypeElement
+import jdkx.lang.model.type.ArrayType
+import jdkx.lang.model.type.DeclaredType
+import jdkx.lang.model.type.TypeVariable
+import openjdk.source.tree.MemberReferenceTree
+import openjdk.source.tree.Scope
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import java.nio.file.Path
+
+/**
+ * Completions for member reference.
+ *
+ * @author Akash Yadav
+ */
+class MemberReferenceCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val trees = Trees.instance(task.task)
+ val select = path.leaf as MemberReferenceTree
+ log.info("...complete methods of {}", select.qualifierExpression)
+
+ val exprPath = TreePath(path, select.qualifierExpression)
+ val element = trees.getElement(exprPath)
+ val isStatic = element is TypeElement
+ val scope = trees.getScope(exprPath)
+
+ abortCompletionIfCancelled()
+ return when (val type = trees.getTypeMirror(exprPath)) {
+ is ArrayType -> completeArrayMemberReference(isStatic, partial)
+ is TypeVariable -> completeTypeVariableMemberReference(task, scope, type, isStatic, partial)
+ is DeclaredType -> completeDeclaredTypeMemberReference(task, scope, type, isStatic, partial)
+ else -> CompletionResult.EMPTY
+ }
+ }
+
+ private fun completeArrayMemberReference(
+ isStatic: Boolean,
+ partialName: CharSequence,
+ ): CompletionResult {
+ abortCompletionIfCancelled()
+ return if (isStatic) {
+ val list = mutableListOf()
+ list.add(keyword("new", partialName, 100))
+ CompletionResult(list)
+ } else {
+ CompletionResult.EMPTY
+ }
+ }
+
+ private fun completeTypeVariableMemberReference(
+ task: CompileTask,
+ scope: Scope,
+ type: TypeVariable,
+ isStatic: Boolean,
+ partial: String,
+ ): CompletionResult {
+ abortCompletionIfCancelled()
+ return when (type.upperBound) {
+ is DeclaredType -> {
+ completeDeclaredTypeMemberReference(
+ task,
+ scope,
+ type.upperBound as DeclaredType,
+ isStatic,
+ partial,
+ )
+ }
+
+ is TypeVariable -> {
+ completeTypeVariableMemberReference(
+ task,
+ scope,
+ type.upperBound as TypeVariable,
+ isStatic,
+ partial,
+ )
+ }
+
+ else -> {
+ CompletionResult.EMPTY
+ }
+ }
+ }
+
+ private fun completeDeclaredTypeMemberReference(
+ task: CompileTask,
+ scope: Scope,
+ type: DeclaredType,
+ isStatic: Boolean,
+ partial: String,
+ ): CompletionResult {
+ abortCompletionIfCancelled()
+ val trees = Trees.instance(task.task)
+ val typeElement = type.asElement() as TypeElement
+ val list: MutableList = ArrayList()
+ val methods: MutableMap> = mutableMapOf()
+ val matchLevels: MutableMap = HashMap()
+ for (member in task.task.elements.getAllMembers(typeElement)) {
+ val matchLevel = matchLevel(member.simpleName, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ if (member.kind != METHOD) {
+ continue
+ }
+
+ if (!trees.isAccessible(scope, member, type)) {
+ continue
+ }
+
+ if (!isStatic && member.modifiers.contains(STATIC)) {
+ continue
+ }
+
+ if (member.kind == METHOD) {
+ putMethod((member as ExecutableElement), methods)
+ matchLevels.putIfAbsent(member.getSimpleName().toString(), matchLevel)
+ } else {
+ list.add(item(task, member, matchLevel))
+ }
+ }
+
+ abortCompletionIfCancelled()
+ for ((key, value) in methods) {
+ val matchLevel = matchLevels.getOrDefault(key, NO_MATCH)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(method(task, value, false, matchLevel, partial))
+ }
+
+ if (isStatic) {
+ list.add(keyword("new", partial, 100))
+ }
+
+ return CompletionResult(list)
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt
new file mode 100644
index 0000000000..0281862b3d
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt
@@ -0,0 +1,240 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.utils.ScopeHelper
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import jdkx.lang.model.element.ElementKind.CONSTRUCTOR
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier.STATIC
+import jdkx.lang.model.element.TypeElement
+import jdkx.lang.model.type.ArrayType
+import jdkx.lang.model.type.DeclaredType
+import jdkx.lang.model.type.TypeVariable
+import openjdk.source.tree.MemberSelectTree
+import openjdk.source.tree.Scope
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import openjdk.tools.javac.code.Symbol
+import java.nio.file.Path
+
+/**
+ * Completions for member select.
+ *
+ * @author Akash Yadav
+ */
+class MemberSelectCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val trees = Trees.instance(task.task)
+ val select =
+ path.leaf as? MemberSelectTree
+ ?: run {
+ log.error("A member select tree was expected but was {}", path.leaf.javaClass)
+ return CompletionResult.EMPTY
+ }
+
+ log.info("...complete members of {}", select.expression)
+
+ val exprPath = TreePath(path, select.expression)
+ val isStatic = trees.getElement(exprPath) is TypeElement
+ val scope = trees.getScope(exprPath)
+
+ abortCompletionIfCancelled()
+ return when (val type = trees.getTypeMirror(exprPath)) {
+ is ArrayType -> {
+ completeArrayMemberSelect(isStatic, partial)
+ }
+
+ is TypeVariable -> {
+ completeTypeVariableMemberSelect(task, scope, type, isStatic, partial, endsWithParen)
+ }
+
+ is DeclaredType -> {
+ completeDeclaredTypeMemberSelect(task, scope, type, isStatic, partial, endsWithParen)
+ }
+
+ else -> {
+ CompletionResult.EMPTY
+ }
+ }
+ }
+
+ private fun completeArrayMemberSelect(
+ isStatic: Boolean,
+ partialName: CharSequence,
+ ): CompletionResult =
+ if (isStatic) {
+ abortCompletionIfCancelled()
+ CompletionResult.EMPTY
+ } else {
+ val list = mutableListOf()
+ list.add(keyword("length", partialName, 100))
+ CompletionResult(list)
+ }
+
+ private fun completeTypeVariableMemberSelect(
+ task: CompileTask,
+ scope: Scope,
+ type: TypeVariable,
+ isStatic: Boolean,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ abortCompletionIfCancelled()
+ return when (type.upperBound) {
+ is DeclaredType -> {
+ completeDeclaredTypeMemberSelect(
+ task,
+ scope,
+ type.upperBound as DeclaredType,
+ isStatic,
+ partial,
+ endsWithParen,
+ )
+ }
+
+ is TypeVariable -> {
+ completeTypeVariableMemberSelect(
+ task,
+ scope,
+ type.upperBound as TypeVariable,
+ isStatic,
+ partial,
+ endsWithParen,
+ )
+ }
+
+ else -> {
+ CompletionResult.EMPTY
+ }
+ }
+ }
+
+ private fun completeDeclaredTypeMemberSelect(
+ task: CompileTask,
+ scope: Scope,
+ type: DeclaredType,
+ isStatic: Boolean,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val trees = Trees.instance(task.task)
+ val typeElement = type.asElement() as TypeElement
+ val list = mutableListOf()
+ val methods = mutableMapOf>()
+ val matchLevels = mutableMapOf()
+
+ log.debug(
+ "DeclaredType {} with members {} in scope: {}",
+ typeElement,
+ (typeElement as Symbol).members(),
+ scope,
+ )
+
+ abortCompletionIfCancelled()
+ for (member in task.task.elements.getAllMembers(typeElement)) {
+ if (member.kind == CONSTRUCTOR) {
+ continue
+ }
+ val matchLevel = matchLevel(member.simpleName, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ if (!trees.isAccessible(scope, member, type)) {
+ continue
+ }
+
+ if (isStatic != member.modifiers.contains(STATIC)) {
+ continue
+ }
+
+ if (member.kind == METHOD) {
+ putMethod((member as ExecutableElement), methods)
+ matchLevels.putIfAbsent(member.getSimpleName().toString(), matchLevel)
+ } else {
+ list.add(item(task, member, matchLevel))
+ }
+ }
+
+ log.debug("Found {} members along with {} methods", list.size, methods.size)
+
+ abortCompletionIfCancelled()
+ for ((key, value) in methods) {
+ val matchLevel = matchLevels.getOrDefault(key, NO_MATCH)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(method(task, value, !endsWithParen, matchLevel, partial))
+ }
+
+ if (isStatic) {
+ list.add(keyword("class", partial, 100))
+ }
+
+ if (!isStatic && isEnclosingClass(type, scope)) {
+ list.add(keyword("this", partial, 100))
+ list.add(keyword("super", partial, 100))
+ }
+
+ return CompletionResult(list)
+ }
+
+ private fun isEnclosingClass(
+ type: DeclaredType,
+ start: Scope,
+ ): Boolean {
+ for (s in ScopeHelper.fastScopes(start)) {
+ // If we reach a static method, stop looking
+ val method = s.enclosingMethod
+ if (method != null && method.modifiers.contains(STATIC)) {
+ return false
+ }
+
+ // If we find the enclosing class
+ val thisElement = s.enclosingClass
+ if (thisElement != null && thisElement.asType() == type) {
+ return true
+ }
+
+ // If the enclosing class is static, stop looking
+ if (thisElement != null && thisElement.modifiers.contains(STATIC)) {
+ return false
+ }
+ }
+ return false
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt
new file mode 100644
index 0000000000..940372ea53
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.api.describeSnippet
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.edits.MultipleClassImportEditHandler
+import com.itsaky.androidide.lsp.java.models.JavaCompletionItem
+import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils.Companion.buildMethod
+import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils.Companion.print
+import com.itsaky.androidide.lsp.java.utils.ScopeHelper
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import com.squareup.javapoet.MethodSpec.Builder
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier.FINAL
+import jdkx.lang.model.element.Modifier.PRIVATE
+import jdkx.lang.model.element.Modifier.STATIC
+import jdkx.lang.model.type.DeclaredType
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.Tree.Kind.CLASS
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import java.nio.file.Path
+import java.util.function.Predicate
+
+/**
+ * Provides completions using [openjdk.source.tree.Scope].
+ *
+ * @author Akash Yadav
+ */
+class ScopeCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val trees = Trees.instance(task.task)
+ val list: MutableList = ArrayList()
+ val scope = trees.getScope(path)
+ val matchLevels = HashMap()
+ val filter =
+ Predicate {
+ if (it == null || it.isEmpty()) {
+ return@Predicate false
+ }
+
+ var name = it
+ if (it.contains('(')) {
+ name = it.substring(0, it.lastIndexOf('('))
+ }
+
+ val level = matchLevel(name, partial)
+ matchLevels[name.toString()] = level
+ return@Predicate level != NO_MATCH
+ }
+
+ abortCompletionIfCancelled()
+ for (member in ScopeHelper.scopeMembers(task, scope, filter)) {
+ var name = member.simpleName.toString()
+ if (name.contains('(')) {
+ name = name.substring(0, name.lastIndexOf('('))
+ }
+
+ val matchLevel = matchLevels.getOrDefault(name, NO_MATCH)
+
+ if (member.kind == METHOD) {
+ val method = member as ExecutableElement
+ // path is the method; go up one more level to get the class
+ val parentPath = path.parentPath.parentPath
+ list.add(overrideIfPossible(task, parentPath, method, endsWithParen, matchLevel, partial))
+ } else {
+ list.add(item(task, member, matchLevel))
+ }
+ }
+
+ log.info("...found {} scope members", list.size)
+
+ return CompletionResult(list)
+ }
+
+/**
+* Override the given method if it is overridable.
+*
+* @param task The compilation task.
+* @param parentPath The tree path of the parent class.
+* @param method The method to override if possible.
+* @param endsWithParen Does the statement at cursor ends with a parenthesis?
+* @return The completion item.
+*/
+ private fun overrideIfPossible(
+ task: CompileTask,
+ parentPath: TreePath,
+ method: ExecutableElement,
+ endsWithParen: Boolean,
+ matchLevel: MatchLevel,
+ partial: String,
+ ): CompletionItem {
+ if (parentPath.leaf.kind != CLASS) {
+ // Can only override if the cursor is directly in a class declaration
+ return method(task, listOf(method), !endsWithParen, matchLevel, partial)
+ }
+
+ abortCompletionIfCancelled()
+ val types = task.task.types
+ val parentElement =
+ Trees.instance(task.task).getElement(parentPath)
+ ?: // Can't get further information for overriding this method
+ return method(task, listOf(method), !endsWithParen, matchLevel, partial)
+ val type = parentElement.asType() as DeclaredType
+ val enclosing = method.enclosingElement
+ val isFinalClass = enclosing.modifiers.contains(FINAL)
+ val isNotOverridable =
+ (
+ method.modifiers.contains(STATIC) ||
+ method.modifiers.contains(FINAL) ||
+ method.modifiers.contains(PRIVATE)
+ )
+ if (
+ isFinalClass ||
+ isNotOverridable ||
+ !types.isAssignable(type, enclosing.asType()) ||
+ parentPath.leaf !is ClassTree
+ ) {
+ // Override is not possible
+ return method(task, listOf(method), !endsWithParen, matchLevel, partial)
+ }
+
+ // Print the method details and the annotations
+ // Print the method details and the annotations
+ val builder: Builder
+ try {
+ builder = buildMethod(method, types, type)
+ } catch (error: Throwable) {
+ log.error("Cannot override method:{} err={}", method.simpleName, error.message)
+ return method(task, listOf(method), !endsWithParen, matchLevel, partial)
+ }
+
+ val imports = mutableSetOf()
+ val methodSpec = builder.build()
+ val insertText = print(methodSpec, imports, false)
+
+ abortCompletionIfCancelled()
+
+ val item = JavaCompletionItem()
+ item.ideLabel = methodSpec.name
+ item.completionKind = com.itsaky.androidide.lsp.models.CompletionItemKind.METHOD
+ item.detail = method.returnType.toString() + " " + method
+ item.ideSortText = item.ideLabel
+ item.insertText = insertText
+ item.insertTextFormat = SNIPPET
+ item.snippetDescription = describeSnippet(partial)
+ item.matchLevel = matchLevel
+ item.data = data(task, method, 1)
+ if (item.additionalTextEdits == null) {
+ item.additionalTextEdits = mutableListOf()
+ }
+
+ imports.removeIf { "java.lang." == it || fileImports.contains(it) || filePackage == it }
+ item.additionalEditHandler = MultipleClassImportEditHandler(imports, fileImports, file)
+ return item
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt
new file mode 100644
index 0000000000..65141620c8
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository
+import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetScope
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.snippets.ISnippet
+import com.itsaky.androidide.preferences.internal.EditorPreferences
+import io.github.rosemoe.sora.text.TextUtils
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.MethodTree
+import openjdk.source.util.TreePath
+import java.nio.file.Path
+
+/**
+ * Provides snippet completion for Java files.
+ *
+ * @author Akash Yadav
+ */
+class SnippetCompletionProvider(
+ cursor: Long,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val scope = findSnippetScope(path) ?: return CompletionResult.EMPTY
+ val indent = spacesBeforeCursor(task.root().sourceFile.getCharContent(true))
+ val snippets = mutableListOf()
+
+ // add global snippets, if any
+ JavaSnippetRepository.snippets[JavaSnippetScope.GLOBAL]?.let { snippets.addAll(it) }
+
+ val snippetScope =
+ when (scope.leaf) {
+ is CompilationUnitTree -> JavaSnippetScope.TOP_LEVEL
+ is ClassTree -> JavaSnippetScope.MEMBER
+ is MethodTree -> JavaSnippetScope.LOCAL
+ else -> null
+ }
+
+ // add snippets for the current scope
+ snippetScope?.let { JavaSnippetRepository.snippets[it]?.let { list -> snippets.addAll(list) } }
+
+ val items = mutableListOf()
+
+ for (snippet in snippets) {
+ val matchLevel = matchLevel(snippet.prefix, partial)
+ if (matchLevel == MatchLevel.NO_MATCH) {
+ continue
+ }
+
+ items.add(snippetItem(snippet, matchLevel, partial, indent))
+ }
+
+ return CompletionResult(items)
+ }
+
+ private fun spacesBeforeCursor(charContent: CharSequence?): Int {
+ charContent ?: return 0
+ var start = cursor.toInt()
+ while (start >= 0) {
+ val c = charContent[start]
+ if (c == '\n' || !c.isWhitespace()) {
+ break
+ }
+ --start
+ }
+ return TextUtils.countLeadingSpaceCount(
+ charContent.substring(start, cursor.toInt()),
+ EditorPreferences.tabSize,
+ )
+ }
+
+ private fun findSnippetScope(path: TreePath?): TreePath? {
+ var scope = path
+ while (scope != null) {
+ if (scope.leaf.let { it is CompilationUnitTree || it is ClassTree || it is MethodTree }) {
+ return scope
+ }
+ scope = scope.parentPath
+ }
+ return null
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt
new file mode 100644
index 0000000000..c00f167571
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.java.providers.CompletionProvider
+import com.itsaky.androidide.lsp.models.CompletionItem
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import jdkx.lang.model.element.Element
+import jdkx.lang.model.element.ElementKind.METHOD
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier.STATIC
+import jdkx.lang.model.element.Name
+import jdkx.lang.model.element.TypeElement
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.MemberSelectTree
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import java.nio.file.Path
+
+/**
+ * Completes static imports.
+ *
+ * @author Akash Yadav
+ */
+class StaticImportCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ val root: CompilationUnitTree,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val list = mutableListOf()
+ val trees = Trees.instance(task.task)
+ val methods = mutableMapOf>()
+ val matchRatios: MutableMap = mutableMapOf()
+
+ abortCompletionIfCancelled()
+
+ outer@ for (i in root.imports) {
+ if (!i.isStatic) {
+ continue
+ }
+
+ val id = i.qualifiedIdentifier as MemberSelectTree
+ if (!importMatchesPartial(id.identifier, partial)) {
+ continue
+ }
+
+ val exprPath = trees.getPath(root, id.expression)
+ val type = trees.getElement(exprPath) as TypeElement
+
+ for (member in type.enclosedElements) {
+ if (!member.modifiers.contains(STATIC)) {
+ continue
+ }
+
+ if (!memberMatchesImport(id.identifier, member)) {
+ continue
+ }
+
+ val matchLevel = matchLevel(member.simpleName, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ if (member.kind == METHOD) {
+ putMethod(member as ExecutableElement, methods)
+ matchRatios.putIfAbsent(member.simpleName.toString(), matchLevel)
+ } else {
+ list.add(item(task, member, matchLevel))
+ }
+ if (list.size + methods.size > CompletionProvider.MAX_COMPLETION_ITEMS) {
+ break@outer
+ }
+ }
+ }
+
+ for ((key, value) in methods) {
+ val matchLevel = matchRatios.getOrDefault(key, NO_MATCH)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(method(task, value, !endsWithParen, matchLevel, partial))
+ }
+
+ log.info("...found {} static imports", list.size)
+
+ return CompletionResult(list)
+ }
+
+ private fun importMatchesPartial(
+ staticImport: Name,
+ partial: String,
+ ): Boolean = (staticImport.contentEquals("*") || matchLevel(staticImport, partial) != NO_MATCH)
+
+ private fun memberMatchesImport(
+ staticImport: Name,
+ member: Element,
+ ): Boolean = staticImport.contentEquals("*") || staticImport.contentEquals(member.simpleName)
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt
new file mode 100644
index 0000000000..372f2c7640
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.java.providers.completion
+
+import com.itsaky.androidide.lsp.api.IServerSettings
+import com.itsaky.androidide.lsp.java.compiler.CompileTask
+import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
+import com.itsaky.androidide.lsp.models.CompletionResult
+import com.itsaky.androidide.lsp.models.MatchLevel.NO_MATCH
+import jdkx.lang.model.element.ElementKind.ENUM
+import jdkx.lang.model.element.ElementKind.ENUM_CONSTANT
+import jdkx.lang.model.element.TypeElement
+import jdkx.lang.model.type.DeclaredType
+import openjdk.source.tree.SwitchTree
+import openjdk.source.util.TreePath
+import openjdk.source.util.Trees
+import java.nio.file.Path
+
+/**
+ * Provides completions for switch constants.
+ *
+ * @author Akash Yadav
+ */
+class SwitchConstantCompletionProvider(
+ completingFile: Path,
+ cursor: Long,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+) : IJavaCompletionProvider(cursor, completingFile, compiler, settings) {
+ override fun doComplete(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ val switchTree = path.leaf as SwitchTree
+ val exprPath = TreePath(path, switchTree.expression)
+ val type = Trees.instance(task.task).getTypeMirror(exprPath)
+
+ if (type.kind.isPrimitive || type !is DeclaredType) {
+ // primitive types do not have any members
+ return completeIdentifier(task, exprPath, partial, endsWithParen)
+ }
+
+ val element = type.asElement() as TypeElement
+
+ if (element.kind != ENUM) {
+ // If the switch's expression is not an enum type
+ // we will not get any constants to complete
+ // In this case, we fall back to completing identifiers
+ // At this point, we are sure that the case expression will definitely be an identifier
+ // tree
+ // see visitCase (CaseTree, Long) in FindCompletionsAt.java
+ return completeIdentifier(task, exprPath, partial, endsWithParen)
+ }
+
+ log.info("...complete constants of type {}", type)
+
+ val list: MutableList = ArrayList()
+
+ abortCompletionIfCancelled()
+
+ for (member in task.task.elements.getAllMembers(element)) {
+ if (member.kind != ENUM_CONSTANT) {
+ continue
+ }
+
+ val matchLevel = matchLevel(member.simpleName, partial)
+ if (matchLevel == NO_MATCH) {
+ continue
+ }
+
+ list.add(item(task, member, matchLevel))
+ }
+
+ return CompletionResult(list)
+ }
+
+ private fun completeIdentifier(
+ task: CompileTask,
+ path: TreePath,
+ partial: String,
+ endsWithParen: Boolean,
+ ): CompletionResult {
+ abortCompletionIfCancelled()
+ return IdentifierCompletionProvider(file, cursor, compiler, settings)
+ .complete(task, path, partial, endsWithParen)
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt
similarity index 50%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt
index 6f01deb224..abce3aefd8 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt
@@ -39,52 +39,55 @@ import java.nio.file.Paths
* @author Akash Yadav
*/
class ErroneousDefinitionProvider(
- position: Position,
- completingFile: Path,
- compiler: JavaCompilerService,
- settings: IServerSettings, cancelChecker: ICancelChecker,
+ position: Position,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
+ override fun doFindDefinition(element: Element): List {
+ val name = element.simpleName ?: return DefinitionProvider.NOT_SUPPORTED
+ val parent = element.enclosingElement as? TypeElement ?: return DefinitionProvider.NOT_SUPPORTED
+ val className = parent.qualifiedName.toString()
+ val memberName = name.toString()
+ return findAllMembers(className, memberName)
+ }
- override fun doFindDefinition(element: Element): List {
- val name = element.simpleName ?: return DefinitionProvider.NOT_SUPPORTED
- val parent = element.enclosingElement as? TypeElement ?: return DefinitionProvider.NOT_SUPPORTED
- val className = parent.qualifiedName.toString()
- val memberName = name.toString()
- return findAllMembers(className, memberName)
- }
+ private fun findAllMembers(
+ className: String,
+ memberName: String,
+ ): List {
+ val otherFile = compiler.findAnywhere(className)
+ abortIfCancelled()
+ if (!otherFile.isPresent) {
+ log.error("Cannot find source file for class: {}", className)
+ return emptyList()
+ }
- private fun findAllMembers(className: String, memberName: String): List {
- val otherFile = compiler.findAnywhere(className)
- abortIfCancelled()
- if (!otherFile.isPresent) {
- log.error("Cannot find source file for class: {}", className)
- return emptyList()
- }
+ val fileAsSource = SourceFileObject(file)
+ var sources = listOf(fileAsSource, otherFile.get())
+ if (isSameFile(Paths.get(otherFile.get().toUri()), file)) {
+ sources = listOf(fileAsSource)
+ }
- val fileAsSource = SourceFileObject(file)
- var sources = listOf(fileAsSource, otherFile.get())
- if (isSameFile(Paths.get(otherFile.get().toUri()), file)) {
- sources = listOf(fileAsSource)
- }
+ abortIfCancelled()
- abortIfCancelled()
+ return compiler.compile(sources).get { task ->
+ val locations = mutableListOf()
+ val trees = Trees.instance(task.task)
+ val elements = task.task.elements
+ val parentClass = elements.getTypeElement(className)
- return compiler.compile(sources).get { task ->
- val locations = mutableListOf()
- val trees = Trees.instance(task.task)
- val elements = task.task.elements
- val parentClass = elements.getTypeElement(className)
+ abortIfCancelled()
+ for (member in elements.getAllMembers(parentClass)) {
+ if (!member.simpleName.contentEquals(memberName)) continue
+ val path = trees.getPath(member) ?: continue
+ val location = FindHelper.location(task, path, memberName)
+ abortIfCancelled()
+ locations.add(location)
+ }
- abortIfCancelled()
- for (member in elements.getAllMembers(parentClass)) {
- if (!member.simpleName.contentEquals(memberName)) continue
- val path = trees.getPath(member) ?: continue
- val location = FindHelper.location(task, path, memberName)
- abortIfCancelled()
- locations.add(location)
- }
-
- locations
- }
- }
+ locations
+ }
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt
similarity index 66%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt
index e677208489..c3a2f27db0 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt
@@ -35,33 +35,32 @@ import java.nio.file.Path
* @author Akash Yadav
*/
abstract class IJavaDefinitionProvider(
- protected val position: Position,
- completingFile: Path,
- compiler: JavaCompilerService,
- settings: IServerSettings,
- cancelChecker: ICancelChecker
-) : BaseJavaServiceProvider(completingFile, compiler, settings), ICancelChecker by cancelChecker {
+ protected val position: Position,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ cancelChecker: ICancelChecker,
+) : BaseJavaServiceProvider(completingFile, compiler, settings),
+ ICancelChecker by cancelChecker {
+ protected val line = position.line
+ protected val column = position.column
- protected val line = position.line
- protected val column = position.column
+ companion object {
+ @JvmStatic
+ protected val log: Logger = LoggerFactory.getLogger(IJavaDefinitionProvider::class.java)
+ }
- companion object {
+ /**
+ * Finds the definition for the given element.
+ * @param element The element to find definition for.
+ */
+ fun findDefinition(element: Element?): List {
+ if (element == null) {
+ return DefinitionProvider.NOT_SUPPORTED
+ }
- @JvmStatic
- protected val log: Logger = LoggerFactory.getLogger(IJavaDefinitionProvider::class.java)
- }
+ return doFindDefinition(element)
+ }
- /**
- * Finds the definition for the given element.
- * @param element The element to find definition for.
- */
- fun findDefinition(element: Element?): List {
- if (element == null) {
- return DefinitionProvider.NOT_SUPPORTED
- }
-
- return doFindDefinition(element)
- }
-
- abstract fun doFindDefinition(element: Element): List
+ abstract fun doFindDefinition(element: Element): List
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt
similarity index 65%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt
index 5102b2eec9..65221adda7 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt
@@ -33,28 +33,28 @@ import java.nio.file.Path
* @author Akash Yadav
*/
class LocalDefinitionProvider(
- position: Position,
- completingFile: Path,
- compiler: JavaCompilerService,
- settings: IServerSettings, cancelChecker: ICancelChecker,
+ position: Position,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
+ override fun doFindDefinition(element: Element): List {
+ return compiler.compile(file).get {
+ val trees = Trees.instance(it.task)
+ val path = trees.getPath(element)
+ if (path == null) {
+ log.error("TreePath of element is null. Cannot find definition. Element is {}", element)
+ return@get emptyList()
+ }
- override fun doFindDefinition(element: Element): List {
- return compiler.compile(file).get {
- val trees = Trees.instance(it.task)
- val path = trees.getPath(element)
- if (path == null) {
- log.error("TreePath of element is null. Cannot find definition. Element is {}", element)
- return@get emptyList()
- }
+ var name = element.simpleName
+ if (name.contentEquals("")) {
+ name = element.enclosingElement.simpleName
+ }
- var name = element.simpleName
- if (name.contentEquals("")) {
- name = element.enclosingElement.simpleName
- }
-
- abortIfCancelled()
- return@get listOf(FindHelper.location(it, path, name))
- }
- }
+ abortIfCancelled()
+ return@get listOf(FindHelper.location(it, path, name))
+ }
+ }
}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt
similarity index 75%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt
index da9a574de7..029be55873 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt
@@ -32,24 +32,24 @@ import java.nio.file.Path
* @author Akash Yadav
*/
class RemoteDefinitionProvider(
- position: Position,
- completingFile: Path,
- compiler: JavaCompilerService,
- settings: IServerSettings, cancelChecker: ICancelChecker,
+ position: Position,
+ completingFile: Path,
+ compiler: JavaCompilerService,
+ settings: IServerSettings,
+ cancelChecker: ICancelChecker,
) : IJavaDefinitionProvider(position, completingFile, compiler, settings, cancelChecker) {
+ private lateinit var otherFile: JavaFileObject
- private lateinit var otherFile: JavaFileObject
+ fun setOtherFile(jfo: JavaFileObject): RemoteDefinitionProvider {
+ this.otherFile = jfo
+ return this
+ }
- fun setOtherFile(jfo: JavaFileObject): RemoteDefinitionProvider {
- this.otherFile = jfo
- return this
- }
-
- override fun doFindDefinition(element: Element): List {
+ override fun doFindDefinition(element: Element): List {
// val task = compiler.compile(listOf(SourceFileObject(file), otherFile))
- val provider = LocalDefinitionProvider(position, file, compiler, settings, this)
- return provider.findDefinition(element)
+ val provider = LocalDefinitionProvider(position, file, compiler, settings, this)
+ return provider.findDefinition(element)
// return provider
// .findDefinition(task.get { NavigationHelper.findElement(it, file, line, column) })
- }
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java
new file mode 100644
index 0000000000..21038fd1ad
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java
@@ -0,0 +1,100 @@
+/*
+ * 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.java.rewrite;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.FindHelper;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import jdkx.lang.model.element.ExecutableElement;
+import openjdk.source.util.Trees;
+
+public class AddException extends Rewrite {
+
+ final String className, methodName;
+ final String[] erasedParameterTypes;
+ final String exceptionType;
+
+ public AddException(String className, String methodName, String[] erasedParameterTypes,
+ String exceptionType) {
+ this.className = className;
+ this.methodName = methodName;
+ this.erasedParameterTypes = erasedParameterTypes;
+ this.exceptionType = exceptionType;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ Path file = compiler.findTypeDeclaration(className);
+ if (file == CompilerProvider.NOT_FOUND) {
+ return CANCELLED;
+ }
+
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(task -> {
+ Trees trees = Trees.instance(task.task);
+ final var type = task.task.getElements().getTypeElement(className);
+ if (type == null) {
+ return CANCELLED;
+ }
+
+ ExecutableElement methodElement = FindHelper.findMethod(task, className, methodName,
+ erasedParameterTypes);
+ if (methodElement == null) {
+ return CANCELLED;
+ }
+
+ final var methodTree = trees.getTree(methodElement);
+ if (methodTree == null || methodTree.getBody() == null) {
+ return CANCELLED;
+ }
+
+ final var pos = trees.getSourcePositions();
+ final var lines = task.root().getLineMap();
+
+ final var index = pos.getStartPosition(task.root(), methodTree.getBody());
+ int line = (int) lines.getLineNumber(index);
+ int column = (int) lines.getColumnNumber(index);
+ Position insertPos = new Position(line - 1, column - 1);
+ String simpleName = exceptionType;
+ int lastDot = simpleName.lastIndexOf('.');
+ if (lastDot != -1) {
+ simpleName = exceptionType.substring(lastDot + 1);
+ }
+
+ String insertText;
+ if (methodTree.getThrows().isEmpty()) {
+ insertText = "throws " + simpleName + " ";
+ } else {
+ insertText = ", " + simpleName + " ";
+ }
+
+ TextEdit insertThrows = new TextEdit(new Range(insertPos, insertPos), insertText);
+ // TODO add import if needed
+ TextEdit[] edits = {insertThrows};
+ return Collections.singletonMap(file, edits);
+ });
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java
similarity index 68%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java
index ae03d08be0..e69e09fa13 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java
@@ -31,23 +31,23 @@
public class AddImport extends Rewrite {
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public final String className;
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public final String className;
- final Path file;
+ final Path file;
- public AddImport(Path file, String className) {
- this.file = file;
- this.className = className;
- }
+ public AddImport(Path file, String className) {
+ this.file = file;
+ this.className = className;
+ }
- @NonNull
- @Override
- public Map rewrite(@NonNull CompilerProvider compiler) {
- final ParseTask task = compiler.parse(file);
- Position point = InsertUtilsKt.positionForImports(className, task);
- String text = "import " + className + ";\n";
- return Collections.singletonMap(
- file, new TextEdit[] {new TextEdit(new Range(point, point), text)});
- }
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ final ParseTask task = compiler.parse(file);
+ Position point = InsertUtilsKt.positionForImports(className, task);
+ String text = "import " + className + ";\n";
+ return Collections.singletonMap(
+ file, new TextEdit[]{new TextEdit(new Range(point, point), text)});
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java
new file mode 100644
index 0000000000..057f8e01de
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java
@@ -0,0 +1,78 @@
+/*
+ * This file is part of AndroidIDE.
+ *
+ * AndroidIDE is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AndroidIDE is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AndroidIDE. If not, see .
+ */
+
+package com.itsaky.androidide.lsp.java.rewrite;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.FindHelper;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.preferences.utils.EditorUtilKt;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import openjdk.source.util.Trees;
+
+public class AddSuppressWarningAnnotation extends Rewrite {
+
+ final String className, methodName;
+ final String[] erasedParameterTypes;
+
+ public AddSuppressWarningAnnotation(
+ String className, String methodName, String[] erasedParameterTypes) {
+ this.className = className;
+ this.methodName = methodName;
+ this.erasedParameterTypes = erasedParameterTypes;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ Path file = compiler.findTypeDeclaration(className);
+ if (file == CompilerProvider.NOT_FOUND) {
+ return CANCELLED;
+ }
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(
+ task -> {
+ final var trees = Trees.instance(task.task);
+ final var methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes);
+ if (methodElement == null) {
+ return CANCELLED;
+ }
+ final var methodTree = trees.getTree(methodElement);
+ if (methodTree == null) {
+ return CANCELLED;
+ }
+ final var startMethod = (int) trees.getSourcePositions()
+ .getStartPosition(task.root(), methodTree);
+ final var lines = task.root().getLineMap();
+ final var line = (int) lines.getLineNumber(startMethod);
+ final var column = (int) lines.getColumnNumber(startMethod);
+ final var startLine = (int) lines.getStartPosition(line);
+ final var indent = EditorUtilKt.indentationString(startMethod - startLine);
+ final var insertText = "@SuppressWarnings(\"unchecked\")\n" + indent;
+ final var insertPoint = new Position(line - 1, column - 1);
+ final var edits = new TextEdit[]{
+ new TextEdit(new Range(insertPoint, insertPoint), insertText)};
+ return Collections.singletonMap(file, edits);
+ });
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java
new file mode 100644
index 0000000000..4e8e4aaabc
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java
@@ -0,0 +1,85 @@
+/*
+ * 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.java.rewrite;
+
+import static com.itsaky.androidide.lsp.java.rewrite.ConvertVariableToStatement.findVariable;
+import static com.itsaky.androidide.lsp.java.rewrite.ConvertVariableToStatement.isExpressionStatement;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.parser.ParseTask;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import jdkx.lang.model.element.Modifier;
+import openjdk.source.tree.ExpressionTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.Trees;
+
+public class ConvertFieldToBlock extends Rewrite {
+ final Path file;
+ final int position;
+
+ public ConvertFieldToBlock(Path file, int position) {
+ this.file = file;
+ this.position = position;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ ParseTask task = compiler.parse(file);
+ Trees trees = Trees.instance(task.task);
+ SourcePositions pos = trees.getSourcePositions();
+ LineMap lines = task.root.getLineMap();
+ VariableTree variable = findVariable(task, position);
+ if (variable == null) {
+ return CANCELLED;
+ }
+ ExpressionTree expression = variable.getInitializer();
+ if (!isExpressionStatement(expression)) {
+ return CANCELLED;
+ }
+ long start = pos.getStartPosition(task.root, variable);
+ long end = pos.getStartPosition(task.root, expression);
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ Range deleteLhs = new Range(startPos, endPos);
+ TextEdit fixLhs = new TextEdit(deleteLhs, "{ ");
+ if (variable.getModifiers().getFlags().contains(Modifier.STATIC)) {
+ fixLhs.setNewText("static { ");
+ }
+ long right = pos.getEndPosition(task.root, variable);
+ int rightLine = (int) lines.getLineNumber(right);
+ int rightColumn = (int) lines.getColumnNumber(right);
+ Position rightPos = new Position(rightLine - 1, rightColumn - 1);
+ Range insertRight = new Range(rightPos, rightPos);
+ TextEdit fixRhs = new TextEdit(insertRight, " }");
+ TextEdit[] edits = {fixLhs, fixRhs};
+ return Collections.singletonMap(file, edits);
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java
new file mode 100644
index 0000000000..e054505d8c
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java
@@ -0,0 +1,100 @@
+/*
+ * 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.java.rewrite;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.parser.ParseTask;
+import com.itsaky.androidide.lsp.java.visitors.FindVariableAtCursor;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import openjdk.source.tree.ExpressionTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.Tree;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.Trees;
+
+public class ConvertVariableToStatement extends Rewrite {
+ static VariableTree findVariable(ParseTask task, int position) {
+ return new FindVariableAtCursor(task.task).scan(task.root, position);
+ }
+
+ /** https://docs.oracle.com/javase/specs/jls/se13/html/jls-14.html#jls-14.8 */
+ static boolean isExpressionStatement(Tree t) {
+ if (t == null)
+ return false;
+ switch (t.getKind()) {
+ case ASSIGNMENT:
+ case PREFIX_INCREMENT:
+ case PREFIX_DECREMENT:
+ case POSTFIX_INCREMENT:
+ case POSTFIX_DECREMENT:
+ case METHOD_INVOCATION:
+ case NEW_CLASS:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ final Path file;
+
+ final int position;
+
+ public ConvertVariableToStatement(Path file, int position) {
+ this.file = file;
+ this.position = position;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ final ParseTask task = compiler.parse(file);
+ final Trees trees = Trees.instance(task.task);
+ final SourcePositions pos = trees.getSourcePositions();
+ final LineMap lines = task.root.getLineMap();
+ final VariableTree variable = findVariable(task, position);
+ if (variable == null) {
+ return CANCELLED;
+ }
+ ExpressionTree expression = variable.getInitializer();
+ if (expression == null) {
+ return CANCELLED;
+ }
+ if (!isExpressionStatement(expression)) {
+ return CANCELLED;
+ }
+ long start = pos.getStartPosition(task.root, variable);
+ long end = pos.getStartPosition(task.root, expression);
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ Range delete = new Range(startPos, endPos);
+ TextEdit edit = new TextEdit(delete, "");
+ TextEdit[] edits = {edit};
+ return Collections.singletonMap(file, edits);
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java
new file mode 100644
index 0000000000..f96361fbef
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java
@@ -0,0 +1,275 @@
+/*
+ * 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.java.rewrite;
+
+import static com.itsaky.androidide.lsp.java.utils.EditHelper.indent;
+import static com.itsaky.androidide.lsp.java.utils.EditHelper.insertAfter;
+import static com.itsaky.androidide.lsp.java.utils.EditHelper.insertAtEndOfClass;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.EditHelper;
+import com.itsaky.androidide.lsp.java.visitors.FindMethodCallAt;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.preferences.internal.EditorPreferences;
+import com.itsaky.androidide.preferences.utils.EditorUtilKt;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Map;
+import java.util.StringJoiner;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.Name;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.type.TypeMirror;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.ExpressionTree;
+import openjdk.source.tree.IdentifierTree;
+import openjdk.source.tree.MemberReferenceTree;
+import openjdk.source.tree.MemberSelectTree;
+import openjdk.source.tree.MethodInvocationTree;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CreateMissingMethod extends Rewrite {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CreateMissingMethod.class);
+ private static final String TODO_COMMENT = "// TODO: Implement this method";
+ final Path file;
+ final int position;
+ int argCount = -1;
+
+ public CreateMissingMethod(Path file, int position) {
+ this.file = file;
+ this.position = position;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(task -> {
+ final Trees trees = Trees.instance(task.task);
+ final FindMethodCallAt methodFinder = new FindMethodCallAt(task.task);
+ final MethodInvocationTree call = methodFinder.scan(task.root(), position);
+ if (call == null || file == null) {
+ return CANCELLED;
+ }
+
+ final TreePath path = trees.getPath(task.root(), call);
+ final String returnType = methodFinder.getReturnType();
+ Path sourceFile = file;
+ // null if the call is inside a field/static initializer rather than a method body --
+ // there's no enclosing MethodTree to report modifiers from or insert relative to.
+ MethodTree currentMethod = surroundingMethod(path);
+ final var insertTextBuilder = new StringBuilder("\n");
+
+ final var indent = EditorUtilKt.getIndentationString();
+
+ final var isStatic = (currentMethod != null
+ && currentMethod.getModifiers().getFlags().contains(Modifier.STATIC)) ||
+ methodFinder.isStaticAccess();
+
+ insertTextBuilder.append(
+ printMethodHeader(task, call, returnType, methodFinder.isMemberSelect(), isStatic))
+ .append(" {\n")
+ .append(indent)
+ .append(TODO_COMMENT)
+ .append("\n")
+ .append(indent)
+ .append(createReturnStatement(returnType))
+ .append("\n")
+ .append("}");
+
+ var insertText = insertTextBuilder.toString();
+
+ final CompilationUnitTree compilationUnit;
+ final ClassTree enclosingClass;
+ final Position insertPoint;
+
+ if (methodFinder.isMemberSelect()) {
+ // Accessing method from another class
+ compilationUnit = methodFinder.getEnclosingTreePath().getCompilationUnit();
+ enclosingClass = methodFinder.getEnclosingClass();
+ insertPoint = insertAtEndOfClass(task.task, compilationUnit, enclosingClass);
+ sourceFile = Paths.get(compilationUnit.getSourceFile().toUri());
+ } else {
+ if (currentMethod == null) {
+ return CANCELLED;
+ }
+ compilationUnit = task.root();
+ enclosingClass = surroundingClass(path);
+ insertPoint = insertAfter(task.task, compilationUnit, currentMethod);
+ }
+
+ final int indentSpaces = indent(task.task, compilationUnit, enclosingClass) + EditorPreferences.INSTANCE.getTabSize();
+ insertText = insertText.replaceAll("\n", "\n" + EditorUtilKt.indentationString(indentSpaces));
+ insertText += "\n";
+
+ final var edits = new TextEdit[]{
+ new TextEdit(new Range(insertPoint, insertPoint), insertText)};
+ return Collections.singletonMap(sourceFile, edits);
+ });
+ }
+
+ private String createReturnStatement(String returnType) {
+ if (returnType == null) {
+ return "";
+ }
+ String value;
+ switch (returnType) {
+ case "int":
+ case "byte":
+ case "short":
+ case "long":
+ case "char":
+ value = "0";
+ break;
+ case "float":
+ value = "0f";
+ break;
+ case "double":
+ value = "0.0";
+ break;
+ case "boolean":
+ value = "false";
+ break;
+
+ // Finding type of variable declaration may result in an error
+ // We should then simply return empty return type
+ case "(ERROR)":
+ return ""; // Directly return empty string
+ default:
+ value = "null";
+ break;
+ }
+ return String.format("return %s;", value);
+ }
+
+ private String extractMethodName(ExpressionTree method) {
+ if (method instanceof IdentifierTree) {
+ IdentifierTree id = (IdentifierTree) method;
+ return id.getName().toString();
+ } else if (method instanceof MemberSelectTree) {
+ MemberSelectTree select = (MemberSelectTree) method;
+ return select.getIdentifier().toString();
+ } else {
+ return "extractedMethod";
+ }
+ }
+
+ private String guessParameterName(Tree argument, TypeMirror type) {
+ String fromTree = guessParameterNameFromTree(argument);
+ if (!fromTree.isEmpty()) {
+ return fromTree;
+ }
+
+ String fromType = guessParameterNameFromType(type);
+ if (!fromType.isEmpty()) {
+ return fromType;
+ }
+
+ argCount++;
+ return "param" + argCount;
+ }
+
+ private String guessParameterNameFromTree(Tree argument) {
+ if (argument instanceof IdentifierTree) {
+ IdentifierTree id = (IdentifierTree) argument;
+ return id.getName().toString();
+ } else if (argument instanceof MemberSelectTree) {
+ MemberSelectTree select = (MemberSelectTree) argument;
+ return select.getIdentifier().toString();
+ } else if (argument instanceof MemberReferenceTree) {
+ MemberReferenceTree reference = (MemberReferenceTree) argument;
+ return reference.getName().toString();
+ } else {
+ return "";
+ }
+ }
+
+ private String guessParameterNameFromType(TypeMirror type) {
+ if (type instanceof DeclaredType) {
+ DeclaredType declared = (DeclaredType) type;
+ // An anonymous class's simple name is empty per the language spec -- fall through to
+ // argCount-based naming (see guessParameterName) instead of throwing on charAt(0).
+ Name name = declared.asElement().getSimpleName();
+ if (name.length() == 0) {
+ return "";
+ }
+ return "" + Character.toLowerCase(name.charAt(0)) + name.subSequence(1, name.length());
+ } else {
+ return "";
+ }
+ }
+
+ private String printMethodHeader(CompileTask task, MethodInvocationTree call, String type,
+ boolean isMemberSelect, boolean isStatic) {
+ String methodName = extractMethodName(call.getMethodSelect());
+ String returnType = type == null || "(ERROR)".equals(type) ? "void" : type;
+ LOG.info("Creating missing method '{}' with return type: {}", methodName, returnType);
+ String parameters = printParameters(task, call);
+ String modifiers = isMemberSelect ? "public" : "private";
+ if (isStatic) {
+ modifiers += " static";
+ }
+ return modifiers + " " + returnType + " " + methodName + "(" + parameters + ")";
+ }
+
+ private String printParameters(CompileTask task, MethodInvocationTree call) {
+ Trees trees = Trees.instance(task.task);
+ StringJoiner join = new StringJoiner(", ");
+ for (int i = 0; i < call.getArguments().size(); i++) {
+ TypeMirror type = trees.getTypeMirror(trees.getPath(task.root(), call.getArguments().get(i)));
+ String name = guessParameterName(call.getArguments().get(i), type);
+ String argType = EditHelper.printType(type);
+ join.add(String.format("final %s %s", argType, name));
+ }
+ return join.toString();
+ }
+
+ private ClassTree surroundingClass(TreePath call) {
+ while (call != null) {
+ if (call.getLeaf() instanceof ClassTree) {
+ return (ClassTree) call.getLeaf();
+ }
+ call = call.getParentPath();
+ }
+ throw new RuntimeException("No surrounding class");
+ }
+
+ /** Returns {@code null} if {@code call} has no enclosing method (e.g. a field/static initializer). */
+ private MethodTree surroundingMethod(TreePath call) {
+ while (call != null) {
+ if (call.getLeaf() instanceof MethodTree) {
+ return (MethodTree) call.getLeaf();
+ }
+ call = call.getParentPath();
+ }
+ return null;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java
new file mode 100644
index 0000000000..e822a5db56
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java
@@ -0,0 +1,182 @@
+/*
+ * 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.java.rewrite;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.EditHelper;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.preferences.internal.EditorPreferences;
+import com.itsaky.androidide.preferences.utils.EditorUtilKt;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.TypeElement;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.Trees;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GenerateRecordConstructor extends Rewrite {
+
+ private static final Logger LOG = LoggerFactory.getLogger(GenerateRecordConstructor.class);
+ final String className;
+
+ public GenerateRecordConstructor(String className) {
+ this.className = className;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ LOG.info("Generate default constructor for {}...", className);
+ // TODO this needs to fall back on looking for inner classes and package-private classes
+ Path file = compiler.findTypeDeclaration(className);
+
+ if (file == CompilerProvider.NOT_FOUND) {
+ LOG.warn("Unable to find source file for class: {}", this.className);
+ return CANCELLED;
+ }
+
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(
+ task -> {
+ TypeElement typeElement = task.task.getElements().getTypeElement(className);
+ if (typeElement == null) {
+ LOG.warn("Unable to resolve type element for class: {}", this.className);
+ return CANCELLED;
+ }
+ ClassTree typeTree = Trees.instance(task.task).getTree(typeElement);
+ if (typeTree == null) {
+ LOG.warn("Unable to resolve class tree for class: {}", this.className);
+ return CANCELLED;
+ }
+ List fields = fieldsNeedingInitialization(typeTree);
+ String parameters = generateParameters(task, fields);
+ String initializers = generateInitializers(fields);
+ StringBuilder buf = new StringBuilder();
+ buf.append("\n");
+ if (typeTree.getModifiers().getFlags().contains(Modifier.PUBLIC)) {
+ buf.append("public ");
+ }
+
+ buf.append(simpleName(className))
+ .append("(")
+ .append(parameters)
+ .append(") {\n ")
+ .append(initializers)
+ .append("\n}");
+ String string = buf.toString();
+ int indent = EditHelper.indent(task.task, task.root(), typeTree)
+ + EditorPreferences.INSTANCE.getTabSize();
+ string = string.replaceAll("\n", "\n" + EditorUtilKt.indentationString(indent));
+ string = string + "\n\n";
+ Position insert = insertPoint(task, typeTree);
+ TextEdit[] edits = {new TextEdit(new Range(insert, insert), string)};
+ return Collections.singletonMap(file, edits);
+ });
+ }
+
+ private CharSequence extract(CompileTask task, Tree typeTree) {
+ try {
+ CharSequence contents = task.root().getSourceFile().getCharContent(true);
+ SourcePositions pos = Trees.instance(task.task).getSourcePositions();
+ int start = (int) pos.getStartPosition(task.root(), typeTree);
+ int end = (int) pos.getEndPosition(task.root(), typeTree);
+ return contents.subSequence(start, end);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List fieldsNeedingInitialization(ClassTree typeTree) {
+ List fields = new ArrayList<>();
+ for (Tree member : typeTree.getMembers()) {
+ if (!(member instanceof VariableTree)) {
+ continue;
+ }
+ VariableTree field = (VariableTree) member;
+ if (field.getInitializer() != null) {
+ continue;
+ }
+ Set flags = field.getModifiers().getFlags();
+ if (flags.contains(Modifier.STATIC)) {
+ continue;
+ }
+ if (!flags.contains(Modifier.FINAL)) {
+ continue;
+ }
+ fields.add(field);
+ }
+
+ return fields;
+ }
+
+ private String generateInitializers(List fields) {
+ StringJoiner join = new StringJoiner("\n ");
+ for (VariableTree f : fields) {
+ join.add("this." + f.getName() + " = " + f.getName() + ";");
+ }
+ return join.toString();
+ }
+
+ private String generateParameters(CompileTask task, List fields) {
+ StringJoiner join = new StringJoiner(", ");
+ for (VariableTree f : fields) {
+ join.add(extract(task, f.getType()) + " " + f.getName());
+ }
+ return join.toString();
+ }
+
+ private Position insertPoint(CompileTask task, ClassTree typeTree) {
+ for (Tree member : typeTree.getMembers()) {
+ if (member.getKind() == Tree.Kind.METHOD) {
+ MethodTree method = (MethodTree) member;
+ if (method.getReturnType() == null) {
+ continue;
+ }
+ LOG.info("...insert constructor before {}", method.getName());
+ return EditHelper.insertBefore(task.task, task.root(), method);
+ }
+ }
+ LOG.info("...insert constructor at end of class");
+ return EditHelper.insertAtEndOfClass(task.task, task.root(), typeTree);
+ }
+
+ private String simpleName(String className) {
+ int dot = className.lastIndexOf('.');
+ if (dot != -1) {
+ return className.substring(dot + 1);
+ }
+ return className;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java
new file mode 100644
index 0000000000..a6f40094cb
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java
@@ -0,0 +1,177 @@
+/*
+ * 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.java.rewrite;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.EditHelper;
+import com.itsaky.androidide.lsp.java.utils.JavaPoetUtils;
+import com.itsaky.androidide.lsp.java.visitors.FindAnonymousTypeDeclaration;
+import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt;
+import com.itsaky.androidide.lsp.models.CodeActionItem;
+import com.itsaky.androidide.lsp.models.Command;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.preferences.internal.EditorPreferences;
+import com.itsaky.androidide.preferences.utils.EditorUtilKt;
+import com.squareup.javapoet.MethodSpec;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.ElementKind;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.util.Elements;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.ImportTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.util.Trees;
+import openjdk.tools.javac.util.JCDiagnostic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ImplementAbstractMethods extends Rewrite {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ImplementAbstractMethods.class);
+ private final String className;
+ private final String classFile;
+ private final long position;
+
+ public ImplementAbstractMethods(@NonNull JCDiagnostic diagnostic) {
+ Object[] args = diagnostic.getArgs();
+ String className = args[0].toString();
+
+ if (!className.contains(" rewrite(@NonNull CompilerProvider compiler) {
+ final Path file = compiler.findTypeDeclaration(this.classFile);
+ if (file == CompilerProvider.NOT_FOUND) {
+ LOG.warn("Unable to find source file for class: {} classFile={}", this.className,
+ this.classFile);
+ return CANCELLED;
+ }
+
+ final SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(
+ task -> {
+ StringJoiner insertText = new StringJoiner("\n");
+ Elements elements = task.task.getElements();
+ Trees trees = Trees.instance(task.task);
+ TypeElement thisClass = elements.getTypeElement(this.className);
+
+ ClassTree thisTree = getClassTree(task, file);
+ if (thisTree == null) {
+ thisTree = trees.getTree(thisClass);
+ }
+
+ final Set imports = new TreeSet<>();
+ int indent = EditHelper.indent(task.task, task.root(), thisTree)
+ + EditorPreferences.INSTANCE.getTabSize();
+ for (Element member : elements.getAllMembers(thisClass)) {
+ if (member.getKind() == ElementKind.METHOD
+ && member.getModifiers().contains(Modifier.ABSTRACT)) {
+ ExecutableElement method = (ExecutableElement) member;
+ final MethodSpec methodSpec = MethodSpec.overriding(method).build();
+ String text = "\n" + JavaPoetUtils.print(methodSpec, imports, false);
+ text = text.replaceAll("\n", "\n" + EditorUtilKt.indentationString(indent));
+ insertText.add(text);
+ }
+ }
+
+ Position insert = EditHelper.insertAtEndOfClass(task.task, task.root(), thisTree);
+ final List edits = new ArrayList<>();
+ edits.add(new TextEdit(new Range(insert, insert), insertText + "\n"));
+ addImports(compiler, task, file, imports, edits);
+
+ return Collections.singletonMap(file, edits.toArray(new TextEdit[0]));
+ });
+ }
+
+ @Override
+ protected void finalizeCodeAction(@NonNull CodeActionItem action) {
+ action.setCommand(new Command("Format code", Command.FORMAT_CODE));
+ }
+
+ private void addImports(
+ CompilerProvider compiler,
+ CompileTask task,
+ Path file,
+ Set imports,
+ List edits) {
+ imports = imports.stream().filter(name -> !name.startsWith("java.lang.")).collect(Collectors.toSet());
+ for (String name : imports) {
+ final List importEdits = EditHelper.addImportIfNeeded(compiler, file, getFileImports(task, file), name);
+ if (importEdits != null && !importEdits.isEmpty()) {
+ edits.addAll(importEdits);
+ }
+ }
+ }
+
+ @Nullable
+ private ClassTree getClassTree(@NonNull CompileTask task, Path file) {
+ ClassTree thisTree = null;
+ CompilationUnitTree root = task.root(file);
+ if (root == null) {
+ return null;
+ }
+
+ if (position != 0) {
+ final FindTypeDeclarationAt scanner = new FindTypeDeclarationAt(task.task);
+ thisTree = scanner.scan(root, position);
+ }
+
+ if (thisTree == null) {
+ final FindAnonymousTypeDeclaration scanner = new FindAnonymousTypeDeclaration(task.task, root);
+ thisTree = scanner.scan(root, position);
+ }
+
+ return thisTree;
+ }
+
+ private Set getFileImports(@NonNull CompileTask task, Path file) {
+ return task.root(file).getImports().stream()
+ .map(ImportTree::getQualifiedIdentifier)
+ .map(Tree::toString)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java
similarity index 70%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java
index 0adbf192dd..fa4116c91f 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java
@@ -29,20 +29,20 @@
import openjdk.source.tree.ClassTree;
public class RemoveClass extends Rewrite {
- final Path file;
- final int position;
+ final Path file;
+ final int position;
- public RemoveClass(Path file, int position) {
- this.file = file;
- this.position = position;
- }
+ public RemoveClass(Path file, int position) {
+ this.file = file;
+ this.position = position;
+ }
- @NonNull
- @Override
- public Map rewrite(@NonNull CompilerProvider compiler) {
- ParseTask task = compiler.parse(file);
- final ClassTree type = new FindTypeDeclarationAt(task.task).scan(task.root, (long) position);
- TextEdit[] edits = {EditHelper.removeTree(task.task, task.root, type)};
- return Collections.singletonMap(file, edits);
- }
+ @NonNull
+ @Override
+ public Map rewrite(@NonNull CompilerProvider compiler) {
+ ParseTask task = compiler.parse(file);
+ final ClassTree type = new FindTypeDeclarationAt(task.task).scan(task.root, (long) position);
+ TextEdit[] edits = {EditHelper.removeTree(task.task, task.root, type)};
+ return Collections.singletonMap(file, edits);
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java
new file mode 100644
index 0000000000..f71b1e2e24
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java
@@ -0,0 +1,202 @@
+/*
+ * 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.java.rewrite;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
+import com.itsaky.androidide.lsp.java.utils.FindHelper;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.DeclaredType;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.ExpressionTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.util.JavacTask;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RemoveException extends Rewrite {
+
+ private static final Pattern THROWS = Pattern.compile("\\s*\\bthrows\\b");
+ private static final Logger LOG = LoggerFactory.getLogger(RemoveException.class);
+ final String className, methodName;
+ final String[] erasedParameterTypes;
+
+ final String exceptionType;
+
+ public RemoveException(
+ String className, String methodName, String[] erasedParameterTypes, String exceptionType) {
+ this.className = className;
+ this.methodName = methodName;
+ this.erasedParameterTypes = erasedParameterTypes;
+ this.exceptionType = exceptionType;
+ }
+
+ @NonNull
+ @Override
+ public Map rewrite(CompilerProvider compiler) {
+ Path file = compiler.findTypeDeclaration(className);
+
+ if (file == CompilerProvider.NOT_FOUND) {
+ LOG.warn("Unable to find source file for class: {}", this.className);
+ return CANCELLED;
+ }
+
+ SynchronizedTask synchronizedTask = compiler.compile(file);
+ return synchronizedTask.get(
+ task -> {
+ final var methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes);
+ if (methodElement == null) {
+ return CANCELLED;
+ }
+
+ final var methodTree = Trees.instance(task.task).getTree(methodElement);
+ if (methodTree == null) {
+ return CANCELLED;
+ }
+
+ if (methodTree.getThrows().size() == 1) {
+ TextEdit delete = removeEntireThrows(task.task, task.root(), methodTree);
+ if (delete == TextEdit.NONE) {
+ return CANCELLED;
+ }
+
+ TextEdit[] edits = {delete};
+ return Collections.singletonMap(file, edits);
+ }
+ TextEdit[] edits = {removeSingleException(task.task, task.root(), methodTree)};
+ return Collections.singletonMap(file, edits);
+ });
+ }
+
+ private CharSequence contents(CompilationUnitTree root) {
+ try {
+ return root.getSourceFile().getCharContent(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private int findNamedException(JavacTask task, CompilationUnitTree root, MethodTree method) {
+ Trees trees = Trees.instance(task);
+ for (int i = 0; i < method.getThrows().size(); i++) {
+ ExpressionTree e = method.getThrows().get(i);
+ TreePath path = trees.getPath(root, e);
+ DeclaredType type = (DeclaredType) trees.getTypeMirror(path);
+ TypeElement el = (TypeElement) type.asElement();
+ if (el.getQualifiedName().contentEquals(exceptionType)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private TextEdit removeEntireThrows(JavacTask task, CompilationUnitTree root, MethodTree method) {
+ Trees trees = Trees.instance(task);
+ SourcePositions pos = trees.getSourcePositions();
+ int startMethod = (int) pos.getStartPosition(root, method);
+ CharSequence contents;
+ try {
+ contents = root.getSourceFile().getCharContent(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ Matcher matcher = THROWS.matcher(contents);
+ if (!matcher.find(startMethod)) {
+ return TextEdit.NONE;
+ }
+
+ LineMap lines = root.getLineMap();
+ int start = matcher.start();
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ ExpressionTree lastException = method.getThrows().get(method.getThrows().size() - 1);
+ int end = (int) pos.getEndPosition(root, lastException);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ return new TextEdit(new Range(startPos, endPos), "");
+ }
+
+ private int removeLeadingComma(CompilationUnitTree root, long start) {
+ CharSequence contents = contents(root);
+ for (int i = (int) start; i > 0; i--) {
+ if (contents.charAt(i) == ',') {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private TextEdit removeSingleException(
+ JavacTask task, CompilationUnitTree root, MethodTree method) {
+ int i = findNamedException(task, root, method);
+ if (i == -1) {
+ return TextEdit.NONE;
+ }
+
+ Trees trees = Trees.instance(task);
+ SourcePositions pos = trees.getSourcePositions();
+ ExpressionTree exn = method.getThrows().get(i);
+ long start = pos.getStartPosition(root, exn);
+ long end = pos.getEndPosition(root, exn);
+ if (i == 0) {
+ end = removeTrailingComma(root, end);
+ } else {
+ start = removeLeadingComma(root, start);
+ }
+
+ LineMap lines = root.getLineMap();
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ return new TextEdit(new Range(startPos, endPos), "");
+ }
+
+ private int removeTrailingComma(CompilationUnitTree root, long end) {
+ CharSequence contents = contents(root);
+ for (int i = (int) end; i < contents.length(); i++) {
+ if (contents.charAt(i) == ',') {
+ if (i + 1 < contents.length() && contents.charAt(i + 1) == ' ') {
+ return i + 2;
+ } else {
+ return i + 1;
+ }
+ }
+ }
+ return -1;
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java
similarity index 50%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java
index 8488bd29e2..4fdb0e19eb 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java
@@ -28,40 +28,39 @@
public class RemoveMethod extends Rewrite {
- final String className, methodName;
- final String[] erasedParameterTypes;
+ final String className, methodName;
+ final String[] erasedParameterTypes;
- public RemoveMethod(String className, String methodName, String[] erasedParameterTypes) {
- this.className = className;
- this.methodName = methodName;
- this.erasedParameterTypes = erasedParameterTypes;
- }
+ public RemoveMethod(String className, String methodName, String[] erasedParameterTypes) {
+ this.className = className;
+ this.methodName = methodName;
+ this.erasedParameterTypes = erasedParameterTypes;
+ }
- @NonNull
- @Override
- public Map rewrite(CompilerProvider compiler) {
- Path file = compiler.findTypeDeclaration(className);
- if (file == CompilerProvider.NOT_FOUND) {
- return CANCELLED;
- }
+ @NonNull
+ @Override
+ public Map rewrite(CompilerProvider compiler) {
+ Path file = compiler.findTypeDeclaration(className);
+ if (file == CompilerProvider.NOT_FOUND) {
+ return CANCELLED;
+ }
- return compiler
- .compile(file)
- .get(
- task -> {
- final var methodElement =
- FindHelper.findMethod(task, className, methodName, erasedParameterTypes);
- if (methodElement == null) {
- return CANCELLED;
- }
+ return compiler
+ .compile(file)
+ .get(
+ task -> {
+ final var methodElement = FindHelper.findMethod(task, className, methodName, erasedParameterTypes);
+ if (methodElement == null) {
+ return CANCELLED;
+ }
- final var methodTree = Trees.instance(task.task).getTree(methodElement);
- if (methodTree == null) {
- return CANCELLED;
- }
+ final var methodTree = Trees.instance(task.task).getTree(methodElement);
+ if (methodTree == null) {
+ return CANCELLED;
+ }
- TextEdit[] edits = {EditHelper.removeTree(task.task, task.root(), methodTree)};
- return Collections.singletonMap(file, edits);
- });
- }
+ TextEdit[] edits = {EditHelper.removeTree(task.task, task.root(), methodTree)};
+ return Collections.singletonMap(file, edits);
+ });
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt
new file mode 100644
index 0000000000..eb2e84fdd8
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.java.rewrite
+
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider
+import com.itsaky.androidide.lsp.models.CodeActionItem
+import com.itsaky.androidide.lsp.models.CodeActionKind
+import com.itsaky.androidide.lsp.models.DocumentChange
+import com.itsaky.androidide.lsp.models.TextEdit
+import java.nio.file.Path
+
+/**
+ * A source code rewrite.
+ *
+ * @author Akash Yadav
+ */
+abstract class Rewrite {
+ /**
+* Converts the edits to code action item.
+*
+* @param compiler The compiler service.
+* @param title The title for the code action.
+* @return The code action item.
+*/
+ fun asCodeActions(
+ compiler: CompilerProvider,
+ title: String,
+ ): CodeActionItem? {
+ val edits = rewrite(compiler)
+ if (edits.isEmpty()) {
+ return null
+ }
+
+ val changes: MutableList = ArrayList(0)
+ for (file in edits.keys) {
+ val textEdits = edits[file] ?: continue
+ val change = DocumentChange()
+ change.file = file
+ change.edits = textEdits.asList()
+ changes.add(change)
+ }
+ val action = CodeActionItem()
+ action.title = title
+ action.kind = CodeActionKind.QuickFix
+ action.changes = changes
+ finalizeCodeAction(action)
+ return action
+ }
+
+/**
+* Perform a rewrite across the entire codebase. The given compiler can be used for anything
+* except compiling other files. If you try to compile any file, the current thread will be
+* blocked.
+*
+* @param compiler The compiler.
+*/
+ abstract fun rewrite(compiler: CompilerProvider): Map>
+
+/**
+* Called after the code action is created. Subclasses can implement this to do some finalization
+* tasks on the given code action.
+*
+* @param action The code action.
+*/
+ protected open fun finalizeCodeAction(action: CodeActionItem) {}
+
+ companion object {
+ /** CANCELLED signals that the rewrite couldn't be completed. */
+ @JvmField val CANCELLED = emptyMap>()
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java
new file mode 100644
index 0000000000..620e270158
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java
@@ -0,0 +1,143 @@
+/*
+ * 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.java.utils;
+
+import androidx.annotation.NonNull;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import openjdk.source.tree.LineMap;
+import openjdk.tools.javac.parser.Scanner;
+import openjdk.tools.javac.parser.ScannerFactory;
+import openjdk.tools.javac.parser.Tokens;
+import openjdk.tools.javac.parser.Tokens.TokenKind;
+import openjdk.tools.javac.util.Context;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * @author Akash Yadav
+ */
+public class ASTFixer {
+ public static final String IDENT = "I_N_J_E_C_T_E_D";
+
+ private static final Set MEMBER_SELECTION_TOKENS = ImmutableSet.of(
+ Tokens.TokenKind.IDENTIFIER,
+ Tokens.TokenKind.LT,
+ TokenKind.NEW,
+ TokenKind.THIS,
+ TokenKind.SUPER,
+ TokenKind.CLASS,
+ TokenKind.STAR);
+ private static final Set INVALID_SELECTION_SUFFIXES = ImmutableSet.of(TokenKind.RBRACE);
+
+ private final Context context;
+
+ public ASTFixer(Context context) {
+ this.context = context;
+ }
+
+ public StringBuilder fix(CharSequence content) {
+ Scanner scanner = ScannerFactory.instance(context).newScanner(content, true);
+ List edits = new ArrayList<>();
+ for (;; scanner.nextToken()) {
+ Tokens.Token token = scanner.token();
+ if (token.kind == TokenKind.EOF) {
+ break;
+ } else if (token.kind == TokenKind.DOT || token.kind == TokenKind.COLCOL) {
+ fixMemberSelection(scanner, edits);
+ } else if (token.kind == TokenKind.ERROR) {
+ int errPos = scanner.errPos();
+ if (errPos >= 0 && errPos < content.length()) {
+ fixError(scanner, content, edits);
+ }
+ }
+ }
+ return Edit.applyInsertions(content, edits);
+ }
+
+ private void fixError(@NonNull Scanner scanner, @NonNull CharSequence content, List edits) {
+ int errPos = scanner.errPos();
+ if (content.charAt(errPos) == '.' && errPos > 0 && content.charAt(errPos) == '.') {
+ if (errPos < content.length() - 1
+ && Character.isJavaIdentifierStart(content.charAt(errPos + 1))) {
+ edits.add(Edit.create(errPos, IDENT));
+ }
+ }
+ }
+
+ private void fixMemberSelection(@NonNull Scanner scanner, List edits) {
+ Tokens.Token token = scanner.token();
+ Tokens.Token nextToken = scanner.token(1);
+
+ LineMap lineMap = scanner.getLineMap();
+ int tokenLine = (int) lineMap.getLineNumber(token.pos);
+ int nextLine = (int) lineMap.getLineNumber(nextToken.pos);
+
+ if (nextLine > tokenLine) {
+ edits.add(Edit.create(token.endPos, IDENT + ";"));
+ } else if (!MEMBER_SELECTION_TOKENS.contains(nextToken.kind)) {
+ String toInsert = IDENT;
+ if (INVALID_SELECTION_SUFFIXES.contains(nextToken.kind)) {
+ toInsert = IDENT + ";";
+ }
+ edits.add(Edit.create(token.endPos, toInsert));
+ }
+ }
+
+ public static class Edit {
+ private static final Ordering REVERSE_INSERTION = Ordering.natural().onResultOf(Edit::getPos).reverse();
+
+ @NonNull
+ public static StringBuilder applyInsertions(CharSequence content, List edits) {
+ ImmutableList reverseEdits = REVERSE_INSERTION.immutableSortedCopy(edits);
+
+ StringBuilder sb = new StringBuilder(content);
+
+ for (Edit edit : reverseEdits) {
+ sb.insert(edit.getPos(), edit.getText());
+ }
+ return sb;
+ }
+
+ @NonNull
+ @Contract(value = "_, _ -> new", pure = true)
+ public static Edit create(int pos, String text) {
+ return new Edit(pos, text);
+ }
+
+ private final int pos;
+
+ private final String text;
+
+ public Edit(int pos, String text) {
+ this.pos = pos;
+ this.text = text;
+ }
+
+ public int getPos() {
+ return pos;
+ }
+
+ public String getText() {
+ return text;
+ }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt
similarity index 77%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt
index 1646c2c1a2..0aa8721d31 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt
@@ -22,18 +22,16 @@ import java.util.concurrent.CancellationException
/** @author Akash Yadav */
class CancelChecker {
+ companion object {
+ @JvmStatic
+ fun isCancelled(err: Throwable?): Boolean {
+ if (err == null) {
+ return false
+ }
- companion object {
-
- @JvmStatic
- fun isCancelled(err: Throwable?): Boolean {
- if (err == null) {
- return false
- }
-
- return err is CancellationException ||
- err is CancelAbort ||
- isCancelled(err.cause)
- }
- }
+ return err is CancellationException ||
+ err is CancelAbort ||
+ isCancelled(err.cause)
+ }
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java
new file mode 100644
index 0000000000..3a87e762ee
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java
@@ -0,0 +1,199 @@
+/*
+ * 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.java.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.itsaky.androidide.javac.services.util.JavaDiagnosticUtils;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.rewrite.Rewrite;
+import com.itsaky.androidide.lsp.java.visitors.FindMethodDeclarationAt;
+import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt;
+import com.itsaky.androidide.lsp.models.CodeActionItem;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.tools.Diagnostic;
+import jdkx.tools.JavaFileObject;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+import openjdk.tools.javac.util.JCDiagnostic;
+import org.jetbrains.annotations.Contract;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Akash Yadav
+ */
+public class CodeActionUtils {
+
+ private static final Pattern NOT_THROWN_EXCEPTION = Pattern.compile("^'((\\w+\\.)*\\w+)' is not thrown");
+ private static final Pattern UNREPORTED_EXCEPTION = Pattern.compile("unreported exception ((\\w+\\.)*\\w+)");
+ private static final Logger LOG = LoggerFactory.getLogger(CodeActionUtils.class);
+
+ public static CodeActionItem createQuickFix(
+ final CompilerProvider compiler, String title, Rewrite rewrite) {
+
+ if (rewrite == null) {
+ return null;
+ }
+
+ return ((Rewrite) rewrite).asCodeActions(compiler, title);
+ }
+
+ public static String extractExceptionName(String message) {
+ final Matcher matcher = UNREPORTED_EXCEPTION.matcher(message);
+ if (!matcher.find()) {
+ LOG.warn("`{}` doesn't match `{}`", message, UNREPORTED_EXCEPTION);
+ return "";
+ }
+ return matcher.group(1);
+ }
+
+ public static String extractNotThrownExceptionName(String message) {
+ final Matcher matcher = NOT_THROWN_EXCEPTION.matcher(message);
+ if (!matcher.find()) {
+ LOG.warn("`{}` doesn't match `{}`", message, NOT_THROWN_EXCEPTION);
+ return "";
+ }
+ return matcher.group(1);
+ }
+
+ @NonNull
+ public static CharSequence extractRange(@NonNull CompileTask task, Range range) {
+ CharSequence contents;
+ try {
+ contents = task.root().getSourceFile().getCharContent(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ int start = (int) task.root()
+ .getLineMap()
+ .getPosition(range.getStart().getLine() + 1, range.getStart().getColumn() + 1);
+ int end = (int) task.root()
+ .getLineMap()
+ .getPosition(range.getEnd().getLine() + 1, range.getEnd().getColumn() + 1);
+ return contents.subSequence(start, end);
+ }
+
+ @Nullable
+ public static String findClassNeedingConstructor(CompileTask task, Range range) {
+ final ClassTree type = findClassTree(task, range);
+ if (type == null || hasConstructor(task, type)) {
+ return null;
+ }
+ return qualifiedName(task, type);
+ }
+
+ public static ClassTree findClassTree(@NonNull CompileTask task, @NonNull Range range) {
+ final long position = task.root()
+ .getLineMap()
+ .getPosition(range.getStart().getLine() + 1, range.getStart().getColumn() + 1);
+ return newClassFinder(task).scan(task.root(), position);
+ }
+
+ @NonNull
+ public static MethodPtr findMethod(@NonNull CompileTask task, @NonNull Range range) {
+ final Trees trees = Trees.instance(task.task);
+ final long position = task.root()
+ .getLineMap()
+ .getPosition(range.getStart().getLine() + 1, range.getStart().getColumn() + 1);
+ final MethodTree tree = new FindMethodDeclarationAt(task.task).scan(task.root(), position);
+ final TreePath path = trees.getPath(task.root(), tree);
+ final ExecutableElement method = (ExecutableElement) trees.getElement(path);
+ return new MethodPtr(task.task, method);
+ }
+
+ public static int findPosition(@NonNull CompileTask task, @NonNull Position position) {
+ final LineMap lines = task.root().getLineMap();
+ return (int) lines.getPosition(position.getLine() + 1, position.getColumn() + 1);
+ }
+
+ public static boolean hasConstructor(CompileTask task, @NonNull ClassTree type) {
+ for (Tree member : type.getMembers()) {
+ if (member instanceof MethodTree) {
+ MethodTree method = (MethodTree) member;
+ if (isConstructor(task, method)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean isBlankLine(@NonNull CompilationUnitTree root, long cursor) {
+ LineMap lines = root.getLineMap();
+ long line = lines.getLineNumber(cursor);
+ long start = lines.getStartPosition(line);
+ CharSequence contents;
+ try {
+ contents = root.getSourceFile().getCharContent(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ for (long i = start; i < cursor; i++) {
+ if (!Character.isWhitespace(contents.charAt((int) i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean isConstructor(CompileTask task, @NonNull MethodTree method) {
+ return method.getName().contentEquals("") && !synthetic(task, method);
+ }
+
+ public static boolean isInMethod(@NonNull CompileTask task, long cursor) {
+ MethodTree method = new FindMethodDeclarationAt(task.task).scan(task.root(), cursor);
+ return method != null;
+ }
+
+ @NonNull
+ @Contract("_ -> new")
+ public static FindTypeDeclarationAt newClassFinder(@NonNull CompileTask task) {
+ return new FindTypeDeclarationAt(task.task);
+ }
+
+ @NonNull
+ public static String qualifiedName(@NonNull CompileTask task, ClassTree tree) {
+ final Trees trees = Trees.instance(task.task);
+ final TreePath path = trees.getPath(task.root(), tree);
+ final TypeElement type = (TypeElement) trees.getElement(path);
+ return type.getQualifiedName().toString();
+ }
+
+ public static boolean synthetic(@NonNull CompileTask task, MethodTree method) {
+ return Trees.instance(task.task).getSourcePositions().getStartPosition(task.root(), method) != -1;
+ }
+
+ @Nullable
+ @Contract(pure = true)
+ public static JCDiagnostic unwrapJCDiagnostic(Diagnostic extends JavaFileObject> diagnostic) {
+ return JavaDiagnosticUtils.asJCDiagnostic(diagnostic);
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java
new file mode 100644
index 0000000000..2198e6ba36
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java
@@ -0,0 +1,202 @@
+/*
+ * 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.java.utils;
+
+import androidx.annotation.NonNull;
+import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
+import com.itsaky.androidide.lsp.java.rewrite.AddImport;
+import com.itsaky.androidide.lsp.models.TextEdit;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import com.itsaky.androidide.projects.util.StringSearch;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.Name;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.ArrayType;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.type.ExecutableType;
+import jdkx.lang.model.type.TypeMirror;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.util.JavacTask;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.Trees;
+
+public class EditHelper {
+
+ public static List addImportIfNeeded(CompilerProvider compiler, Path file,
+ Set imports, String className) {
+ if (file == null || containsImport(file, imports, className)) {
+ return Collections.emptyList();
+ }
+
+ AddImport addImport = new AddImport(file, className);
+ return Arrays.asList(Objects.requireNonNull(addImport.rewrite(compiler).get(file)));
+ }
+
+ public static boolean containsImport(@NonNull Path file, Set imports, String className) {
+ if (imports == null) {
+ imports = Collections.emptySet();
+ }
+
+ final String pkgName = Extractors.packageName(className);
+ final String star = pkgName + ".*";
+ if ("java.lang".equals(pkgName) || imports.contains(className) || imports.contains(star)) {
+ return true;
+ }
+
+ final var filePackage = StringSearch.packageName(file);
+ return filePackage != null && filePackage.equals(pkgName);
+ }
+
+ public static int indent(final JavacTask task, final CompilationUnitTree root, final Tree leaf) {
+ SourcePositions pos = Trees.instance(task).getSourcePositions();
+ return indent(root, leaf, pos);
+ }
+
+ public static Position insertAfter(final JavacTask task, final CompilationUnitTree root,
+ final Tree member) {
+ SourcePositions pos = Trees.instance(task).getSourcePositions();
+ LineMap lines = root.getLineMap();
+ long end = pos.getEndPosition(root, member);
+ int line = (int) lines.getLineNumber(end);
+ return new Position(line, 0);
+ }
+
+ public static Position insertAtEndOfClass(JavacTask task, CompilationUnitTree root, ClassTree leaf) {
+ SourcePositions pos = Trees.instance(task).getSourcePositions();
+ LineMap lines = root.getLineMap();
+
+ long end = pos.getEndPosition(root, leaf);
+
+ if (end < 0)
+ throw new IllegalStateException("Cannot determine class end position");
+
+ int line = (int) lines.getLineNumber(end);
+ int column = (int) lines.getColumnNumber(end);
+
+ if (line <= 0 || column <= 0) {
+ throw new IllegalStateException("Invalid class end position: line=" + line + ", column=" + column);
+ }
+
+ return new Position(line - 1, Math.max(0, column - 2));
+ }
+
+ public static Position insertBefore(final JavacTask task, final CompilationUnitTree root,
+ final Tree member) {
+ SourcePositions pos = Trees.instance(task).getSourcePositions();
+ LineMap lines = root.getLineMap();
+ long start = pos.getStartPosition(root, member);
+ int line = (int) lines.getLineNumber(start);
+ return new Position(line - 1, 0);
+ }
+
+ public static String printMethod(final ExecutableElement method,
+ final ExecutableType parameterizedType, MethodTree source) {
+ final StringBuilder buf = new StringBuilder();
+ // TODO leading \n is extra, but needed for indent replaceAll trick
+ buf.append("\n@Override\n");
+ if (method.getModifiers().contains(Modifier.PUBLIC)) {
+ buf.append("public ");
+ }
+ if (method.getModifiers().contains(Modifier.PROTECTED)) {
+ buf.append("protected ");
+ }
+ buf.append(EditHelper.printType(parameterizedType.getReturnType())).append(" ");
+ buf.append(method.getSimpleName()).append("(");
+ buf.append(printParameters(parameterizedType, source));
+ buf.append(") {\n // TODO\n}");
+ return buf.toString();
+ }
+
+ public static String printType(final TypeMirror type) {
+ if (type instanceof DeclaredType) {
+ DeclaredType declared = (DeclaredType) type;
+ String string = printTypeName((TypeElement) declared.asElement());
+ if (!declared.getTypeArguments().isEmpty()) {
+ string = string + "<" + printTypeParameters(declared.getTypeArguments()) + ">";
+ }
+ return string;
+ } else if (type instanceof ArrayType) {
+ ArrayType array = (ArrayType) type;
+ return printType(array.getComponentType()) + "[]";
+ } else {
+ return type.toString();
+ }
+ }
+
+ public static String printTypeName(final TypeElement type) {
+ if (type.getEnclosingElement() instanceof TypeElement) {
+ return printTypeName((TypeElement) type.getEnclosingElement()) + "." + type.getSimpleName();
+ }
+ return type.getSimpleName().toString();
+ }
+
+ public static TextEdit removeTree(final JavacTask task, final CompilationUnitTree root,
+ final Tree remove) {
+ SourcePositions pos = Trees.instance(task).getSourcePositions();
+ LineMap lines = root.getLineMap();
+ long start = pos.getStartPosition(root, remove);
+ long end = pos.getEndPosition(root, remove);
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ Range range = new Range(startPos, endPos);
+ return new TextEdit(range, "");
+ }
+
+ private static int indent(@NonNull CompilationUnitTree root, Tree leaf,
+ @NonNull SourcePositions pos) {
+ LineMap lines = root.getLineMap();
+ long startClass = pos.getStartPosition(root, leaf);
+ long startLine = lines.getStartPosition(lines.getLineNumber(startClass));
+ return (int) (startClass - startLine);
+ }
+
+ private static String printParameters(final ExecutableType method, final MethodTree source) {
+ StringJoiner join = new StringJoiner(", ");
+ for (int i = 0; i < method.getParameterTypes().size(); i++) {
+ String type = EditHelper.printType(method.getParameterTypes().get(i));
+ Name name = source.getParameters().get(i).getName();
+ join.add(type + " " + name);
+ }
+ return join.toString();
+ }
+
+ private static String printTypeParameters(final List extends TypeMirror> arguments) {
+ StringJoiner join = new StringJoiner(", ");
+ for (TypeMirror a : arguments) {
+ join.add(printType(a));
+ }
+ return join.toString();
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java
similarity index 58%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java
index 2d277a0fd7..54b2016b29 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java
@@ -23,25 +23,24 @@
public class Extractors {
- private static final Pattern PACKAGE_EXTRACTOR =
- Pattern.compile("^([a-z][_a-zA-Z0-9]*\\.)*[a-z][_a-zA-Z0-9]*");
- private static final Pattern SIMPLE_EXTRACTOR = Pattern.compile("[A-Z][_a-zA-Z0-9]*$");
+ private static final Pattern PACKAGE_EXTRACTOR = Pattern.compile("^([a-z][_a-zA-Z0-9]*\\.)*[a-z][_a-zA-Z0-9]*");
+ private static final Pattern SIMPLE_EXTRACTOR = Pattern.compile("[A-Z][_a-zA-Z0-9]*$");
- @NonNull
- public static String packageName(String className) {
- Matcher matcher = PACKAGE_EXTRACTOR.matcher(className);
- if (matcher.find()) {
- return matcher.group();
- }
- return "";
- }
+ @NonNull
+ public static String packageName(String className) {
+ Matcher matcher = PACKAGE_EXTRACTOR.matcher(className);
+ if (matcher.find()) {
+ return matcher.group();
+ }
+ return "";
+ }
- @NonNull
- public static String simpleName(String className) {
- Matcher matcher = SIMPLE_EXTRACTOR.matcher(className);
- if (matcher.find()) {
- return matcher.group();
- }
- return "";
- }
+ @NonNull
+ public static String simpleName(String className) {
+ Matcher matcher = SIMPLE_EXTRACTOR.matcher(className);
+ if (matcher.find()) {
+ return matcher.group();
+ }
+ return "";
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java
new file mode 100644
index 0000000000..b5e47428c2
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java
@@ -0,0 +1,242 @@
+/*
+ * 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.java.utils;
+
+import androidx.annotation.Nullable;
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import com.itsaky.androidide.lsp.java.parser.ParseTask;
+import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationNamed;
+import com.itsaky.androidide.models.Location;
+import com.itsaky.androidide.models.Position;
+import com.itsaky.androidide.models.Range;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Paths;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.ElementKind;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.TypeMirror;
+import jdkx.lang.model.util.Types;
+import openjdk.source.tree.ArrayTypeTree;
+import openjdk.source.tree.ClassTree;
+import openjdk.source.tree.CompilationUnitTree;
+import openjdk.source.tree.IdentifierTree;
+import openjdk.source.tree.LineMap;
+import openjdk.source.tree.MemberSelectTree;
+import openjdk.source.tree.MethodTree;
+import openjdk.source.tree.ParameterizedTypeTree;
+import openjdk.source.tree.PrimitiveTypeTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.tree.VariableTree;
+import openjdk.source.util.SourcePositions;
+import openjdk.source.util.TreePath;
+import openjdk.source.util.Trees;
+
+public class FindHelper {
+
+ public static String[] erasedParameterTypes(CompileTask task, ExecutableElement method) {
+ Types types = task.task.getTypes();
+ String[] erasedParameterTypes = new String[method.getParameters().size()];
+ for (int i = 0; i < erasedParameterTypes.length; i++) {
+ TypeMirror p = method.getParameters().get(i).asType();
+ erasedParameterTypes[i] = types.erasure(p).toString();
+ }
+ return erasedParameterTypes;
+ }
+
+ public static VariableTree findField(ParseTask task, String className, String memberName) {
+ ClassTree classTree = findType(task, className);
+ for (Tree member : classTree.getMembers()) {
+ if (member.getKind() != Tree.Kind.VARIABLE) {
+ continue;
+ }
+ VariableTree variable = (VariableTree) member;
+ if (!variable.getName().contentEquals(memberName)) {
+ continue;
+ }
+ return variable;
+ }
+ throw new RuntimeException("no variable");
+ }
+
+ public static ExecutableElement findMethod(
+ CompileTask task, String className, String methodName, String[] erasedParameterTypes) {
+ TypeElement type = task.task.getElements().getTypeElement(className);
+ if (type == null) {
+ return null;
+ }
+ for (Element member : type.getEnclosedElements()) {
+ if (member.getKind() != ElementKind.METHOD) {
+ continue;
+ }
+ ExecutableElement method = (ExecutableElement) member;
+ if (isSameMethod(task, method, className, methodName, erasedParameterTypes)) {
+ return method;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Find the method with name methodName in class clasName with the given parameter types.
+ *
+ * @param task
+ * The parse task.
+ * @param className
+ * The fully qualified class name.
+ * @param methodName
+ * The name of method in class className.
+ * @param erasedParameterTypes
+ * The parameter types of the method (fully qualified class names).
+ * @return The {@link MethodTree} for the method, or null if method was not found.
+ */
+ @Nullable
+ public static MethodTree findMethod(
+ ParseTask task, String className, String methodName, String[] erasedParameterTypes) {
+ ClassTree classTree = findType(task, className);
+ for (Tree member : classTree.getMembers()) {
+ if (member.getKind() != Tree.Kind.METHOD) {
+ continue;
+ }
+ MethodTree method = (MethodTree) member;
+ if (!method.getName().contentEquals(methodName)) {
+ continue;
+ }
+ if (!isSameMethodType(method, erasedParameterTypes)) {
+ continue;
+ }
+ return method;
+ }
+
+ return null;
+ }
+
+ public static int findNameIn(CompilationUnitTree root, CharSequence name, int start, int end) {
+ CharSequence contents;
+ try {
+ contents = root.getSourceFile().getCharContent(true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ Matcher matcher = Pattern.compile("\\b" + Pattern.quote(name.toString()) + "\\b").matcher(contents);
+ matcher.region(start, end);
+ if (matcher.find()) {
+ return matcher.start();
+ }
+ return -1;
+ }
+
+ public static ClassTree findType(ParseTask task, String className) {
+ return new FindTypeDeclarationNamed().scan(task.root, className);
+ }
+
+ public static Location location(CompileTask task, TreePath path) {
+ return location(task, path, "");
+ }
+
+ public static Location location(CompileTask task, TreePath path, CharSequence name) {
+ final CompilationUnitTree compilationUnit = path.getCompilationUnit();
+ final Tree leaf = path.getLeaf();
+ LineMap lines = compilationUnit.getLineMap();
+ SourcePositions pos = Trees.instance(task.task).getSourcePositions();
+ int start = (int) pos.getStartPosition(compilationUnit, leaf);
+ int end = (int) pos.getEndPosition(compilationUnit, leaf);
+ if (name.length() > 0) {
+ start = FindHelper.findNameIn(compilationUnit, name, start, end);
+ end = start + name.length();
+ }
+
+ int startLine = (int) lines.getLineNumber(start);
+ int startColumn = (int) lines.getColumnNumber(start);
+ Position startPos = new Position(startLine - 1, startColumn - 1);
+ int endLine = (int) lines.getLineNumber(end);
+ int endColumn = (int) lines.getColumnNumber(end);
+ Position endPos = new Position(endLine - 1, endColumn - 1);
+ Range range = new Range(startPos, endPos);
+ URI uri = compilationUnit.getSourceFile().toUri();
+ return new Location(Paths.get(uri), range);
+ }
+
+ private static boolean isSameMethod(
+ CompileTask task,
+ ExecutableElement method,
+ String className,
+ String methodName,
+ String[] erasedParameterTypes) {
+ Types types = task.task.getTypes();
+ TypeElement parent = (TypeElement) method.getEnclosingElement();
+ if (!parent.getQualifiedName().contentEquals(className)) {
+ return false;
+ }
+ if (!method.getSimpleName().contentEquals(methodName)) {
+ return false;
+ }
+ if (method.getParameters().size() != erasedParameterTypes.length) {
+ return false;
+ }
+ for (int i = 0; i < erasedParameterTypes.length; i++) {
+ TypeMirror erasure = types.erasure(method.getParameters().get(i).asType());
+ boolean same = erasure.toString().equals(erasedParameterTypes[i]);
+ if (!same) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isSameMethodType(MethodTree candidate, String[] erasedParameterTypes) {
+ if (candidate.getParameters().size() != erasedParameterTypes.length) {
+ return false;
+ }
+ for (int i = 0; i < candidate.getParameters().size(); i++) {
+ if (!typeMatches(candidate.getParameters().get(i).getType(), erasedParameterTypes[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean typeMatches(Tree candidate, String erasedType) {
+ if (candidate instanceof ParameterizedTypeTree) {
+ ParameterizedTypeTree parameterized = (ParameterizedTypeTree) candidate;
+ return typeMatches(parameterized.getType(), erasedType);
+ }
+ if (candidate instanceof PrimitiveTypeTree) {
+ return candidate.toString().equals(erasedType);
+ }
+ if (candidate instanceof IdentifierTree) {
+ String simpleName = candidate.toString();
+ return erasedType.endsWith(simpleName);
+ }
+ if (candidate instanceof MemberSelectTree) {
+ return candidate.toString().equals(erasedType);
+ }
+ if (candidate instanceof ArrayTypeTree) {
+ ArrayTypeTree array = (ArrayTypeTree) candidate;
+ if (!erasedType.endsWith("[]")) {
+ return false;
+ }
+ String erasedElement = erasedType.substring(0, erasedType.length() - "[]".length());
+ return typeMatches(array.getType(), erasedElement);
+ }
+ return true;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt
new file mode 100644
index 0000000000..d2f17b8999
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt
@@ -0,0 +1,856 @@
+/*
+ * 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 .
+ */
+@file:Suppress("unused", "MemberVisibilityCanBePrivate")
+
+package com.itsaky.androidide.lsp.java.utils
+
+import androidx.annotation.NonNull
+import androidx.annotation.Nullable
+import com.github.javaparser.StaticJavaParser
+import com.github.javaparser.ast.CompilationUnit
+import com.github.javaparser.ast.ImportDeclaration
+import com.github.javaparser.ast.Node
+import com.github.javaparser.ast.NodeList
+import com.github.javaparser.ast.PackageDeclaration
+import com.github.javaparser.ast.body.BodyDeclaration
+import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
+import com.github.javaparser.ast.body.FieldDeclaration
+import com.github.javaparser.ast.body.MethodDeclaration
+import com.github.javaparser.ast.body.Parameter
+import com.github.javaparser.ast.body.ReceiverParameter
+import com.github.javaparser.ast.body.VariableDeclarator
+import com.github.javaparser.ast.expr.AnnotationExpr
+import com.github.javaparser.ast.expr.AssignExpr
+import com.github.javaparser.ast.expr.BooleanLiteralExpr
+import com.github.javaparser.ast.expr.CharLiteralExpr
+import com.github.javaparser.ast.expr.DoubleLiteralExpr
+import com.github.javaparser.ast.expr.Expression
+import com.github.javaparser.ast.expr.FieldAccessExpr
+import com.github.javaparser.ast.expr.IntegerLiteralExpr
+import com.github.javaparser.ast.expr.LiteralExpr
+import com.github.javaparser.ast.expr.LongLiteralExpr
+import com.github.javaparser.ast.expr.MarkerAnnotationExpr
+import com.github.javaparser.ast.expr.MemberValuePair
+import com.github.javaparser.ast.expr.MethodCallExpr
+import com.github.javaparser.ast.expr.Name
+import com.github.javaparser.ast.expr.NameExpr
+import com.github.javaparser.ast.expr.NormalAnnotationExpr
+import com.github.javaparser.ast.expr.NullLiteralExpr
+import com.github.javaparser.ast.expr.SimpleName
+import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr
+import com.github.javaparser.ast.expr.StringLiteralExpr
+import com.github.javaparser.ast.expr.SuperExpr
+import com.github.javaparser.ast.expr.VariableDeclarationExpr
+import com.github.javaparser.ast.nodeTypes.NodeWithSimpleName
+import com.github.javaparser.ast.stmt.BlockStmt
+import com.github.javaparser.ast.stmt.ExpressionStmt
+import com.github.javaparser.ast.stmt.ReturnStmt
+import com.github.javaparser.ast.stmt.Statement
+import com.github.javaparser.ast.type.ArrayType
+import com.github.javaparser.ast.type.PrimitiveType
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.BOOLEAN
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.BYTE
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.CHAR
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.DOUBLE
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.FLOAT
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.INT
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.LONG
+import com.github.javaparser.ast.type.PrimitiveType.Primitive.SHORT
+import com.github.javaparser.ast.type.ReferenceType
+import com.github.javaparser.ast.type.Type
+import com.github.javaparser.ast.type.TypeParameter
+import com.github.javaparser.printer.DefaultPrettyPrinter
+import com.github.javaparser.printer.configuration.DefaultPrinterConfiguration
+import com.github.javaparser.printer.configuration.PrinterConfiguration
+import com.itsaky.androidide.lsp.java.utils.TypeUtils.toType
+import com.itsaky.androidide.lsp.java.visitors.PrettyPrintingVisitor
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier
+import jdkx.lang.model.element.TypeParameterElement
+import jdkx.lang.model.element.VariableElement
+import jdkx.lang.model.type.ExecutableType
+import jdkx.lang.model.type.TypeKind
+import jdkx.lang.model.type.TypeMirror
+import jdkx.lang.model.type.TypeVariable
+import openjdk.source.tree.AnnotationTree
+import openjdk.source.tree.AssignmentTree
+import openjdk.source.tree.BlockTree
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.ErroneousTree
+import openjdk.source.tree.ExpressionStatementTree
+import openjdk.source.tree.ExpressionTree
+import openjdk.source.tree.IdentifierTree
+import openjdk.source.tree.ImportTree
+import openjdk.source.tree.LiteralTree
+import openjdk.source.tree.MemberSelectTree
+import openjdk.source.tree.MethodInvocationTree
+import openjdk.source.tree.MethodTree
+import openjdk.source.tree.PackageTree
+import openjdk.source.tree.StatementTree
+import openjdk.source.tree.Tree
+import openjdk.source.tree.TypeParameterTree
+import openjdk.source.tree.VariableTree
+import org.jetbrains.annotations.Contract
+import java.util.function.Predicate
+import java.util.stream.IntStream
+import java.util.stream.Stream
+import kotlin.jvm.optionals.getOrNull
+
+object JavaParserUtils {
+ /**
+ * Collects all the type names from the given node.
+ *
+ * @param type The method to get types from.
+ * @return A set of fully qualified names of the types in the given node.
+ */
+ fun collectImports(type: ExecutableType): MutableSet {
+ val types = mutableSetOf()
+ val returnType = type.returnType
+ if (returnType != null) {
+ if (
+ returnType.kind != TypeKind.VOID &&
+ returnType.kind != TypeKind.TYPEVAR &&
+ !returnType.kind.isPrimitive
+ ) {
+ val fqn = getTypeToImport(returnType)
+ if (fqn != null) {
+ types.add(fqn)
+ }
+ }
+ }
+ if (type.thrownTypes != null) {
+ for (thrown in type.thrownTypes) {
+ val fqn = getTypeToImport(thrown)
+ if (fqn != null) {
+ types.add(fqn)
+ }
+ }
+ }
+ for (t in type.parameterTypes) {
+ if (t.kind.isPrimitive) {
+ continue
+ }
+ val fqn = getTypeToImport(t)
+ if (fqn != null) {
+ types.add(fqn)
+ }
+ }
+ return types
+ }
+
+ @Nullable
+ private fun getTypeToImport(type: TypeMirror): String? {
+ if (type.kind.isPrimitive) {
+ return null
+ }
+ if (type.kind == TypeKind.TYPEVAR) {
+ return null
+ }
+ var fqn = toType(type).toString()
+ if (type.kind == TypeKind.ARRAY) {
+ fqn = removeArray(fqn)
+ }
+
+ return removeDiamond(fqn)
+ }
+
+ fun printMethod(
+ method: ExecutableElement,
+ parameterizedType: ExecutableType?,
+ source: MethodTree,
+ ): MethodDeclaration {
+ val methodDeclaration: MethodDeclaration = toMethodDeclaration(source, parameterizedType)
+ printMethodInternal(methodDeclaration, method)
+ return methodDeclaration
+ }
+
+ fun printMethod(
+ method: ExecutableElement?,
+ parameterizedType: ExecutableType?,
+ source: ExecutableElement,
+ ): MethodDeclaration {
+ val methodDeclaration: MethodDeclaration = toMethodDeclaration(method, parameterizedType)
+ printMethodInternal(methodDeclaration, source)
+ return methodDeclaration
+ }
+
+ private fun printMethodInternal(
+ methodDeclaration: MethodDeclaration,
+ method: ExecutableElement,
+ ) {
+ methodDeclaration.addMarkerAnnotation(Override::class.java)
+ val recentlyNonNull = methodDeclaration.getAnnotationByName("RecentlyNonNull")
+ if (recentlyNonNull.isPresent) {
+ methodDeclaration.remove(recentlyNonNull.get())
+ methodDeclaration.addMarkerAnnotation(NonNull::class.java)
+ }
+
+ val blockStmt = BlockStmt()
+ if (method.modifiers.contains(Modifier.ABSTRACT)) {
+ methodDeclaration.removeModifier(com.github.javaparser.ast.Modifier.Keyword.ABSTRACT)
+ if (methodDeclaration.type.isClassOrInterfaceType) {
+ blockStmt.addStatement(ReturnStmt(NullLiteralExpr()))
+ }
+ if (methodDeclaration.type.isPrimitiveType) {
+ val type = methodDeclaration.type.asPrimitiveType()
+ blockStmt.addStatement(ReturnStmt(getReturnExpr(type)))
+ }
+ } else {
+ val methodCallExpr = MethodCallExpr()
+ methodCallExpr.name = methodDeclaration.name
+ methodCallExpr.arguments =
+ methodDeclaration.parameters
+ .stream()
+ .map { obj: Parameter -> obj.nameAsExpression }
+ .collect(NodeList.toNodeList())
+ methodCallExpr.setScope(SuperExpr())
+ if (methodDeclaration.type.isVoidType) {
+ blockStmt.addStatement(methodCallExpr)
+ } else {
+ blockStmt.addStatement(ReturnStmt(methodCallExpr))
+ }
+ }
+ methodDeclaration.setBody(blockStmt)
+ }
+
+ private fun getReturnExpr(type: PrimitiveType): Expression =
+ when (type.type) {
+ BOOLEAN -> BooleanLiteralExpr()
+
+ BYTE,
+ DOUBLE,
+ CHAR,
+ SHORT,
+ LONG,
+ FLOAT,
+ INT,
+ -> IntegerLiteralExpr("0")
+
+ else -> NullLiteralExpr()
+ }
+
+ @Suppress("Since15")
+ fun toCompilationUnit(tree: CompilationUnitTree): CompilationUnit {
+ val compilationUnit = CompilationUnit()
+ compilationUnit.setPackageDeclaration(toPackageDeclaration(tree.getPackage()))
+ tree.imports.forEach { importTree: ImportTree? ->
+ compilationUnit.addImport(toImportDeclaration(importTree!!))
+ }
+ compilationUnit.types =
+ tree.typeDecls
+ .stream()
+ .map { toClassOrInterfaceDeclaration(it) }
+ .collect(NodeList.toNodeList())
+ return compilationUnit
+ }
+
+ @Suppress("Since15")
+ fun toPackageDeclaration(tree: PackageTree): PackageDeclaration {
+ val declaration = PackageDeclaration()
+ declaration.setName(tree.packageName.toString())
+ return declaration
+ }
+
+ fun toImportDeclaration(tree: ImportTree): ImportDeclaration {
+ val name = tree.qualifiedIdentifier.toString()
+ val isAsterisk = name.endsWith("*")
+ return ImportDeclaration(name, tree.isStatic, isAsterisk)
+ }
+
+ fun toClassOrInterfaceDeclaration(tree: Tree?): ClassOrInterfaceDeclaration? =
+ if (tree is ClassTree) {
+ toClassOrInterfaceDeclaration(tree as ClassTree?)
+ } else {
+ null
+ }
+
+ fun toBlockStatement(tree: BlockTree): BlockStmt {
+ val blockStmt = BlockStmt()
+ blockStmt.statements =
+ tree.statements
+ .stream()
+ .map { toStatement(it) }
+ .collect(NodeList.toNodeList())
+ return blockStmt
+ }
+
+ fun toStatement(tree: StatementTree?): Statement? {
+ if (tree is ExpressionStatementTree) {
+ return toExpressionStatement((tree as ExpressionStatementTree?)!!)
+ }
+ return if (tree is VariableTree) {
+ toVariableDeclarationExpression((tree as VariableTree?)!!)
+ } else {
+ StaticJavaParser.parseStatement(tree.toString())
+ }
+ }
+
+ fun toExpressionStatement(tree: ExpressionStatementTree): ExpressionStmt {
+ val expressionStmt = ExpressionStmt()
+ expressionStmt.expression = toExpression(tree.expression)
+ return expressionStmt
+ }
+
+ fun toExpression(tree: ExpressionTree?): Expression? {
+ if (tree is MethodInvocationTree) {
+ return toMethodCallExpression((tree as MethodInvocationTree?)!!)
+ }
+ if (tree is MemberSelectTree) {
+ return toFieldAccessExpression((tree as MemberSelectTree?)!!)
+ }
+ if (tree is IdentifierTree) {
+ return toNameExpr((tree as IdentifierTree?)!!)
+ }
+ if (tree is LiteralTree) {
+ return toLiteralExpression((tree as LiteralTree?)!!)
+ }
+ if (tree is AssignmentTree) {
+ return toAssignExpression((tree as AssignmentTree?)!!)
+ }
+ if (tree is ErroneousTree) {
+ val erroneousTree = tree as ErroneousTree?
+ if (erroneousTree!!.errorTrees.isNotEmpty()) {
+ val errorTree = erroneousTree.errorTrees[0]
+ return toExpression(errorTree as ExpressionTree)
+ }
+ }
+ return null
+ }
+
+ private fun toAssignExpression(tree: AssignmentTree): AssignExpr {
+ val assignExpr = AssignExpr()
+ assignExpr.target = toExpression(tree.variable)
+ assignExpr.value = toExpression(tree.expression)
+ return assignExpr
+ }
+
+ fun toVariableDeclarationExpression(tree: VariableTree): ExpressionStmt {
+ val expr = VariableDeclarationExpr()
+ expr.modifiers =
+ tree.modifiers.flags
+ .stream()
+ .map { toModifier(it) }
+ .collect(NodeList.toNodeList())
+ val declarator = VariableDeclarator()
+ declarator.setName(tree.name.toString())
+ declarator.setInitializer(toExpression(tree.initializer))
+ declarator.type = toType(tree.type)
+ expr.addVariable(declarator)
+ val stmt = ExpressionStmt()
+ stmt.expression = expr
+ return stmt
+ }
+
+ fun toNameExpr(tree: IdentifierTree): NameExpr {
+ val nameExpr = NameExpr()
+ nameExpr.setName(tree.name.toString())
+ return nameExpr
+ }
+
+ fun toLiteralExpression(tree: LiteralTree): LiteralExpr? {
+ val value = tree.value
+ if (value is String) {
+ return StringLiteralExpr(value)
+ }
+ if (value is Boolean) {
+ return BooleanLiteralExpr(value)
+ }
+ if (value is Int) {
+ return IntegerLiteralExpr(value.toString())
+ }
+ if (value is Char) {
+ return CharLiteralExpr(value)
+ }
+ if (value is Long) {
+ return LongLiteralExpr(value.toString())
+ }
+ return if (value is Double) {
+ DoubleLiteralExpr(value)
+ } else {
+ null
+ }
+ }
+
+ fun toMethodCallExpression(tree: MethodInvocationTree): MethodCallExpr {
+ val expr = MethodCallExpr()
+ if (tree.methodSelect is MemberSelectTree) {
+ val methodSelect = tree.methodSelect as MemberSelectTree
+ expr.setScope(toExpression(methodSelect.expression))
+ expr.setName(methodSelect.identifier.toString())
+ }
+ expr.arguments =
+ tree.arguments
+ .stream()
+ .map { toExpression(it) }
+ .collect(NodeList.toNodeList())
+ expr.setTypeArguments(
+ tree.typeArguments
+ .stream()
+ .map { toType(it) }
+ .collect(NodeList.toNodeList()),
+ )
+ if (tree.methodSelect is IdentifierTree) {
+ expr.name = toNameExpr((tree.methodSelect as IdentifierTree)).name
+ }
+ return expr
+ }
+
+ fun toFieldAccessExpression(tree: MemberSelectTree): FieldAccessExpr {
+ val fieldAccessExpr = FieldAccessExpr()
+ fieldAccessExpr.setName(tree.identifier.toString())
+ fieldAccessExpr.scope = toExpression(tree.expression)
+ return fieldAccessExpr
+ }
+
+ fun toClassOrInterfaceDeclaration(tree: ClassTree): ClassOrInterfaceDeclaration {
+ val declaration = ClassOrInterfaceDeclaration()
+ declaration.setName(tree.simpleName.toString())
+ declaration.extendedTypes =
+ NodeList.nodeList(TypeUtils.toClassOrInterfaceType(tree.extendsClause))
+ declaration.typeParameters =
+ tree.typeParameters
+ .stream()
+ .map { toTypeParameter(it) }
+ .collect(NodeList.toNodeList())
+ declaration.typeParameters =
+ tree.typeParameters
+ .stream()
+ .map { toTypeParameter(it) }
+ .collect(NodeList.toNodeList())
+ declaration.implementedTypes =
+ tree.implementsClause
+ .stream()
+ .map { TypeUtils.toClassOrInterfaceType(it) }
+ .collect(NodeList.toNodeList())
+ declaration.modifiers =
+ tree.modifiers.flags
+ .stream()
+ .map { toModifier(it) }
+ .collect(NodeList.toNodeList())
+ declaration.members =
+ tree.members
+ .stream()
+ .map { toBodyDeclaration(it) }
+ .collect(NodeList.toNodeList())
+ return declaration
+ }
+
+ fun toBodyDeclaration(tree: Tree?): BodyDeclaration<*>? {
+ if (tree is MethodTree) {
+ return toMethodDeclaration((tree as MethodTree?)!!, null)
+ }
+ return if (tree is VariableTree) {
+ toFieldDeclaration((tree as VariableTree?)!!)
+ } else {
+ null
+ }
+ }
+
+ fun toFieldDeclaration(tree: VariableTree): FieldDeclaration {
+ val declaration = FieldDeclaration()
+ declaration.modifiers =
+ tree.modifiers.flags
+ .stream()
+ .map { toModifier(it) }
+ .collect(NodeList.toNodeList())
+ val declarator = VariableDeclarator()
+ declarator.setName(tree.name.toString())
+ val initializer = toExpression(tree.initializer)
+ if (initializer != null) {
+ declarator.setInitializer(initializer)
+ }
+ val type = toType(tree.type)
+ if (type != null) {
+ declarator.type = type
+ }
+ declaration.addVariable(declarator)
+ return declaration
+ }
+
+ fun toMethodDeclaration(
+ method: MethodTree,
+ type: ExecutableType?,
+ ): MethodDeclaration {
+ val methodDeclaration = MethodDeclaration()
+ methodDeclaration.annotations =
+ method.modifiers.annotations
+ .stream()
+ .map { toAnnotation(it) }
+ .collect(NodeList.toNodeList())
+ methodDeclaration.setName(method.name.toString())
+ val returnType =
+ if (type != null) {
+ toType(type.returnType)
+ } else {
+ toType(method.returnType)
+ }
+ if (returnType != null) {
+ methodDeclaration.type = getTypeWithoutBounds(returnType)
+ }
+ methodDeclaration.modifiers =
+ method.modifiers.flags
+ .stream()
+ .map { toModifier(it) }
+ .collect(NodeList.toNodeList())
+ methodDeclaration.parameters =
+ method.parameters
+ .map { variable ->
+ return@map toParameter(variable).also { param ->
+ val firstType = getTypeWithoutBounds(param.type)
+ param.type = firstType
+ }
+ }.toNodeList()
+ methodDeclaration.typeParameters =
+ method.typeParameters
+ .mapNotNull {
+ return@mapNotNull toType(it as Tree?)?.toTypeParameter()?.getOrNull()
+ }.toNodeList()
+ if (method.body != null) {
+ methodDeclaration.setBody(toBlockStatement(method.body))
+ }
+ if (method.receiverParameter != null) {
+ methodDeclaration.setReceiverParameter(toReceiverParameter(method.receiverParameter))
+ }
+ return methodDeclaration
+ }
+
+ fun toAnnotation(tree: AnnotationTree): AnnotationExpr {
+ if (tree.arguments.isEmpty()) {
+ val expr = MarkerAnnotationExpr()
+ expr.setName(toType(tree.annotationType).toString())
+ return expr
+ }
+ if (tree.arguments.size == 1) {
+ val expr = SingleMemberAnnotationExpr()
+ expr.setName(toType(tree.annotationType).toString())
+ expr.memberValue = toExpression(tree.arguments[0])
+ return expr
+ }
+ val expr = NormalAnnotationExpr()
+ expr.setName(toType(tree.annotationType).toString())
+ expr.pairs =
+ tree.arguments
+ .map {
+ return@map if (it is AssignmentTree) {
+ val assignExpr = toAssignExpression((it as AssignmentTree?)!!)
+ val pair = MemberValuePair()
+ pair.setName(assignExpr.target.toString())
+ pair.value = assignExpr.value
+ pair
+ } else {
+ null
+ }
+ }.toNodeList()
+ return expr
+ }
+
+ fun toParameter(tree: VariableTree): Parameter {
+ val parameter = Parameter()
+ parameter.type = toType(tree.type)
+ tree.modifiers.flags
+ .map { toModifier(it) }
+ .toNodeList()
+ .also { parameter.modifiers = it }
+ parameter.setName(tree.name.toString())
+ return parameter
+ }
+
+ /**
+ * Convert a parameter into [Parameter] object. This method is called from source files, giving
+ * their accurate names.
+ */
+ fun toParameter(
+ type: TypeMirror?,
+ name: VariableTree,
+ ): Parameter {
+ val parameter = Parameter()
+ parameter.setType(EditHelper.printType(type))
+ parameter.setName(name.name.toString())
+ parameter.modifiers =
+ name.modifiers.flags
+ .map { toModifier(it) }
+ .toNodeList()
+ parameter.setName(name.name.toString())
+ return parameter
+ }
+
+ fun toTypeParameter(type: TypeParameterTree): TypeParameter? = StaticJavaParser.parseTypeParameter(type.toString())
+
+ fun toReceiverParameter(parameter: VariableTree): ReceiverParameter {
+ val receiverParameter = ReceiverParameter()
+ receiverParameter.setName(parameter.name.toString())
+ receiverParameter.type = toType(parameter.type)
+ return receiverParameter
+ }
+
+ fun toMethodDeclaration(
+ method: ExecutableElement?,
+ type: ExecutableType?,
+ ): MethodDeclaration {
+ val methodDeclaration = MethodDeclaration()
+ val returnType =
+ if (type != null) {
+ toType(type.returnType)
+ } else {
+ toType(method!!.returnType)
+ }
+ if (returnType != null) {
+ methodDeclaration.type = getTypeWithoutBounds(returnType)
+ }
+ methodDeclaration.isDefault = method!!.isDefault
+ methodDeclaration.setName(method.simpleName.toString())
+ methodDeclaration.setModifiers(
+ *method.modifiers
+ .stream()
+ .map {
+ com.github.javaparser.ast.Modifier.Keyword
+ .valueOf(it!!.name)
+ }.asArray(),
+ )
+ methodDeclaration.parameters =
+ IntStream
+ .range(0, method.parameters.size)
+ .mapToObj {
+ return@mapToObj toParameter(
+ type!!.parameterTypes[it],
+ method.parameters[it],
+ ).also { parameter ->
+ val firstType = getTypeWithoutBounds(parameter.type)
+ parameter.type = firstType
+ }
+ }.collect(NodeList.toNodeList())
+ methodDeclaration.typeParameters =
+ type!!
+ .typeVariables
+ .mapNotNull { toType(it as TypeMirror?)?.toTypeParameter()?.getOrNull() }
+ .toNodeList()
+ return methodDeclaration
+ }
+
+ fun getFirstArrayType(type: Type): Type {
+ if (type.isTypeParameter) {
+ val typeParameter = type.asTypeParameter()
+ if (typeParameter!!.typeBound.isNonEmpty) {
+ val first = typeParameter.typeBound.first
+ if (first!!.isPresent) {
+ return ArrayType(first.get())
+ }
+ }
+ }
+ return type
+ }
+
+ fun getFirstType(type: Type): Type {
+ if (type.isTypeParameter) {
+ val typeParameter = type.asTypeParameter()
+ if (typeParameter!!.typeBound.isNonEmpty) {
+ val first = typeParameter.typeBound.first
+ if (first!!.isPresent) {
+ return first.get()
+ }
+ }
+ }
+ if (!type.isClassOrInterfaceType) {
+ return type
+ }
+
+ val typeArguments = type.asClassOrInterfaceType().typeArguments
+ if (!typeArguments!!.isPresent || !typeArguments.get().isNonEmpty) {
+ return type
+ }
+
+ val first = typeArguments.get().first
+ if (!first!!.isPresent || !first.get().isTypeParameter) {
+ return type
+ }
+
+ val typeBound = first.get().asTypeParameter().typeBound
+ if (!typeBound!!.isNonEmpty) {
+ return type
+ }
+
+ val first1 = typeBound.first
+ if (!first1!!.isPresent) {
+ return type
+ }
+
+ type.asClassOrInterfaceType().setTypeArguments(first1.get())
+ return type
+ }
+
+ fun getTypeWithoutBounds(type: Type): Type {
+ if (type.isArrayType && !type.asArrayType().componentType.isTypeParameter) {
+ return type
+ }
+ if (!type.isArrayType && !type.isTypeParameter) {
+ return type
+ }
+ if (type is NodeWithSimpleName<*>) {
+ return StaticJavaParser.parseClassOrInterfaceType(
+ (type as NodeWithSimpleName<*>).nameAsString,
+ )
+ }
+ return if (type.isArrayType) {
+ ArrayType(getTypeWithoutBounds(type.asArrayType().componentType))
+ } else {
+ type
+ }
+ }
+
+ @Contract("_ -> new")
+ fun toModifier(modifier: Modifier): com.github.javaparser.ast.Modifier =
+ com.github.javaparser.ast.Modifier(
+ com.github.javaparser.ast.Modifier.Keyword
+ .valueOf(modifier.name),
+ )
+
+ /**
+ * Convert a parameter into [Parameter] object. This method is called from compiled class files,
+ * giving inaccurate parameter names
+ */
+ fun toParameter(
+ type: TypeMirror?,
+ name: VariableElement?,
+ ): Parameter {
+ val parameter = Parameter()
+ parameter.setType(EditHelper.printType(type))
+ if (parameter.type.isArrayType) {
+ if ((type as openjdk.tools.javac.code.Type.ArrayType?)!!.isVarargs) {
+ parameter.type = parameter.type.asArrayType().componentType
+ parameter.isVarArgs = true
+ }
+ }
+ parameter.setName(name!!.simpleName.toString())
+ parameter.modifiers = name.modifiers.map { toModifier(it) }.toNodeList()
+ parameter.setName(name.simpleName.toString())
+ return parameter
+ }
+
+ fun toTypeParameter(type: TypeParameterElement): TypeParameter? = StaticJavaParser.parseTypeParameter(type.toString())
+
+ fun toTypeParameter(typeVariable: TypeVariable): TypeParameter? = StaticJavaParser.parseTypeParameter(typeVariable.toString())
+
+ fun getClassNames(type: Type): MutableList {
+ val classNames: MutableList = ArrayList()
+ if (type.isClassOrInterfaceType) {
+ classNames.add(type.asClassOrInterfaceType().name.asString())
+ }
+ if (type.isWildcardType) {
+ val wildcardType = type.asWildcardType()
+ wildcardType!!.extendedType.ifPresent { t: ReferenceType? ->
+ classNames.addAll(getClassNames(t!!))
+ }
+ wildcardType.superType.ifPresent { t: ReferenceType? ->
+ classNames.addAll(getClassNames(t!!))
+ }
+ }
+ if (type.isArrayType) {
+ classNames.addAll(getClassNames(type.asArrayType().componentType))
+ }
+ if (type.isIntersectionType) {
+ type
+ .asIntersectionType()
+ .elements
+ .stream()
+ .map { getClassNames(it) }
+ .forEach { c: MutableList? -> classNames.addAll(c!!) }
+ }
+ return classNames
+ }
+
+ /**
+ * Print a node declaration into its string representation
+ *
+ * @param node node to print
+ * @param delegate callback to whether a class name should be printed as fully qualified names
+ * @return String representation of the method declaration properly formatted
+ */
+ fun prettyPrint(
+ node: Node?,
+ delegate: Predicate?,
+ ): String? {
+ val configuration: PrinterConfiguration = DefaultPrinterConfiguration()
+ val visitor: PrettyPrintingVisitor =
+ object : PrettyPrintingVisitor(configuration) {
+ override fun visit(
+ n: SimpleName?,
+ arg: Void?,
+ ) {
+ printOrphanCommentsBeforeThisChildNode(n)
+ printComment(n!!.comment, arg)
+ val identifier = n.identifier
+ if (delegate!!.test(identifier)) {
+ printer.print(identifier)
+ } else {
+ printer.print(getSimpleName(identifier))
+ }
+ }
+
+ override fun visit(
+ n: Name?,
+ arg: Void?,
+ ) {
+ super.visit(n, arg)
+ }
+ }
+ val prettyPrinter = DefaultPrettyPrinter({ visitor }, configuration)
+ return prettyPrinter.print(node)
+ }
+
+ @JvmStatic
+ fun getSimpleName(className: String?): String {
+ var name = className
+ name = removeDiamond(name!!)
+ val dot = name.lastIndexOf('.')
+ if (dot == -1) {
+ return name
+ }
+ return if (name.startsWith("? extends")) {
+ "? extends " + name.substring(dot + 1)
+ } else {
+ name.substring(dot + 1)
+ }
+ }
+
+ fun removeDiamond(className: String): String {
+ var name = className
+ if (name.contains("<")) {
+ name = name.substring(0, name.indexOf('<'))
+ }
+ return name
+ }
+
+ fun removeArray(className: String): String {
+ var name = className
+ if (name.contains("[")) {
+ name = name.substring(0, name.indexOf('['))
+ }
+ return name
+ }
+}
+
+private fun Collection.toNodeList(): NodeList = NodeList(this)
+
+private inline fun Stream.asArray(): Array {
+ val arr = mutableListOf()
+ for (element in this) {
+ arr.add(element)
+ }
+
+ return arr.toTypedArray()
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt
new file mode 100644
index 0000000000..29b591d457
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.java.utils
+
+import com.itsaky.androidide.preferences.utils.indentationString
+import com.squareup.javapoet.AnnotationSpec
+import com.squareup.javapoet.ImportCollectingCodeWriter
+import com.squareup.javapoet.MethodSpec
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier
+import jdkx.lang.model.type.DeclaredType
+import jdkx.lang.model.type.NoType
+import jdkx.lang.model.type.NullType
+import jdkx.lang.model.type.TypeKind
+import jdkx.lang.model.util.Types
+
+/** @author Akash Yadav */
+class JavaPoetUtils {
+ companion object {
+ @JvmStatic
+ fun print(
+ method: MethodSpec,
+ importsOut: MutableSet,
+ qualifiedNames: Boolean = true,
+ ): String {
+ val sb = StringBuilder()
+ val writer = ImportCollectingCodeWriter(sb, indentationString, emptySet(), emptySet())
+
+ writer.isPrintQualifiedNames = qualifiedNames
+ writer.emit(method)
+ importsOut.addAll(writer.importClasses)
+
+ return sb.toString()
+ }
+
+ @JvmStatic
+ fun print(
+ build: MethodSpec,
+ imports: MutableSet,
+ ): String = print(build, imports, true)
+
+ @JvmStatic
+ fun buildMethod(
+ method: ExecutableElement,
+ types: Types,
+ type: DeclaredType,
+ ): MethodSpec.Builder {
+ val builder = MethodSpec.overriding(method, type, types)
+ val mirrors = method.annotationMirrors
+ if (mirrors != null && mirrors.isNotEmpty()) {
+ for (mirror in mirrors) {
+ if (mirror !is NullType && mirror.annotationType.kind != TypeKind.NULL) {
+ builder.addAnnotation(AnnotationSpec.get(mirror))
+ }
+ }
+ }
+ var addComment = true
+ // Add super call if the method is not abstract
+ if (!method.modifiers.contains(Modifier.ABSTRACT)) {
+ if (method.returnType is NoType) {
+ builder.addStatement(createSuperCall(builder))
+ } else {
+ addComment = false
+ builder.addComment("TODO: Implement this method")
+ builder.addStatement("return " + createSuperCall(builder))
+ }
+ }
+ if (addComment) {
+ builder.addComment("TODO: Implement this method")
+ }
+ return builder
+ }
+
+ /**
+ * Create a superclass method invocation statement.
+ *
+ * @param builder The method builder.
+ * @return The super invocation statement string without ending ';'.
+ */
+ private fun createSuperCall(builder: MethodSpec.Builder): String {
+ val sb = java.lang.StringBuilder()
+ sb.append("super.")
+ sb.append(builder.name)
+ sb.append("(")
+ for (i in builder.parameters.indices) {
+ sb.append(builder.parameters[i].name)
+ if (i != builder.parameters.size - 1) {
+ sb.append(", ")
+ }
+ }
+ sb.append(")")
+ return sb.toString()
+ }
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java
new file mode 100644
index 0000000000..e83a1bce61
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java
@@ -0,0 +1,224 @@
+/*
+ * 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.java.utils;
+
+import com.itsaky.androidide.lsp.models.MarkupContent;
+import com.itsaky.androidide.lsp.models.MarkupKind;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.CharBuffer;
+import java.util.List;
+import java.util.StringJoiner;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import openjdk.source.doctree.DocCommentTree;
+import openjdk.source.doctree.DocTree;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class MarkdownHelper {
+
+ private static final Pattern HTML_TAG = Pattern.compile("<(\\w+)[^>]*>");
+ private static final Logger LOG = Logger.getLogger("main");
+
+ public static String asMarkdown(DocCommentTree comment) {
+ List extends DocTree> lines = comment.getFirstSentence();
+ return asMarkdown(lines);
+ }
+
+ /** If `commentText` looks like HTML, convert it to markdown */
+ public static String asMarkdown(String commentText) {
+ if (isHtml(commentText)) {
+ commentText = htmlToMarkdown(commentText);
+ }
+ commentText = replaceTags(commentText);
+ return commentText;
+ }
+
+ public static MarkupContent asMarkupContent(DocCommentTree comment) {
+ String markdown = asMarkdown(comment);
+ MarkupContent content = new MarkupContent();
+ content.setKind(MarkupKind.MARKDOWN);
+ content.setValue(markdown);
+ return content;
+ }
+
+ private static String asMarkdown(List extends DocTree> lines) {
+ StringJoiner join = new StringJoiner("\n");
+ for (DocTree l : lines)
+ join.add(l.toString());
+ String html = join.toString();
+ return asMarkdown(html);
+ }
+
+ private static void check(CharBuffer in, char expected) {
+ char head = in.get();
+ if (head != expected) {
+ throw new RuntimeException(String.format("want `%s` got `%s`", expected, head));
+ }
+ }
+
+ private static boolean empty(CharBuffer in) {
+ return in.position() == in.limit();
+ }
+
+ private static String htmlToMarkdown(String html) {
+ html = replaceTags(html);
+
+ Document doc = parse(html);
+
+ replaceNodes(doc, "i", contents -> String.format("*%s*", contents));
+ replaceNodes(doc, "b", contents -> String.format("**%s**", contents));
+ replaceNodes(doc, "pre", contents -> String.format("`%s`", contents));
+ replaceNodes(doc, "code", contents -> String.format("`%s`", contents));
+ replaceNodes(doc, "a", contents -> contents);
+
+ return print(doc);
+ }
+
+ private static boolean isHtml(String text) {
+ Matcher tags = HTML_TAG.matcher(text);
+ while (tags.find()) {
+ String tag = tags.group(1);
+ String close = String.format("%s>", tag);
+ int findClose = text.indexOf(close, tags.end());
+ if (findClose != -1)
+ return true;
+ }
+ return false;
+ }
+
+ private static void parse(CharBuffer in, StringBuilder out) {
+ while (!empty(in)) {
+ parseInner(in, out);
+ }
+ }
+
+ private static Document parse(String html) {
+ try {
+ String xml = "" + html + "";
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(false);
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ return builder.parse(new InputSource(new StringReader(xml)));
+ } catch (ParserConfigurationException | SAXException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void parseBlock(CharBuffer in, StringBuilder out) {
+ check(in, '{');
+ if (peek(in) == '@') {
+ String tag = parseTag(in);
+ if (peek(in) == ' ')
+ in.get();
+ switch (tag) {
+ case "code":
+ case "link":
+ case "linkplain":
+ out.append("`");
+ parseInner(in, out);
+ out.append("`");
+ break;
+ case "literal":
+ parseInner(in, out);
+ break;
+ default:
+ LOG.warning(String.format("Unknown tag `@%s`", tag));
+ parseInner(in, out);
+ }
+ } else {
+ parseInner(in, out);
+ }
+ check(in, '}');
+ }
+
+ private static void parseInner(CharBuffer in, StringBuilder out) {
+ while (!empty(in)) {
+ switch (peek(in)) {
+ case '{':
+ parseBlock(in, out);
+ break;
+ case '}':
+ return;
+ default:
+ out.append(in.get());
+ }
+ }
+ }
+
+ private static String parseTag(CharBuffer in) {
+ check(in, '@');
+ StringBuilder tag = new StringBuilder();
+ while (!empty(in) && Character.isAlphabetic(peek(in))) {
+ tag.append(in.get());
+ }
+ return tag.toString();
+ }
+
+ private static char peek(CharBuffer in) {
+ return in.get(in.position());
+ }
+
+ private static String print(Document doc) {
+ try {
+ TransformerFactory tf = TransformerFactory.newInstance();
+ Transformer transformer = tf.newTransformer();
+ transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+ StringWriter writer = new StringWriter();
+ transformer.transform(new DOMSource(doc), new StreamResult(writer));
+ String wrapped = writer.getBuffer().toString();
+ return wrapped.substring("".length(), wrapped.length() - "".length());
+ } catch (TransformerException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void replaceNodes(Document doc, String tagName, Function replace) {
+ NodeList nodes = doc.getElementsByTagName(tagName);
+ while (nodes.getLength() > 0) {
+ Node node = nodes.item(0);
+ Node parent = node.getParentNode();
+ String text = replace.apply(node.getTextContent().trim());
+ Node replacement = doc.createTextNode(text);
+ parent.replaceChild(replacement, node);
+ nodes = doc.getElementsByTagName(tagName);
+ }
+ }
+
+ private static String replaceTags(String in) {
+ StringBuilder out = new StringBuilder();
+ parse(CharBuffer.wrap(in), out);
+ return out.toString();
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java
new file mode 100644
index 0000000000..8e415d4636
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java
@@ -0,0 +1,105 @@
+/*
+ * 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.java.utils;
+
+import androidx.annotation.NonNull;
+import com.squareup.javapoet.ImportCollectingCodeWriter;
+import com.squareup.javapoet.TypeName;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+import jdkx.lang.model.element.ExecutableElement;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.element.VariableElement;
+import jdkx.lang.model.type.TypeKind;
+import jdkx.lang.model.type.TypeMirror;
+import jdkx.lang.model.util.Types;
+import openjdk.source.util.JavacTask;
+
+/**
+ * @author Akash Yadav
+ */
+public class MethodPtr {
+
+ public String className, methodName;
+ public String[] erasedParameterTypes;
+ public String[] simplifiedErasedParameterTypes;
+
+ public MethodPtr(@NonNull JavacTask task, @NonNull ExecutableElement method) {
+ final Types types = task.getTypes();
+ final TypeElement parent = (TypeElement) method.getEnclosingElement();
+ className = parent.getQualifiedName().toString();
+ methodName = method.getSimpleName().toString();
+ erasedParameterTypes = new String[method.getParameters().size()];
+ simplifiedErasedParameterTypes = new String[erasedParameterTypes.length];
+
+ for (int i = 0; i < erasedParameterTypes.length; i++) {
+ final VariableElement param = method.getParameters().get(i);
+ final TypeMirror type = param.asType();
+ final TypeMirror erased = types.erasure(type);
+ erasedParameterTypes[i] = erased.toString();
+ simplifiedErasedParameterTypes[i] = simplify(erased);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof MethodPtr)) {
+ return false;
+ }
+ MethodPtr methodPtr = (MethodPtr) o;
+ return Objects.equals(className, methodPtr.className)
+ && Objects.equals(methodName, methodPtr.methodName)
+ && Arrays.equals(erasedParameterTypes, methodPtr.erasedParameterTypes)
+ && Arrays.equals(simplifiedErasedParameterTypes, methodPtr.simplifiedErasedParameterTypes);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(className, methodName);
+ result = 31 * result + Arrays.hashCode(erasedParameterTypes);
+ result = 31 * result + Arrays.hashCode(simplifiedErasedParameterTypes);
+ return result;
+ }
+
+ @NonNull
+ private String getSimpleName(TypeName name) throws IOException {
+ final StringBuilder sb = new StringBuilder();
+ final ImportCollectingCodeWriter writer = new ImportCollectingCodeWriter(sb);
+ writer.setPrintQualifiedNames(false);
+ writer.emit(name);
+ return sb.toString();
+ }
+
+ private String simplify(@NonNull TypeMirror type) {
+
+ if (type.getKind() == TypeKind.NULL) {
+ return type.toString();
+ }
+
+ final TypeName name = TypeName.get(type);
+ try {
+ return getSimpleName(name);
+ } catch (IOException e) {
+ return type.toString();
+ }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java
similarity index 54%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java
index 309d5fe1fb..3e517ae421 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java
@@ -47,65 +47,65 @@
public class NavigationHelper {
- @Nullable
- public static Element findElement(CompileTask task, Path file, int line, int column, ICancelChecker cancelChecker) {
- Trees trees = Trees.instance(task.task);
- for (CompilationUnitTree root : task.roots) {
- if (cancelChecker != null) {
- cancelChecker.abortIfCancelled();
- }
+ @Nullable
+ public static Element findElement(CompileTask task, Path file, int line, int column, ICancelChecker cancelChecker) {
+ Trees trees = Trees.instance(task.task);
+ for (CompilationUnitTree root : task.roots) {
+ if (cancelChecker != null) {
+ cancelChecker.abortIfCancelled();
+ }
- if (root.getSourceFile().toUri().equals(file.toUri())) {
- long cursor = root.getLineMap().getPosition(line, column);
- TreePath path = new FindNameAt(task).scan(root, cursor);
- if (cancelChecker != null) {
- cancelChecker.abortIfCancelled();
- }
- if (path == null) {
- return null;
- }
- return trees.getElement(path);
- }
- }
- throw new RuntimeException("file not found");
- }
+ if (root.getSourceFile().toUri().equals(file.toUri())) {
+ long cursor = root.getLineMap().getPosition(line, column);
+ TreePath path = new FindNameAt(task).scan(root, cursor);
+ if (cancelChecker != null) {
+ cancelChecker.abortIfCancelled();
+ }
+ if (path == null) {
+ return null;
+ }
+ return trees.getElement(path);
+ }
+ }
+ throw new RuntimeException("file not found");
+ }
- public static boolean isLocal(Element element) {
- if (element.getModifiers().contains(Modifier.PRIVATE)) {
- return true;
- }
- switch (element.getKind()) {
- case EXCEPTION_PARAMETER:
- case LOCAL_VARIABLE:
- case PARAMETER:
- case TYPE_PARAMETER:
- return true;
- default:
- return false;
- }
- }
+ public static boolean isLocal(Element element) {
+ if (element.getModifiers().contains(Modifier.PRIVATE)) {
+ return true;
+ }
+ switch (element.getKind()) {
+ case EXCEPTION_PARAMETER:
+ case LOCAL_VARIABLE:
+ case PARAMETER:
+ case TYPE_PARAMETER:
+ return true;
+ default:
+ return false;
+ }
+ }
- public static boolean isMember(Element element) {
- switch (element.getKind()) {
- case ENUM_CONSTANT:
- case FIELD:
- case METHOD:
- case CONSTRUCTOR:
- return true;
- default:
- return false;
- }
- }
+ public static boolean isMember(Element element) {
+ switch (element.getKind()) {
+ case ENUM_CONSTANT:
+ case FIELD:
+ case METHOD:
+ case CONSTRUCTOR:
+ return true;
+ default:
+ return false;
+ }
+ }
- public static boolean isType(Element element) {
- switch (element.getKind()) {
- case ANNOTATION_TYPE:
- case CLASS:
- case ENUM:
- case INTERFACE:
- return true;
- default:
- return false;
- }
- }
+ public static boolean isType(Element element) {
+ switch (element.getKind()) {
+ case ANNOTATION_TYPE:
+ case CLASS:
+ case ENUM:
+ case INTERFACE:
+ return true;
+ default:
+ return false;
+ }
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java
new file mode 100644
index 0000000000..73d9934af8
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java
@@ -0,0 +1,93 @@
+/*
+ * 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.java.utils;
+
+import com.itsaky.androidide.lsp.java.compiler.CompileTask;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import jdkx.lang.model.element.Element;
+import jdkx.lang.model.element.Modifier;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.util.Elements;
+import openjdk.source.tree.Scope;
+import openjdk.source.util.Trees;
+
+public class ScopeHelper {
+ // TODO is this still necessary? Test speed. We could get rid of the extra static-imports step.
+ public static List fastScopes(Scope start) {
+ List scopes = new ArrayList<>();
+ for (Scope s = start; s != null; s = s.getEnclosingScope()) {
+ scopes.add(s);
+ }
+ // Scopes may be contained in an enclosing scope.
+ // The outermost scope contains those elements available via "star import" declarations;
+ // the scope within that contains the top level elements of the compilation unit, including
+ // any named imports.
+ // https://parent.docs.oracle.com/en/java/javase/11/docs/api/jdk.compiler/com/sun/source/tree/Scope.html
+ return scopes.subList(0, scopes.size() - 2);
+ }
+
+ public static List scopeMembers(
+ CompileTask task, Scope inner, Predicate filter) {
+ Trees trees = Trees.instance(task.task);
+ Elements elements = task.task.getElements();
+ boolean isStatic = false;
+ List list = new ArrayList<>();
+ for (Scope scope : fastScopes(inner)) {
+ if (scope.getEnclosingMethod() != null) {
+ isStatic = isStatic || scope.getEnclosingMethod().getModifiers().contains(Modifier.STATIC);
+ }
+ for (Element member : scope.getLocalElements()) {
+ if (!filter.test(member.getSimpleName())) {
+ continue;
+ }
+ if ("".contentEquals(member.getSimpleName())
+ || "".contentEquals(member.getSimpleName())) {
+ continue;
+ }
+ if (isStatic && member.getSimpleName().contentEquals("this")) {
+ continue;
+ }
+ if (isStatic && member.getSimpleName().contentEquals("super")) {
+ continue;
+ }
+ list.add(member);
+ }
+ if (scope.getEnclosingClass() != null) {
+ TypeElement typeElement = scope.getEnclosingClass();
+ DeclaredType typeType = (DeclaredType) typeElement.asType();
+ for (Element member : elements.getAllMembers(typeElement)) {
+ if (!filter.test(member.getSimpleName())) {
+ continue;
+ }
+ if (!trees.isAccessible(scope, member, typeType)) {
+ continue;
+ }
+ if (isStatic && !member.getModifiers().contains(Modifier.STATIC)) {
+ continue;
+ }
+ list.add(member);
+ }
+ isStatic = isStatic || typeElement.getModifiers().contains(Modifier.STATIC);
+ }
+ }
+ return list;
+ }
+}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java
new file mode 100644
index 0000000000..17e9f4a7b1
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java
@@ -0,0 +1,111 @@
+package com.itsaky.androidide.lsp.java.utils;
+
+import java.util.stream.Collectors;
+import jdkx.lang.model.type.ArrayType;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.type.ErrorType;
+import jdkx.lang.model.type.ExecutableType;
+import jdkx.lang.model.type.IntersectionType;
+import jdkx.lang.model.type.NoType;
+import jdkx.lang.model.type.NullType;
+import jdkx.lang.model.type.PrimitiveType;
+import jdkx.lang.model.type.TypeMirror;
+import jdkx.lang.model.type.TypeVariable;
+import jdkx.lang.model.type.UnionType;
+import jdkx.lang.model.type.WildcardType;
+import jdkx.lang.model.util.AbstractTypeVisitor8;
+
+public class ShortTypePrinter extends AbstractTypeVisitor8 {
+ public static final ShortTypePrinter NO_PACKAGE = new ShortTypePrinter("*");
+
+ private final String packageContext;
+
+ private ShortTypePrinter(String packageContext) {
+ this.packageContext = packageContext;
+ }
+
+ public String print(TypeMirror type) {
+ return type.accept(new ShortTypePrinter(packageContext), null);
+ }
+
+ @Override
+ public String visitArray(ArrayType t, Void aVoid) {
+ return print(t.getComponentType()) + "[]";
+ }
+
+ @Override
+ public String visitDeclared(DeclaredType t, Void aVoid) {
+ String result = t.asElement().toString();
+
+ if (!t.getTypeArguments().isEmpty()) {
+ String params = t.getTypeArguments().stream().map(this::print).collect(Collectors.joining(", "));
+
+ result += "<" + params + ">";
+ }
+
+ if (packageContext.equals("*"))
+ return result.substring(result.lastIndexOf('.') + 1);
+ else if (result.startsWith("java.lang"))
+ return result.substring("java.lang.".length());
+ else if (result.startsWith("java.util"))
+ return result.substring("java.util.".length());
+ else if (result.startsWith(packageContext))
+ return result.substring(packageContext.length());
+ else
+ return result;
+ }
+
+ @Override
+ public String visitError(ErrorType t, Void aVoid) {
+ return "_";
+ }
+
+ @Override
+ public String visitExecutable(ExecutableType t, Void aVoid) {
+ return t.toString();
+ }
+
+ @Override
+ public String visitIntersection(IntersectionType t, Void aVoid) {
+ return t.getBounds().stream().map(this::print).collect(Collectors.joining(" & "));
+ }
+
+ @Override
+ public String visitNoType(NoType t, Void aVoid) {
+ return t.toString();
+ }
+
+ @Override
+ public String visitNull(NullType t, Void aVoid) {
+ return t.toString();
+ }
+
+ @Override
+ public String visitPrimitive(PrimitiveType t, Void aVoid) {
+ return t.toString();
+ }
+
+ @Override
+ public String visitTypeVariable(TypeVariable t, Void aVoid) {
+ return t.asElement().toString();
+ }
+
+ @Override
+ public String visitUnion(UnionType t, Void aVoid) {
+ return t.getAlternatives().stream().map(this::print).collect(Collectors.joining(" | "));
+ }
+
+ @Override
+ public String visitWildcard(WildcardType t, Void aVoid) {
+ String result = "?";
+ if (t.getSuperBound() != null) {
+ result += " super " + print(t.getSuperBound());
+ }
+
+ if (t.getExtendsBound() != null) {
+ result += " extends " + print(t.getExtendsBound());
+ }
+
+ return result;
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt
similarity index 74%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt
index c3362e37b9..8c79ff397a 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt
@@ -1,37 +1,36 @@
-/*
- * 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.java.utils
-
-/**
- * Utility related to tests.
- *
- * @author Akash Yadav
- */
-class TestUtils {
- companion object {
- @JvmStatic
- fun isTestEnvironment(): Boolean {
- return try {
- Class.forName("org.robolectric.RobolectricTestRunner")
- true
- } catch (error: ClassNotFoundException) {
- false
- }
- }
- }
-}
+/*
+ * 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.java.utils
+
+/**
+ * Utility related to tests.
+ *
+ * @author Akash Yadav
+ */
+class TestUtils {
+ companion object {
+ @JvmStatic
+ fun isTestEnvironment(): Boolean =
+ try {
+ Class.forName("org.robolectric.RobolectricTestRunner")
+ true
+ } catch (error: ClassNotFoundException) {
+ false
+ }
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt
similarity index 57%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt
index a9e67596a0..67cebe93ec 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt
@@ -31,45 +31,38 @@ import openjdk.source.tree.Tree.Kind.METHOD
* @author Akash Yadav
*/
class TreeUtils {
+ companion object {
+ @JvmStatic
+ fun isType(tree: Tree?): Boolean = isType(tree?.kind)
- companion object {
- @JvmStatic
- fun isType(tree: Tree?): Boolean {
- return isType(tree?.kind)
- }
+ @JvmStatic
+ fun isType(kind: Tree.Kind?): Boolean {
+ kind ?: return false
- @JvmStatic
- fun isType(kind: Tree.Kind?): Boolean {
- kind ?: return false
+ return when (kind) {
+ CLASS,
+ INTERFACE,
+ ANNOTATION_TYPE,
+ ENUM,
+ -> true
- return when (kind) {
- CLASS,
- INTERFACE,
- ANNOTATION_TYPE,
- ENUM -> true
- else -> false
- }
- }
+ else -> false
+ }
+ }
- @JvmStatic
- fun isMethod(tree: Tree?): Boolean {
- return isMethod(tree?.kind)
- }
+ @JvmStatic
+ fun isMethod(tree: Tree?): Boolean = isMethod(tree?.kind)
- @JvmStatic
- fun isMethod(kind: Tree.Kind?): Boolean {
- return kind == METHOD
- }
+ @JvmStatic
+ fun isMethod(kind: Tree.Kind?): Boolean = kind == METHOD
- @JvmStatic
- fun isConstructor(tree: Tree?): Boolean {
- tree ?: return false
- return tree.kind == METHOD && (tree as MethodTree).name.contentEquals("")
- }
+ @JvmStatic
+ fun isConstructor(tree: Tree?): Boolean {
+ tree ?: return false
+ return tree.kind == METHOD && (tree as MethodTree).name.contentEquals("")
+ }
- @JvmStatic
- fun isMethodOrConstructor(tree: Tree?): Boolean {
- return isMethod(tree) || isConstructor(tree)
- }
- }
+ @JvmStatic
+ fun isMethodOrConstructor(tree: Tree?): Boolean = isMethod(tree) || isConstructor(tree)
+ }
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java
new file mode 100644
index 0000000000..e3d5a7c6ae
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java
@@ -0,0 +1,286 @@
+/*
+ * 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.java.utils;
+
+import static com.itsaky.androidide.projects.util.StringSearch.containsClass;
+import static com.itsaky.androidide.projects.util.StringSearch.containsInterface;
+
+import com.github.javaparser.StaticJavaParser;
+import com.github.javaparser.ast.NodeList;
+import com.github.javaparser.ast.expr.SimpleName;
+import com.github.javaparser.ast.type.ArrayType;
+import com.github.javaparser.ast.type.ClassOrInterfaceType;
+import com.github.javaparser.ast.type.IntersectionType;
+import com.github.javaparser.ast.type.PrimitiveType;
+import com.github.javaparser.ast.type.ReferenceType;
+import com.github.javaparser.ast.type.Type;
+import com.github.javaparser.ast.type.TypeParameter;
+import com.github.javaparser.ast.type.UnknownType;
+import com.github.javaparser.ast.type.VoidType;
+import com.github.javaparser.ast.type.WildcardType;
+import com.github.javaparser.printer.DefaultPrettyPrinter;
+import com.github.javaparser.printer.configuration.DefaultPrinterConfiguration;
+import com.github.javaparser.printer.configuration.PrinterConfiguration;
+import com.itsaky.androidide.lsp.java.visitors.PrettyPrintingVisitor;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.function.Predicate;
+import jdkx.lang.model.element.TypeElement;
+import jdkx.lang.model.type.DeclaredType;
+import jdkx.lang.model.type.NoType;
+import jdkx.lang.model.type.TypeKind;
+import jdkx.lang.model.type.TypeMirror;
+import jdkx.lang.model.type.TypeVariable;
+import openjdk.source.tree.IdentifierTree;
+import openjdk.source.tree.ParameterizedTypeTree;
+import openjdk.source.tree.PrimitiveTypeTree;
+import openjdk.source.tree.Tree;
+import openjdk.source.tree.TypeParameterTree;
+import openjdk.source.tree.WildcardTree;
+import openjdk.tools.javac.code.BoundKind;
+import openjdk.tools.javac.tree.JCTree;
+
+public class TypeUtils {
+
+ public static boolean containsType(Path file, TypeElement el) {
+ switch (el.getKind()) {
+ case INTERFACE:
+ return containsInterface(file, el.getSimpleName().toString());
+ case CLASS:
+ return containsClass(file, el.getSimpleName().toString());
+ default:
+ throw new RuntimeException("Don't know what to do with " + el.getKind());
+ }
+ }
+
+ public static String getName(Type type, Predicate needFqnDelegate) {
+ PrinterConfiguration configuration = new DefaultPrinterConfiguration();
+ PrettyPrintingVisitor visitor = new PrettyPrintingVisitor(configuration) {
+ @Override
+ public void visit(SimpleName n, Void arg) {
+ printOrphanCommentsBeforeThisChildNode(n);
+ printComment(n.getComment(), arg);
+
+ String identifier = n.getIdentifier();
+ if (needFqnDelegate.test(identifier)) {
+ printer.print(identifier);
+ } else {
+ printer.print(JavaParserUtils.getSimpleName(identifier));
+ }
+ }
+ };
+ DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(t -> visitor, configuration);
+ return prettyPrinter.print(type);
+ }
+
+ public static Type getPrimitiveType(PrimitiveTypeTree tree) {
+ Type type;
+ switch (tree.getPrimitiveTypeKind()) {
+ case INT:
+ type = PrimitiveType.intType();
+ break;
+ case BOOLEAN:
+ type = PrimitiveType.booleanType();
+ break;
+ case LONG:
+ type = PrimitiveType.longType();
+ break;
+ case SHORT:
+ type = PrimitiveType.shortType();
+ break;
+ case CHAR:
+ type = PrimitiveType.charType();
+ break;
+ case FLOAT:
+ type = PrimitiveType.floatType();
+ break;
+ case VOID:
+ type = new VoidType();
+ break;
+ default:
+ type = new UnknownType();
+ }
+ return type;
+ }
+
+ public static String getSimpleName(Type type) {
+ PrinterConfiguration configuration = new DefaultPrinterConfiguration();
+ PrettyPrintingVisitor visitor = new PrettyPrintingVisitor(configuration);
+ DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(t -> visitor, configuration);
+ return prettyPrinter.print(type);
+ }
+
+ // type mirrors
+
+ public static ArrayType toArrayType(jdkx.lang.model.type.ArrayType type) {
+ Type componentType = toType(type.getComponentType());
+ return new ArrayType(componentType);
+ }
+
+ public static ClassOrInterfaceType toClassOrInterfaceType(DeclaredType type) {
+ ClassOrInterfaceType classOrInterfaceType = new ClassOrInterfaceType();
+ if (!type.getTypeArguments().isEmpty()) {
+ classOrInterfaceType.setTypeArguments(
+ type.getTypeArguments().stream()
+ .map(TypeUtils::toType)
+ .filter(Objects::nonNull)
+ .collect(NodeList.toNodeList()));
+ }
+ if (!type.asElement().toString().isEmpty()) {
+ classOrInterfaceType.setName(type.asElement().toString());
+ }
+ return classOrInterfaceType;
+ }
+
+ // trees
+ public static ClassOrInterfaceType toClassOrInterfaceType(Tree tree) {
+ ClassOrInterfaceType type = new ClassOrInterfaceType();
+ if (tree instanceof IdentifierTree) {
+ type.setName(((IdentifierTree) tree).getName().toString());
+ }
+ if (tree instanceof ParameterizedTypeTree) {
+ ParameterizedTypeTree parameterizedTypeTree = (ParameterizedTypeTree) tree;
+ Type t = toType(parameterizedTypeTree.getType());
+
+ NodeList typeArguments = new NodeList<>();
+ for (Tree typeArgument : parameterizedTypeTree.getTypeArguments()) {
+ Type typ = toType(typeArgument);
+ typeArguments.add(typ);
+ }
+ if (t.isClassOrInterfaceType()) {
+ type.setName(t.asClassOrInterfaceType().getName());
+ }
+ type.setTypeArguments(typeArguments);
+ }
+ return type;
+ }
+
+ public static IntersectionType toIntersectionType(jdkx.lang.model.type.IntersectionType type) {
+ NodeList collect = type.getBounds().stream()
+ .map(TypeUtils::toType)
+ .map(it -> ((ReferenceType) it))
+ .collect(NodeList.toNodeList());
+ return new IntersectionType(collect);
+ }
+
+ public static PrimitiveType toPrimitiveType(jdkx.lang.model.type.PrimitiveType type) {
+ PrimitiveType.Primitive primitive = PrimitiveType.Primitive.valueOf(type.getKind().name());
+ return new PrimitiveType(primitive);
+ }
+
+ public static Type toType(Tree tree) {
+ Type type;
+ if (tree instanceof PrimitiveTypeTree) {
+ type = getPrimitiveType((PrimitiveTypeTree) tree);
+ } else if (tree instanceof IdentifierTree) {
+ type = toClassOrInterfaceType(tree);
+ } else if (tree instanceof WildcardTree) {
+ JCTree.JCWildcard wildcardTree = (JCTree.JCWildcard) tree;
+ WildcardType wildcardType = new WildcardType();
+ Tree bound = wildcardTree.getBound();
+ Type boundType = toType(bound);
+ if (wildcardTree.kind.kind == BoundKind.EXTENDS) {
+ wildcardType.setExtendedType((ReferenceType) boundType);
+ } else {
+ wildcardType.setSuperType((ReferenceType) boundType);
+ }
+ type = wildcardType;
+ } else if (tree instanceof ParameterizedTypeTree) {
+ type = toClassOrInterfaceType(tree);
+ } else if (tree instanceof TypeParameterTree) {
+ TypeParameter typeParameter = new TypeParameter();
+ typeParameter.setName(((TypeParameterTree) tree).getName().toString());
+ typeParameter.setTypeBound(
+ ((TypeParameterTree) tree)
+ .getBounds().stream()
+ .map(TypeUtils::toClassOrInterfaceType)
+ .collect(NodeList.toNodeList()));
+ type = typeParameter;
+ } else {
+ if (tree != null) {
+ type = StaticJavaParser.parseType(tree.toString());
+ } else {
+ type = null;
+ }
+ }
+ return type;
+ }
+
+ public static Type toType(TypeMirror typeMirror) {
+ if (typeMirror.getKind() == TypeKind.ARRAY) {
+ return toArrayType((jdkx.lang.model.type.ArrayType) typeMirror);
+ }
+ if (typeMirror.getKind().isPrimitive()) {
+ return toPrimitiveType((jdkx.lang.model.type.PrimitiveType) typeMirror);
+ }
+ if (typeMirror instanceof jdkx.lang.model.type.IntersectionType) {
+ return toIntersectionType((jdkx.lang.model.type.IntersectionType) typeMirror);
+ }
+ if (typeMirror instanceof jdkx.lang.model.type.WildcardType) {
+ return toWildcardType((jdkx.lang.model.type.WildcardType) typeMirror);
+ }
+ if (typeMirror instanceof jdkx.lang.model.type.DeclaredType) {
+ return toClassOrInterfaceType((DeclaredType) typeMirror);
+ }
+ if (typeMirror instanceof jdkx.lang.model.type.TypeVariable) {
+ return toType(((TypeVariable) typeMirror));
+ }
+ if (typeMirror instanceof NoType) {
+ return new VoidType();
+ }
+ return null;
+ }
+
+ public static Type toType(TypeVariable typeVariable) {
+ TypeParameter typeParameter = new TypeParameter();
+ TypeMirror upperBound = typeVariable.getUpperBound();
+
+ if (!typeVariable.equals(upperBound)) {
+ Type type = toType(upperBound);
+ if (type != null) {
+ if (type.isIntersectionType()) {
+ typeParameter.setTypeBound(
+ type.asIntersectionType().getElements().stream()
+ .filter(Type::isClassOrInterfaceType)
+ .map(Type::asClassOrInterfaceType)
+ .collect(NodeList.toNodeList()));
+ } else if (type.isClassOrInterfaceType()) {
+ typeParameter.setTypeBound(NodeList.nodeList(type.asClassOrInterfaceType()));
+ }
+ }
+ }
+ typeParameter.setName(typeVariable.toString());
+ return typeParameter;
+ }
+
+ public static WildcardType toWildcardType(jdkx.lang.model.type.WildcardType type) {
+ WildcardType wildcardType = new WildcardType();
+ if (type.getSuperBound() != null) {
+ Type result = toType(type.getSuperBound());
+ if (result instanceof ReferenceType) {
+ wildcardType.setSuperType((ReferenceType) result);
+ } else if (result instanceof WildcardType) {
+ wildcardType = result.asWildcardType();
+ }
+ }
+
+ if (type.getExtendsBound() != null) {
+ wildcardType.setExtendedType((ReferenceType) toType(type.getExtendsBound()));
+ }
+ return wildcardType;
+ }
+}
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt
similarity index 62%
rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt
rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt
index 5375b05d0b..3ef4b148e5 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt
@@ -27,41 +27,41 @@ import openjdk.source.util.Trees
/** @author Akash Yadav */
fun positionForImports(className: String, task: ParseTask): Position {
- return positionForImports(className, task.task, task.root)
+return positionForImports(className, task.task, task.root)
}
fun positionForImports(className: String, task: CompileTask): Position {
- return positionForImports(className, task.task, task.root())
+return positionForImports(className, task.task, task.root())
}
fun positionForImports(className: String, task: JavacTask, root: CompilationUnitTree): Position {
- val imports = root.imports
- for (i in imports) {
- val next = i.qualifiedIdentifier.toString()
- if (className < next) {
- return insertBefore(task, root, i)
- }
- }
- if (imports.isNotEmpty()) {
- val last = imports[imports.size - 1]
- return insertAfter(task, root, last)
- }
+val imports = root.imports
+for (i in imports) {
+ val next = i.qualifiedIdentifier.toString()
+ if (className < next) {
+ return insertBefore(task, root, i)
+ }
+}
+if (imports.isNotEmpty()) {
+ val last = imports[imports.size - 1]
+ return insertAfter(task, root, last)
+}
- return if (root.getPackage() != null) {
- insertAfter(task, root, root.getPackage())
- } else Position(0, 0)
+return if (root.getPackage() != null) {
+ insertAfter(task, root, root.getPackage())
+} else Position(0, 0)
}
fun insertBefore(task: JavacTask, root: CompilationUnitTree, tree: Tree): Position {
- val pos = Trees.instance(task).sourcePositions
- val offset = pos.getStartPosition(root, tree)
- val line = root.lineMap.getLineNumber(offset).toInt()
- return Position(line - 1, 0)
+val pos = Trees.instance(task).sourcePositions
+val offset = pos.getStartPosition(root, tree)
+val line = root.lineMap.getLineNumber(offset).toInt()
+return Position(line - 1, 0)
}
fun insertAfter(task: JavacTask, root: CompilationUnitTree, tree: Tree): Position {
- val pos = Trees.instance(task).sourcePositions
- val offset = pos.getStartPosition(root, tree)
- val line = root.lineMap.getLineNumber(offset).toInt()
- return Position(line, 0)
+val pos = Trees.instance(task).sourcePositions
+val offset = pos.getStartPosition(root, tree)
+val line = root.lineMap.getLineNumber(offset).toInt()
+return Position(line, 0)
}
diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt
new file mode 100644
index 0000000000..5c1c9bc023
--- /dev/null
+++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt
@@ -0,0 +1,442 @@
+/*
+ * 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.java.visitors
+
+import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled
+import jdkx.lang.model.element.Element
+import jdkx.lang.model.element.ExecutableElement
+import jdkx.lang.model.element.Modifier
+import jdkx.lang.model.element.TypeElement
+import jdkx.lang.model.type.DeclaredType
+import jdkx.lang.model.type.TypeKind
+import jdkx.lang.model.type.TypeMirror
+import openjdk.source.tree.BlockTree
+import openjdk.source.tree.ClassTree
+import openjdk.source.tree.CompilationUnitTree
+import openjdk.source.tree.DoWhileLoopTree
+import openjdk.source.tree.ForLoopTree
+import openjdk.source.tree.IdentifierTree
+import openjdk.source.tree.IfTree
+import openjdk.source.tree.MemberReferenceTree
+import openjdk.source.tree.MemberSelectTree
+import openjdk.source.tree.MethodInvocationTree
+import openjdk.source.tree.MethodTree
+import openjdk.source.tree.NewClassTree
+import openjdk.source.tree.ThrowTree
+import openjdk.source.tree.Tree
+import openjdk.source.tree.TryTree
+import openjdk.source.tree.VariableTree
+import openjdk.source.tree.WhileLoopTree
+import openjdk.source.util.TreePath
+import openjdk.source.util.TreeScanner
+import openjdk.source.util.Trees
+import openjdk.tools.javac.api.JavacTaskImpl
+import java.util.Objects
+
+class DiagnosticVisitor(
+ task: JavacTaskImpl?,
+) : TreeScanner>() {
+ private val trees = Trees.instance(task)
+ private val privateDeclarations = mutableMapOf()
+ private val localVariables = mutableMapOf()
+ private val used = mutableSetOf()
+ private var declaredExceptions = mutableMapOf()
+ private var observedExceptions = mutableSetOf()
+ val emptyBlocks = mutableMapOf()
+
+ // Copied from TreePathScanner
+ // We need to be able to call scan(path, _) recursively
+ private var path: TreePath? = null
+
+ private fun scanPath(path: TreePath) {
+ abortIfCancelled()
+ val prev = this.path
+ this.path = path
+ try {
+ path.leaf.accept(this, null)
+ } finally {
+ this.path = prev // So we can call scan(path, _) recursively
+ }
+ }
+
+ override fun scan(
+ tree: Tree?,
+ p: MutableMap?,
+ ): Void? {
+ abortIfCancelled()
+ if (tree == null) {
+ return null
+ }
+
+ val prev = path
+ path = TreePath(path, tree)
+ return try {
+ tree.accept(this, p)
+ } finally {
+ path = prev
+ }
+ }
+
+ fun notUsed(): Set {
+ val unused = mutableSetOf()
+ unused.addAll(privateDeclarations.keys)
+ unused.addAll(localVariables.keys)
+ unused.removeAll(used)
+ // Remove if there are any null elements somehow ended up being added
+ // during async work which calls `lint`
+ unused.removeIf { Objects.isNull(it) }
+ // Remove if