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 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 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 __) { + // 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 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 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 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 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 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 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 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("", 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 field was injected while forming the AST + unused.removeIf { it.toString() == "" } + return unused + } + + private fun foundPrivateDeclaration() { + abortIfCancelled() + val element = trees.getElement(path) ?: return + privateDeclarations[element] = path!! + } + + private fun foundLocalVariable() { + abortIfCancelled() + val element = trees.getElement(path) ?: return + localVariables[element] = path!! + } + + private fun foundReference() { + abortIfCancelled() + val toEl = trees.getElement(path) ?: return + if (toEl.asType().kind == TypeKind.ERROR) { + foundPseudoReference(toEl) + return + } + + sweep(toEl) + } + + private fun foundPseudoReference(toEl: Element) { + abortIfCancelled() + val parent = toEl.enclosingElement as? TypeElement ?: return + val memberName = toEl.simpleName + for (member in parent.enclosedElements) { + if (member.simpleName.contentEquals(memberName)) { + sweep(member) + } + } + } + + private fun sweep(toEl: Element) { + abortIfCancelled() + val firstUse = used.add(toEl) + val notScanned = firstUse && privateDeclarations.containsKey(toEl) + if (notScanned) { + scanPath(privateDeclarations[toEl]!!) + } + } + + private fun isReachable(path: TreePath): Boolean { + abortIfCancelled() + // Check if t is reachable because it's public + val leaf = path.leaf + if (leaf is VariableTree) { + val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) + if (!isPrivate || isLocalVariable(path)) { + return true + } + } + + if (leaf is MethodTree) { + val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) + val isEmptyConstructor = leaf.parameters.isEmpty() && leaf.returnType == null + if (!isPrivate || isEmptyConstructor) { + return true + } + } + + if (leaf is ClassTree) { + val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) + if (!isPrivate) { + return true + } + } + + abortIfCancelled() + + // Check if t has been referenced by a reachable element + val el = trees.getElement(path) + return used.contains(el) + } + + private fun isLocalVariable(path: TreePath): Boolean { + abortIfCancelled() + val kind = path.leaf.kind + if (kind != Tree.Kind.VARIABLE) { + return false + } + + val parent = path.parentPath.leaf.kind + if (parent == Tree.Kind.CLASS || parent == Tree.Kind.INTERFACE) { + return false + } + + if (parent == Tree.Kind.METHOD) { + val method = path.parentPath.leaf as MethodTree + return method.body != null + } + + return true + } + + private fun declared(t: MethodTree): MutableMap { + abortIfCancelled() + val names = mutableMapOf() + for (e in t.throws) { + val path = TreePath(path, e) + val to = trees.getElement(path) as? TypeElement ?: continue + val name = to.qualifiedName.toString() + names[name] = path + } + return names + } + + override fun visitCompilationUnit( + t: CompilationUnitTree, + notThrown: MutableMap, + ): Void? { + abortIfCancelled() + return super.visitCompilationUnit(t, notThrown) + } + + override fun visitVariable( + t: VariableTree?, + notThrown: MutableMap?, + ): Void? { + when { + isLocalVariable(path!!) -> { + foundLocalVariable() + super.visitVariable(t, notThrown) + } + + isReachable(path!!) -> { + super.visitVariable(t, notThrown) + } + + else -> { + foundPrivateDeclaration() + } + } + return null + } + + override fun visitMethod( + t: MethodTree?, + notThrown: MutableMap?, + ): Void? { + abortIfCancelled() + if (t == null || notThrown == null) { + return null + } + + // Create a new method scope + val pushDeclared = declaredExceptions + val pushObserved = observedExceptions + declaredExceptions = declared(t) + observedExceptions = HashSet() + + abortIfCancelled() + // Recursively scan for 'throw' and method calls + super.visitMethod(t, notThrown) + abortIfCancelled() + + // Check for exceptions that were never thrown + for (exception in declaredExceptions.keys) { + if (!observedExceptions.contains(exception)) { + notThrown[declaredExceptions[exception]] = exception + } + } + declaredExceptions = pushDeclared + observedExceptions = pushObserved + if (!isReachable(path!!)) { + foundPrivateDeclaration() + } + return null + } + + override fun visitClass( + t: ClassTree?, + notThrown: MutableMap?, + ): Void? { + if (isReachable(path!!)) { + super.visitClass(t, notThrown) + } else { + foundPrivateDeclaration() + } + return null + } + + override fun visitIdentifier( + t: IdentifierTree?, + notThrown: MutableMap?, + ): Void? { + foundReference() + return super.visitIdentifier(t, notThrown) + } + + override fun visitMemberSelect( + t: MemberSelectTree?, + notThrown: MutableMap?, + ): Void? { + foundReference() + return super.visitMemberSelect(t, notThrown) + } + + override fun visitMemberReference( + t: MemberReferenceTree?, + notThrown: MutableMap?, + ): Void? { + foundReference() + return super.visitMemberReference(t, notThrown) + } + + override fun visitNewClass( + t: NewClassTree?, + notThrown: MutableMap?, + ): Void? { + foundReference() + return super.visitNewClass(t, notThrown) + } + + override fun visitThrow( + t: ThrowTree?, + notThrown: MutableMap?, + ): Void? { + abortIfCancelled() + if (t == null) { + return null + } + + val path = TreePath(path, t.expression) + val type = trees.getTypeMirror(path) + addThrown(type) + return super.visitThrow(t, notThrown) + } + + override fun visitMethodInvocation( + t: MethodInvocationTree?, + notThrown: MutableMap?, + ): Void? { + abortIfCancelled() + val target = trees.getElement(path) + if (target is ExecutableElement) { + for (type in target.thrownTypes) { + addThrown(type) + } + } + + return super.visitMethodInvocation(t, notThrown) + } + + override fun visitBlock( + node: BlockTree?, + p: MutableMap?, + ): Void? { + abortIfCancelled() + if (node != null && node.statements.isEmpty()) { + val name: String? = + when (val parent = path!!.parentPath.leaf) { + is IfTree -> fromIfTree(node, parent) + is TryTree -> fromTryTree(node, parent) + is ForLoopTree -> fromForTree(parent, node) + is WhileLoopTree -> fromWhileTree(parent, node) + is DoWhileLoopTree -> fromDoWhileTree(parent, node) + else -> null + } + + if (name != null) { + emptyBlocks[path!!] = name + } + } + return super.visitBlock(node, p) + } + + private fun fromDoWhileTree( + parent: DoWhileLoopTree, + node: BlockTree?, + ) = if (parent.statement == node) { + "do" + } else { + null + } + + private fun fromWhileTree( + parent: WhileLoopTree, + node: BlockTree?, + ) = if (parent.statement == node) { + "while" + } else { + null + } + + private fun fromForTree( + parent: ForLoopTree, + node: BlockTree?, + ) = if (parent.statement == node) { + "for" + } else { + null + } + + private fun fromTryTree( + node: BlockTree, + parent: TryTree, + ) = when (node) { + parent.block -> { + "try" + } + + parent.finallyBlock -> { + "finally" + } + + else -> { + val catch = + if (parent.catches.find { it.block == node } != null) { + "catch" + } else { + null + } + catch + } + } + + private fun fromIfTree( + node: BlockTree, + parent: IfTree, + ) = when (node) { + parent.thenStatement -> "if" + parent.elseStatement -> "else" + else -> null + } + + private fun addThrown(type: TypeMirror) { + abortIfCancelled() + if (type is DeclaredType) { + val el = type.asElement() as TypeElement + val name = el.qualifiedName.toString() + observedExceptions.add(name) + } + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java similarity index 55% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java index 434f5bbec1..5c370986ba 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java @@ -1,73 +1,74 @@ -/* - * 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 openjdk.source.tree.ClassTree; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.NewClassTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -/** - * @author Akash Yadav - */ -public class FindAnonymousTypeDeclaration extends TreePathScanner { - - private final SourcePositions pos; - private final CompilationUnitTree root; - private TreePath stored; - - public FindAnonymousTypeDeclaration(JavacTask task, CompilationUnitTree root) { - this.pos = Trees.instance(task).getSourcePositions(); - this.root = root; - } - - @Override - public ClassTree reduce(ClassTree a, ClassTree b) { - if (a != null) return a; - return b; - } - - @Override - public ClassTree visitNewClass(NewClassTree t, Long find) { - - if (pos == null) { - return null; - } - - ClassTree smaller = super.visitNewClass(t, find); - if (smaller != null) { - return smaller; - } - - if (pos.getStartPosition(root, t.getClassBody()) <= find - && find < pos.getEndPosition(root, t.getClassBody())) { - stored = getCurrentPath(); - return t.getClassBody(); - } - - return null; - } - - public TreePath getStoredPath() { - return stored; - } -} +/* + * 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 openjdk.source.tree.ClassTree; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.NewClassTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +/** + * @author Akash Yadav + */ +public class FindAnonymousTypeDeclaration extends TreePathScanner { + + private final SourcePositions pos; + private final CompilationUnitTree root; + private TreePath stored; + + public FindAnonymousTypeDeclaration(JavacTask task, CompilationUnitTree root) { + this.pos = Trees.instance(task).getSourcePositions(); + this.root = root; + } + + public TreePath getStoredPath() { + return stored; + } + + @Override + public ClassTree reduce(ClassTree a, ClassTree b) { + if (a != null) + return a; + return b; + } + + @Override + public ClassTree visitNewClass(NewClassTree t, Long find) { + + if (pos == null) { + return null; + } + + ClassTree smaller = super.visitNewClass(t, find); + if (smaller != null) { + return smaller; + } + + if (pos.getStartPosition(root, t.getClassBody()) <= find + && find < pos.getEndPosition(root, t.getClassBody())) { + stored = getCurrentPath(); + return t.getClassBody(); + } + + return null; + } +} diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java new file mode 100644 index 0000000000..10af46455a --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java @@ -0,0 +1,154 @@ +/* + * 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.itsaky.androidide.models.Position; +import com.itsaky.androidide.models.Range; +import openjdk.source.tree.ClassTree; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.LineMap; +import openjdk.source.tree.MethodTree; +import openjdk.source.tree.PackageTree; +import openjdk.source.tree.Tree; +import openjdk.source.tree.TryTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +/** + * @author Akash Yadav + */ +public class FindBiggerRange extends TreePathScanner { + + private final SourcePositions positions; + private final CompilationUnitTree root; + private final LineMap lineMap; + private final Range rootRange; + + public FindBiggerRange(JavacTask task, @NonNull CompilationUnitTree root) { + this.positions = Trees.instance(task).getSourcePositions(); + this.root = root; + this.lineMap = root.getLineMap(); + + this.rootRange = getRange(root); + } + + @Override + public Range reduce(Range r1, Range r2) { + return r1 == null ? r2 : r1; + } + + @Override + public Range scan(Tree tree, Range range) { + if (range.equals(rootRange)) { + // if whole file content selected, no need to scan the tree + return null; + } + + final Range smallerThanThis = super.scan(tree, range); + if (smallerThanThis != null) { + return smallerThanThis; + } + + final Range treeRange = getRange(tree); + if (treeRange != null && range.isSmallerThan(treeRange)) { + return treeRange; + } + + return null; + } + + @Override + public Range visitClass(ClassTree node, Range range) { + final var classRange = getRange(node); + if (range.equals(classRange)) { + final var parentPath = getCurrentPath().getParentPath(); + if (parentPath != null && parentPath.getLeaf() instanceof CompilationUnitTree) { + return rootRange; + } + } + return super.visitClass(node, range); + } + + @Override + public Range visitMethod(MethodTree node, Range range) { + + // If this methods body is selected, then select the entire method + final Range methodRange = getRange(node); + final Range blockRange = getRange(node.getBody()); + if (range.equals(blockRange) && methodRange != null) { + return methodRange; + } + + return super.visitMethod(node, range); + } + + @Override + public Range visitPackage(PackageTree node, Range range) { + final var packageRange = getRange(node); + if (range.equals(packageRange)) { + final var parentPath = getCurrentPath().getParentPath(); + if (parentPath != null && parentPath.getLeaf() instanceof CompilationUnitTree) { + return rootRange; + } + } + return super.visitPackage(node, range); + } + + @Override + public Range visitTry(TryTree node, Range range) { + + // If this try's body or finally block is selected, then select the entire try + final Range methodRange = getRange(node); + final Range blockRange = getRange(node.getBlock()); + final Range finallyRange = getRange(node.getFinallyBlock()); + if ((range.equals(blockRange) || range.equals(finallyRange)) && methodRange != null) { + return methodRange; + } + + return super.visitTry(node, range); + } + + @Nullable + private Range getRange(Tree leaf) { + final Range range = new Range(); + final Position start = new Position(0, 0); + final Position end = new Position(0, 0); + + final long startPos = positions.getStartPosition(root, leaf); + final long endPos = positions.getEndPosition(root, leaf); + + if (startPos == -1 || endPos == -1) { + return null; + } + + start.setLine((int) lineMap.getLineNumber(startPos) - 1); + start.setColumn((int) lineMap.getColumnNumber(startPos) - 1); + + end.setLine((int) lineMap.getLineNumber(endPos) - 1); + end.setColumn((int) lineMap.getColumnNumber(endPos) - 1); + + range.setStart(start); + range.setEnd(end); + + return range; + } +} diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java new file mode 100644 index 0000000000..d4691d4d20 --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java @@ -0,0 +1,141 @@ +/* + * 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 openjdk.source.tree.CaseTree; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.ErroneousTree; +import openjdk.source.tree.IdentifierTree; +import openjdk.source.tree.ImportTree; +import openjdk.source.tree.MemberReferenceTree; +import openjdk.source.tree.MemberSelectTree; +import openjdk.source.tree.Tree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +public class FindCompletionsAt extends TreePathScanner { + + // private static final ILogger LOG = ILogger.newInstance("FindCompletionsAt"); + private final JavacTask task; + private CompilationUnitTree root; + + public FindCompletionsAt(JavacTask task) { + this.task = task; + } + + @Override + public TreePath reduce(TreePath a, TreePath b) { + if (a != null) { + return a; + } + return b; + } + + @Override + public TreePath visitCase(CaseTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + + // check if the cursor is in the case expression + // default statements have null expression + // In case of an identifier tree, we have to check for both, variables and switch constants + // in + // CompletionProvider + if (t.getExpression() != null && !(t.getExpression() instanceof IdentifierTree)) { + long start = pos.getStartPosition(root, t.getExpression()); + long end = pos.getEndPosition(root, t.getExpression()); + if (start <= find && find <= end) { + return new TreePath(getCurrentPath(), t.getExpression()); + } + } + + long start = pos.getStartPosition(root, t) + "case".length(); + long end = pos.getEndPosition(root, t.getExpression()); + if (start <= find && find <= end) { + return getCurrentPath().getParentPath(); + } + + return super.visitCase(t, find); + } + + @Override + public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return reduce(super.visitCompilationUnit(t, find), getCurrentPath()); + } + + @Override + public TreePath visitErroneous(ErroneousTree t, Long find) { + if (t.getErrorTrees() == null) { + return null; + } + for (Tree e : t.getErrorTrees()) { + TreePath found = scan(e, find); + if (found != null) { + return found; + } + } + return null; + } + + @Override + public TreePath visitIdentifier(IdentifierTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getStartPosition(root, t); + long end = pos.getEndPosition(root, t); + if (start <= find && find <= end) { + return getCurrentPath(); + } + return super.visitIdentifier(t, find); + } + + @Override + public TreePath visitImport(ImportTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getStartPosition(root, t.getQualifiedIdentifier()); + long end = pos.getEndPosition(root, t.getQualifiedIdentifier()); + if (start <= find && find <= end) { + return getCurrentPath(); + } + return super.visitImport(t, find); + } + + @Override + public TreePath visitMemberReference(MemberReferenceTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getEndPosition(root, t.getQualifierExpression()) + 2; + long end = pos.getEndPosition(root, t); + if (start <= find && find <= end) { + return getCurrentPath(); + } + return super.visitMemberReference(t, find); + } + + @Override + public TreePath visitMemberSelect(MemberSelectTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getEndPosition(root, t.getExpression()) + 1; + long end = pos.getEndPosition(root, t); + if (start <= find && find <= end) { + return getCurrentPath(); + } + return super.visitMemberSelect(t, find); + } +} diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java new file mode 100644 index 0000000000..2960d9587e --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java @@ -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.visitors; + +import com.itsaky.androidide.progress.ICancelChecker; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.MethodInvocationTree; +import openjdk.source.tree.NewClassTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +public class FindInvocationAt extends TreePathScanner { + + private final JavacTask task; + private final ICancelChecker cancelChecker; + private CompilationUnitTree root; + + public FindInvocationAt(JavacTask task, ICancelChecker cancelChecker) { + this.task = task; + this.cancelChecker = cancelChecker; + } + + @Override + public TreePath reduce(TreePath a, TreePath b) { + cancelChecker.abortIfCancelled(); + if (a != null) { + return a; + } + return b; + } + + @Override + public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { + cancelChecker.abortIfCancelled(); + root = t; + return reduce(super.visitCompilationUnit(t, find), getCurrentPath()); + } + + @Override + public TreePath visitMethodInvocation(MethodInvocationTree t, Long find) { + cancelChecker.abortIfCancelled(); + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getEndPosition(root, t.getMethodSelect()) + 1; + long end = pos.getEndPosition(root, t) - 1; + if (start <= find && find <= end) { + return reduce(super.visitMethodInvocation(t, find), getCurrentPath()); + } + return super.visitMethodInvocation(t, find); + } + + @Override + public TreePath visitNewClass(NewClassTree t, Long find) { + cancelChecker.abortIfCancelled(); + SourcePositions pos = Trees.instance(task).getSourcePositions(); + long start = pos.getEndPosition(root, t.getIdentifier()) + 1; + long end = pos.getEndPosition(root, t) - 1; + if (start <= find && find <= end) { + return reduce(super.visitNewClass(t, find), getCurrentPath()); + } + return super.visitNewClass(t, find); + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt similarity index 52% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt index 7314cddc2f..f6c61e7d33 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt @@ -29,36 +29,43 @@ import openjdk.source.util.Trees * * @author Akash Yadav */ -class FindMethodAt(val task: JavacTask) : TreePathScanner() { +class FindMethodAt( + val task: JavacTask, +) : TreePathScanner() { + private val sourcePositions = Trees.instance(task).sourcePositions + private var root: CompilationUnitTree? = null - private val sourcePositions = Trees.instance(task).sourcePositions - private var root: CompilationUnitTree? = null + override fun visitCompilationUnit( + node: CompilationUnitTree?, + p: Long, + ): TreePath? { + this.root = node + return super.visitCompilationUnit(node, p) + } - override fun visitCompilationUnit(node: CompilationUnitTree?, p: Long): TreePath? { - this.root = node - return super.visitCompilationUnit(node, p) - } + override fun visitMethod( + node: MethodTree?, + p: Long, + ): TreePath? { + val smaller = super.visitMethod(node, p) + if (smaller != null || node == null) { + return smaller + } - override fun visitMethod(node: MethodTree?, p: Long): TreePath? { - val smaller = super.visitMethod(node, p) - if (smaller != null || node == null) { - return smaller - } + if (node.body != null) { + val bodyStart = sourcePositions.getStartPosition(root, node.body) + val bodyEnd = sourcePositions.getEndPosition(root, node.body) + if (p in bodyStart..bodyEnd) { + return currentPath + } + } - if (node.body != null) { - val bodyStart = sourcePositions.getStartPosition(root, node.body) - val bodyEnd = sourcePositions.getEndPosition(root, node.body) - if (p in bodyStart..bodyEnd) { - return currentPath - } - } + val start = sourcePositions.getStartPosition(root, node) + val end = sourcePositions.getEndPosition(root, node) + if (p in start..end) { + return currentPath + } - val start = sourcePositions.getStartPosition(root, node) - val end = sourcePositions.getEndPosition(root, node) - if (p in start..end) { - return currentPath - } - - return null - } + return null + } } diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java new file mode 100644 index 0000000000..cb10572d8c --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java @@ -0,0 +1,256 @@ +/* + * 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 jdkx.lang.model.element.Element; +import jdkx.lang.model.element.NestingKind; +import jdkx.lang.model.element.TypeElement; +import jdkx.lang.model.type.TypeKind; +import jdkx.lang.model.type.TypeMirror; +import openjdk.source.tree.AssignmentTree; +import openjdk.source.tree.ClassTree; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.MemberSelectTree; +import openjdk.source.tree.MethodInvocationTree; +import openjdk.source.tree.Tree; +import openjdk.source.tree.VariableTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +public class FindMethodCallAt extends TreePathScanner { + + private final Trees trees; + private final SourcePositions pos; + private CompilationUnitTree root; + private boolean isMemberSelect; + private boolean isStatic; + private String returnType; + private ClassTree enclosingTree; + private TreePath enclosingTreePath; + private TypeElement declaredInTopLevel; + private TypeElement enclosingElement; + + public FindMethodCallAt(JavacTask task) { + this.trees = Trees.instance(task); + this.pos = trees.getSourcePositions(); + } + + public TypeElement getDeclaringType() { + return declaredInTopLevel; + } + + public ClassTree getEnclosingClass() { + return enclosingTree; + } + + public TypeElement getEnclosingElement() { + return enclosingElement; + } + + public TreePath getEnclosingTreePath() { + return enclosingTreePath; + } + + public String getReturnType() { + return returnType; + } + + public boolean isMemberSelect() { + return isMemberSelect; + } + + public boolean isStaticAccess() { + return isStatic; + } + + @Override + public MethodInvocationTree reduce(MethodInvocationTree r1, MethodInvocationTree r2) { + if (r1 != null) { + return r1; + } + return r2; + } + + @Override + public MethodInvocationTree scan(Tree tree, Integer find) { + final var result = super.scan(tree, find); + if (result != null && result.getMethodSelect() instanceof MemberSelectTree) { + initProperties(((MemberSelectTree) result.getMethodSelect()), find); + } + return result; + } + + /** + * Find method invocation while initializing a variable E.g. + * + *
+	 * 	int x;
+	 * 	...
+	 * 	x = foo();
+	 * 
+ */ + @Override + public MethodInvocationTree visitAssignment(AssignmentTree tree, Integer find) { + if (tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if (start <= find && find <= end && tree.getExpression() instanceof MethodInvocationTree) { + returnType = findType(); + return visitMethodInvocation((MethodInvocationTree) tree.getExpression(), find); + } + } + return super.visitAssignment(tree, find); + } + + @Override + public MethodInvocationTree visitCompilationUnit(CompilationUnitTree t, Integer find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + /** + * A method invocation + * + *
+	 * foo();
+	 * field.foo();
+	 * Class.foo();
+	 * Class.field.foo();
+	 * 
+ */ + @Override + public MethodInvocationTree visitMethodInvocation(MethodInvocationTree t, Integer find) { + MethodInvocationTree smaller = super.visitMethodInvocation(t, find); + if (smaller != null) { + return smaller; + } + + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + return t; + } + + return null; + } + + /** + * Find method invocation while declaring a variable E.g. + * + *
+	 * int x = foo();
+	 * 
+ */ + @Override + public MethodInvocationTree visitVariable(VariableTree tree, Integer find) { + if (tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if (start <= find && find <= end && tree.getInitializer() instanceof MethodInvocationTree) { + returnType = tree.getType().toString(); + return visitMethodInvocation((MethodInvocationTree) tree.getInitializer(), find); + } + } + return super.visitVariable(tree, find); + } + + private ClassTree enclosingClass(TreePath path) { + while (path != null) { + if (path.getLeaf() instanceof ClassTree) { + return (ClassTree) path.getLeaf(); + } + + path = path.getParentPath(); + } + return null; + } + + private TypeElement enclosingTopLevelType(Element element) { + while (element != null) { + if (element instanceof TypeElement) { + TypeElement type = (TypeElement) element; + if (type.getNestingKind() == NestingKind.TOP_LEVEL) { + return type; + } + } + + element = element.getEnclosingElement(); + } + return null; + } + + private TypeElement enclosingType(Element element) { + while (element != null) { + if (element instanceof TypeElement) { + TypeElement type = (TypeElement) element; + final ClassTree tree = trees.getTree(type); + if (tree != null) { + return type; + } + } + + element = element.getEnclosingElement(); + } + return null; + } + + private String findType() { + if (this.trees != null && getCurrentPath() != null) { + TypeMirror typeMirror = this.trees.getTypeMirror(getCurrentPath()); + if (typeMirror != null + && typeMirror.getKind() != TypeKind.NONE + && typeMirror.getKind() != TypeKind.ERROR) { + return typeMirror.toString(); + } + } + return null; + } + + private void initProperties(MemberSelectTree tree, Integer find) { + if (tree != null) { + long start = pos.getStartPosition(root, tree); + long end = pos.getEndPosition(root, tree); + if (start <= find && find <= end) { + Element element = trees.getElement(trees.getPath(root, tree)); + + // Is this a static access? + this.isStatic = element instanceof TypeElement; + + // Find enclosing element to get the TypeElement from which this method is being + // called + this.enclosingElement = enclosingType(element); + + // find top level declaration to get the qualified name of the class + this.declaredInTopLevel = enclosingTopLevelType(enclosingElement); + + // Get the tree path of the enclosing element + this.enclosingTreePath = trees.getPath(enclosingElement); + + // Get the ClassTree of the enclosing element + // Will be needed in CreateMissingMethod.java for EditHelper + this.enclosingTree = enclosingClass(this.enclosingTreePath); + + this.isMemberSelect = enclosingElement != null + && declaredInTopLevel != null + && enclosingTreePath != null + && enclosingTree != null; + } + } + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java similarity index 58% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java index 28243dc7f1..a7364729ed 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java @@ -26,34 +26,35 @@ public class FindMethodDeclarationAt extends TreeScanner { - private final SourcePositions pos; - private CompilationUnitTree root; - - public FindMethodDeclarationAt(JavacTask task) { - pos = Trees.instance(task).getSourcePositions(); - } - - @Override - public MethodTree reduce(MethodTree r1, MethodTree r2) { - if (r1 != null) return r1; - return r2; - } - - @Override - public MethodTree visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - return super.visitCompilationUnit(t, find); - } - - @Override - public MethodTree visitMethod(MethodTree t, Long find) { - MethodTree smaller = super.visitMethod(t, find); - if (smaller != null) { - return smaller; - } - if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { - return t; - } - return null; - } + private final SourcePositions pos; + private CompilationUnitTree root; + + public FindMethodDeclarationAt(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } + + @Override + public MethodTree reduce(MethodTree r1, MethodTree r2) { + if (r1 != null) + return r1; + return r2; + } + + @Override + public MethodTree visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + @Override + public MethodTree visitMethod(MethodTree t, Long find) { + MethodTree smaller = super.visitMethod(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + return t; + } + return null; + } } diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java new file mode 100644 index 0000000000..17dd1cb61a --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java @@ -0,0 +1,140 @@ +/* + * 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.lsp.java.compiler.CompileTask; +import com.itsaky.androidide.lsp.java.utils.FindHelper; +import jdkx.lang.model.element.Name; +import openjdk.source.tree.ClassTree; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.IdentifierTree; +import openjdk.source.tree.MemberReferenceTree; +import openjdk.source.tree.MemberSelectTree; +import openjdk.source.tree.MethodTree; +import openjdk.source.tree.NewClassTree; +import openjdk.source.tree.Tree; +import openjdk.source.tree.VariableTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +public class FindNameAt extends TreePathScanner { + + private final JavacTask task; + private CompilationUnitTree root; + private ClassTree surroundingClass; + + public FindNameAt(CompileTask task) { + this.task = task.task; + } + + @Override + public TreePath reduce(TreePath r1, TreePath r2) { + if (r1 != null) + return r1; + return r2; + } + + @Override + public TreePath visitClass(ClassTree t, Long find) { + ClassTree push = surroundingClass; + surroundingClass = t; + if (contains(t, t.getSimpleName(), find)) { + surroundingClass = push; + return getCurrentPath(); + } + TreePath result = super.visitClass(t, find); + surroundingClass = push; + return result; + } + + @Override + public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return super.visitCompilationUnit(t, find); + } + + @Override + public TreePath visitIdentifier(IdentifierTree t, Long find) { + if (contains(t, t.getName(), find)) { + return getCurrentPath(); + } + return super.visitIdentifier(t, find); + } + + @Override + public TreePath visitMemberReference(MemberReferenceTree t, Long find) { + if (contains(t, t.getName(), find)) { + return getCurrentPath(); + } + return super.visitMemberReference(t, find); + } + + @Override + public TreePath visitMemberSelect(MemberSelectTree t, Long find) { + if (contains(t, t.getIdentifier(), find)) { + return getCurrentPath(); + } + return super.visitMemberSelect(t, find); + } + + @Override + public TreePath visitMethod(MethodTree t, Long find) { + Name name = t.getName(); + if (name.contentEquals("")) { + name = surroundingClass.getSimpleName(); + } + if (contains(t, name, find)) { + return getCurrentPath(); + } + return super.visitMethod(t, find); + } + + @Override + public TreePath visitNewClass(NewClassTree t, Long find) { + long start = Trees.instance(task).getSourcePositions().getStartPosition(root, t); + long end = start + "new".length(); + if (start <= find && find < end) { + return getCurrentPath(); + } + return super.visitNewClass(t, find); + } + + @Override + public TreePath visitVariable(VariableTree t, Long find) { + if (contains(t, t.getName(), find)) { + return getCurrentPath(); + } + return super.visitVariable(t, find); + } + + private boolean contains(Tree t, CharSequence name, long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + int start = (int) pos.getStartPosition(root, t); + int end = (int) pos.getEndPosition(root, t); + if (start == -1 || end == -1) + return false; + start = FindHelper.findNameIn(root, name, start, end); + end = start + name.length(); + if (start == -1 || end == -1) + return false; + return start <= find && find < end; + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java similarity index 53% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java index 1c843344c6..28996a36b1 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java @@ -30,48 +30,48 @@ public class FindReferences extends TreePathScanner> { - final JavacTask task; - final Element find; + final JavacTask task; + final Element find; - public FindReferences(JavacTask task, Element find) { - this.task = task; - this.find = find; - } + public FindReferences(JavacTask task, Element find) { + this.task = task; + this.find = find; + } - @Override - public Void visitNewClass(NewClassTree t, List list) { - if (check()) { - list.add(getCurrentPath()); - } - return super.visitNewClass(t, list); - } + @Override + public Void visitIdentifier(IdentifierTree t, List list) { + if (check()) { + list.add(getCurrentPath()); + } + return super.visitIdentifier(t, list); + } - @Override - public Void visitMemberSelect(MemberSelectTree t, List list) { - if (check()) { - list.add(getCurrentPath()); - } - return super.visitMemberSelect(t, list); - } + @Override + public Void visitMemberReference(MemberReferenceTree t, List list) { + if (check()) { + list.add(getCurrentPath()); + } + return super.visitMemberReference(t, list); + } - @Override - public Void visitMemberReference(MemberReferenceTree t, List list) { - if (check()) { - list.add(getCurrentPath()); - } - return super.visitMemberReference(t, list); - } + @Override + public Void visitMemberSelect(MemberSelectTree t, List list) { + if (check()) { + list.add(getCurrentPath()); + } + return super.visitMemberSelect(t, list); + } - @Override - public Void visitIdentifier(IdentifierTree t, List list) { - if (check()) { - list.add(getCurrentPath()); - } - return super.visitIdentifier(t, list); - } + @Override + public Void visitNewClass(NewClassTree t, List list) { + if (check()) { + list.add(getCurrentPath()); + } + return super.visitNewClass(t, list); + } - private boolean check() { - Element candidate = Trees.instance(task).getElement(getCurrentPath()); - return find.equals(candidate); - } + private boolean check() { + Element candidate = Trees.instance(task).getElement(getCurrentPath()); + return find.equals(candidate); + } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java similarity index 56% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java index 7f45f1b124..9dd251ec82 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java @@ -26,41 +26,42 @@ import openjdk.source.util.Trees; public class FindTypeDeclarationAt extends TreePathScanner { - private final SourcePositions pos; - private CompilationUnitTree root; + private final SourcePositions pos; + private CompilationUnitTree root; - private TreePath path; + private TreePath path; - public FindTypeDeclarationAt(JavacTask task) { - pos = Trees.instance(task).getSourcePositions(); - } + public FindTypeDeclarationAt(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } - @Override - public ClassTree reduce(ClassTree a, ClassTree b) { - if (a != null) return a; - return b; - } + public TreePath getPath() { + return path; + } - @Override - public ClassTree visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - return super.visitCompilationUnit(t, find); - } + @Override + public ClassTree reduce(ClassTree a, ClassTree b) { + if (a != null) + return a; + return b; + } - @Override - public ClassTree visitClass(ClassTree t, Long find) { - ClassTree smaller = super.visitClass(t, find); - if (smaller != null) { - return smaller; - } - if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { - this.path = getCurrentPath(); - return t; - } - return null; - } + @Override + public ClassTree visitClass(ClassTree t, Long find) { + ClassTree smaller = super.visitClass(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + this.path = getCurrentPath(); + return t; + } + return null; + } - public TreePath getPath() { - return path; - } + @Override + public ClassTree visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + return super.visitCompilationUnit(t, find); + } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java similarity index 58% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java index 00df0274fc..d355aacb7a 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java @@ -25,29 +25,30 @@ import openjdk.source.util.TreeScanner; public class FindTypeDeclarationNamed extends TreeScanner { - private List qualifiedName = new ArrayList<>(); + private List qualifiedName = new ArrayList<>(); - @Override - public ClassTree reduce(ClassTree a, ClassTree b) { - if (a != null) return a; - return b; - } + @Override + public ClassTree reduce(ClassTree a, ClassTree b) { + if (a != null) + return a; + return b; + } - @Override - public ClassTree visitCompilationUnit(CompilationUnitTree t, String find) { - String name = Objects.toString(t.getPackageName(), ""); - qualifiedName.add(name); - return super.visitCompilationUnit(t, find); - } + @Override + public ClassTree visitClass(ClassTree t, String find) { + qualifiedName.add(t.getSimpleName()); + if (String.join(".", qualifiedName).equals(find)) { + return t; + } + ClassTree recurse = super.visitClass(t, find); + qualifiedName.remove(qualifiedName.size() - 1); + return recurse; + } - @Override - public ClassTree visitClass(ClassTree t, String find) { - qualifiedName.add(t.getSimpleName()); - if (String.join(".", qualifiedName).equals(find)) { - return t; - } - ClassTree recurse = super.visitClass(t, find); - qualifiedName.remove(qualifiedName.size() - 1); - return recurse; - } + @Override + public ClassTree visitCompilationUnit(CompilationUnitTree t, String find) { + String name = Objects.toString(t.getPackageName(), ""); + qualifiedName.add(name); + return super.visitCompilationUnit(t, find); + } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java similarity index 63% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java index 0838d33ee4..071e844745 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java @@ -25,21 +25,21 @@ import openjdk.source.util.TreeScanner; public class FindTypeDeclarations extends TreeScanner> { - private List qualifiedName = new ArrayList<>(); + private List qualifiedName = new ArrayList<>(); - @Override - public Void visitCompilationUnit(CompilationUnitTree root, List found) { - String name = Objects.toString(root.getPackageName(), ""); - qualifiedName.add(name); - return super.visitCompilationUnit(root, found); - } + @Override + public Void visitClass(ClassTree type, List found) { + qualifiedName.add(type.getSimpleName()); + found.add(String.join(".", qualifiedName)); + super.visitClass(type, found); + qualifiedName.remove(qualifiedName.size() - 1); + return null; + } - @Override - public Void visitClass(ClassTree type, List found) { - qualifiedName.add(type.getSimpleName()); - found.add(String.join(".", qualifiedName)); - super.visitClass(type, found); - qualifiedName.remove(qualifiedName.size() - 1); - return null; - } + @Override + public Void visitCompilationUnit(CompilationUnitTree root, List found) { + String name = Objects.toString(root.getPackageName(), ""); + qualifiedName.add(name); + return super.visitCompilationUnit(root, found); + } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java similarity index 57% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java index 057c6be63f..20a7de1bb1 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java @@ -25,34 +25,35 @@ import openjdk.source.util.Trees; public class FindVariableAtCursor extends TreeScanner { - private final SourcePositions pos; - private CompilationUnitTree root; + private final SourcePositions pos; + private CompilationUnitTree root; - public FindVariableAtCursor(JavacTask task) { - pos = Trees.instance(task).getSourcePositions(); - } + public FindVariableAtCursor(JavacTask task) { + pos = Trees.instance(task).getSourcePositions(); + } - @Override - public VariableTree reduce(VariableTree r1, VariableTree r2) { - if (r1 != null) return r1; - return r2; - } + @Override + public VariableTree reduce(VariableTree r1, VariableTree r2) { + if (r1 != null) + return r1; + return r2; + } - @Override - public VariableTree visitCompilationUnit(CompilationUnitTree t, Integer find) { - root = t; - return super.visitCompilationUnit(t, find); - } + @Override + public VariableTree visitCompilationUnit(CompilationUnitTree t, Integer find) { + root = t; + return super.visitCompilationUnit(t, find); + } - @Override - public VariableTree visitVariable(VariableTree t, Integer find) { - VariableTree smaller = super.visitVariable(t, find); - if (smaller != null) { - return smaller; - } - if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { - return t; - } - return null; - } + @Override + public VariableTree visitVariable(VariableTree t, Integer find) { + VariableTree smaller = super.visitVariable(t, find); + if (smaller != null) { + return smaller; + } + if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { + return t; + } + return null; + } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java similarity index 52% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java index 5098e40359..16b7665aed 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java @@ -1,76 +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.visitors; - -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.VariableTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -/** - * Finds variables between the given start and end indexes. - * - * @author Akash Yadav - */ -public class FindVariablesBetween extends TreePathScanner { - - private final long start; - private final long end; - private final SourcePositions positions; - private final List paths = new ArrayList<>(); - private CompilationUnitTree root; - - public FindVariablesBetween(@NonNull JavacTask task, long start, long end) { - Trees trees = Trees.instance(task); - this.positions = trees.getSourcePositions(); - this.start = start; - this.end = end; - } - - @Override - public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { - this.root = node; - return super.visitCompilationUnit(node, unused); - } - - @Override - public Void visitVariable(VariableTree node, Void unused) { - if (isInRange(node)) { - this.paths.add(getCurrentPath()); - } - - return super.visitVariable(node, unused); - } - - private boolean isInRange(VariableTree node) { - final long start = this.positions.getStartPosition(root, node); - final long end = this.positions.getEndPosition(root, node); - return (this.start <= start && end <= this.end) || (start <= this.start && end >= this.end); - } - - @NonNull - public List getPaths() { - return paths; - } -} +/* + * 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 androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.List; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.VariableTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreePath; +import openjdk.source.util.TreePathScanner; +import openjdk.source.util.Trees; + +/** + * Finds variables between the given start and end indexes. + * + * @author Akash Yadav + */ +public class FindVariablesBetween extends TreePathScanner { + + private final long start; + private final long end; + private final SourcePositions positions; + private final List paths = new ArrayList<>(); + private CompilationUnitTree root; + + public FindVariablesBetween(@NonNull JavacTask task, long start, long end) { + Trees trees = Trees.instance(task); + this.positions = trees.getSourcePositions(); + this.start = start; + this.end = end; + } + + @NonNull + public List getPaths() { + return paths; + } + + @Override + public Void visitCompilationUnit(CompilationUnitTree node, Void unused) { + this.root = node; + return super.visitCompilationUnit(node, unused); + } + + @Override + public Void visitVariable(VariableTree node, Void unused) { + if (isInRange(node)) { + this.paths.add(getCurrentPath()); + } + + return super.visitVariable(node, unused); + } + + private boolean isInRange(VariableTree node) { + final long start = this.positions.getStartPosition(root, node); + final long end = this.positions.getEndPosition(root, node); + return (this.start <= start && end <= this.end) || (start <= this.start && end >= this.end); + } +} diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt new file mode 100644 index 0000000000..ce5111c556 --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt @@ -0,0 +1,99 @@ +/* + * 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 androidx.core.util.Pair +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import openjdk.source.tree.CompilationUnitTree +import openjdk.source.tree.LineMap +import openjdk.source.tree.MethodTree +import openjdk.source.util.TreePath +import openjdk.source.util.TreePathScanner +import openjdk.source.util.Trees +import openjdk.tools.javac.api.JavacTaskImpl +import org.slf4j.LoggerFactory + +/** + * Visits all methods and adds them to the given list of pair of method range and its tree. + * + * @author Akash Yadav + */ +class MethodRangeScanner( + val task: JavacTaskImpl, +) : TreePathScanner>>() { + var root: CompilationUnitTree? = null + var lines: LineMap? = null + val pos = Trees.instance(task).sourcePositions + + companion object { + private val log = LoggerFactory.getLogger(MethodRangeScanner::class.java) + } + + override fun visitCompilationUnit( + node: CompilationUnitTree?, + p: MutableList>?, + ) { + this.root = node + this.lines = node?.lineMap + return super.visitCompilationUnit(node, p) + } + + override fun visitMethod( + node: MethodTree?, + list: MutableList>, + ) { + // Do not call super.visitMethod + // We only want methods defined directly in declared (not anonymous) classes. + if (node == null || this.root == null) { + return + } + + val start = getStartPosition(node) + val end = getEndPosition(node) + + if (start == null || end == null) { + log.warn("Method '{}' skipped. Invalid position.", node.name) + return + } + + list.add(Pair.create(Range(start, end), currentPath)) + } + + fun getStartPosition(node: MethodTree): Position? { + val position = this.pos.getStartPosition(this.root!!, node) + if (position.toInt() == -1) { + return null + } + return getPosition(position) + } + + fun getEndPosition(node: MethodTree): Position? { + val position = this.pos.getEndPosition(this.root!!, node) + if (position.toInt() == -1) { + return null + } + return getPosition(position) + } + + fun getPosition(position: Long): Position { + val line = lines!!.getLineNumber(position).toInt() + val column = lines!!.getColumnNumber(position).toInt() + return Position(line, column).apply { index = position.toInt() } + } +} diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java new file mode 100644 index 0000000000..8893f14114 --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java @@ -0,0 +1,150 @@ +/* + * 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 static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; +import static com.itsaky.androidide.lsp.java.utils.JavaParserUtils.getSimpleName; + +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.Name; +import com.github.javaparser.ast.expr.SimpleName; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.printer.DefaultPrettyPrinterVisitor; +import com.github.javaparser.printer.SourcePrinter; +import com.github.javaparser.printer.configuration.ConfigurationOption; +import com.github.javaparser.printer.configuration.DefaultConfigurationOption; +import com.github.javaparser.printer.configuration.DefaultPrinterConfiguration; +import com.github.javaparser.printer.configuration.PrinterConfiguration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class PrettyPrintingVisitor extends DefaultPrettyPrinterVisitor { + + public PrettyPrintingVisitor(PrinterConfiguration configuration) { + super(configuration); + } + + public PrettyPrintingVisitor(PrinterConfiguration configuration, SourcePrinter printer) { + super(configuration, printer); + } + + @Override + public void visit(ClassOrInterfaceType n, Void arg) { + printOrphanCommentsBeforeThisChildNode(n); + printComment(n.getComment(), arg); + + printAnnotations(n.getAnnotations(), false, arg); + + n.getName().accept(this, arg); + + if (n.isUsingDiamondOperator()) { + printer.print("<>"); + } else { + printTypeArgs(n, arg); + } + } + + @Override + public void visit(Name n, Void arg) { + printOrphanCommentsBeforeThisChildNode(n); + printComment(n.getComment(), arg); + printer.print(n.getIdentifier()); + printOrphanCommentsEnding(n); + } + + @Override + public void visit(SimpleName n, Void arg) { + printOrphanCommentsBeforeThisChildNode(n); + printComment(n.getComment(), arg); + + String identifier = n.getIdentifier(); + printer.print(getSimpleName(identifier)); + } + + protected void printOrphanCommentsBeforeThisChildNode(final Node node) { + if (!getOption(DefaultPrinterConfiguration.ConfigOption.PRINT_COMMENTS).isPresent()) + return; + if (node instanceof Comment) + return; + + Node parent = node.getParentNode().orElse(null); + if (parent == null) + return; + List everything = new ArrayList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); ++i) { // indexOf is by equality, so this + // is used to index by identity + if (everything.get(i) == node) { + positionOfTheChild = i; + break; + } + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) + positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + nodeToPrint.accept(this, null); + } + } + + protected void printOrphanCommentsEnding(final Node node) { + if (!getOption(DefaultPrinterConfiguration.ConfigOption.PRINT_COMMENTS).isPresent()) + return; + + List everything = new ArrayList<>(node.getChildNodes()); + sortByBeginPosition(everything); + if (everything.isEmpty()) { + return; + } + + int commentsAtEnd = 0; + boolean findingComments = true; + while (findingComments && commentsAtEnd < everything.size()) { + Node last = everything.get(everything.size() - 1 - commentsAtEnd); + findingComments = (last instanceof Comment); + if (findingComments) { + commentsAtEnd++; + } + } + for (int i = 0; i < commentsAtEnd; i++) { + everything.get(everything.size() - commentsAtEnd + i).accept(this, null); + } + } + + private Optional getOption( + DefaultPrinterConfiguration.ConfigOption cOption) { + return configuration.get(new DefaultConfigurationOption(cOption)); + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt similarity index 72% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt index 5094206c48..f6015ee5aa 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt @@ -28,21 +28,19 @@ import org.slf4j.LoggerFactory * @author Akash Yadav */ class PrintingVisitor : TreeScanner() { + companion object { + private val log = LoggerFactory.getLogger(PrintingVisitor::class.java) + } - companion object { + override fun scan(tree: JCTree?) { + log.debug(if (tree != null) tree::class.java.name else "NullClass", tree) + super.scan(tree) + } - private val log = LoggerFactory.getLogger(PrintingVisitor::class.java) - } - - override fun scan(tree: JCTree?) { - log.debug(if (tree != null) tree::class.java.name else "NullClass", tree) - super.scan(tree) - } - - override fun visitErroneous(tree: JCErroneous?) { - if (tree?.errs != null) { - tree.errs.forEach { scan(it) } - } - super.visitErroneous(tree) - } + override fun visitErroneous(tree: JCErroneous?) { + if (tree?.errs != null) { + tree.errs.forEach { scan(it) } + } + super.visitErroneous(tree) + } } diff --git a/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java new file mode 100644 index 0000000000..ab24ead76b --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java @@ -0,0 +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.java.visitors; + +import java.io.IOException; +import openjdk.source.tree.CompilationUnitTree; +import openjdk.source.tree.MethodTree; +import openjdk.source.util.JavacTask; +import openjdk.source.util.SourcePositions; +import openjdk.source.util.TreeScanner; +import openjdk.source.util.Trees; + +public class PruneMethodBodies extends TreeScanner { + private final JavacTask task; + private final StringBuilder buf = new StringBuilder(); + private CompilationUnitTree root; + + public PruneMethodBodies(JavacTask task) { + this.task = task; + } + + @Override + public StringBuilder reduce(StringBuilder a, StringBuilder b) { + return buf; + } + + @Override + public StringBuilder visitCompilationUnit(CompilationUnitTree t, Long find) { + root = t; + try { + CharSequence contents = t.getSourceFile().getCharContent(true); + buf.setLength(0); + buf.append(contents); + } catch (IOException e) { + throw new RuntimeException(e); + } + super.visitCompilationUnit(t, find); + return buf; + } + + @Override + public StringBuilder visitMethod(MethodTree t, Long find) { + SourcePositions pos = Trees.instance(task).getSourcePositions(); + if (t.getBody() == null) { + return buf; + } + long start = pos.getStartPosition(root, t.getBody()); + long end = pos.getEndPosition(root, t.getBody()); + if (!(start <= find && find < end)) { + for (int i = (int) start + 1; i < end - 1; i++) { + if (!Character.isWhitespace(buf.charAt(i))) { + buf.setCharAt(i, ' '); + } + } + return buf; + } + super.visitMethod(t, find); + return buf; + } +} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaCompilerProviderTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/JavaCompilerProviderTest.kt similarity index 100% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaCompilerProviderTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/JavaCompilerProviderTest.kt diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt similarity index 68% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt index 3fc4ff7219..4fe9c86e0c 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt @@ -27,27 +27,24 @@ import org.junit.Ignore /** @author Akash Yadav */ @Ignore("Base singleton class") object JavaLSPTest : LSPTest() { + val server by lazy { + ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) + as JavaLanguageServer + } - val server by lazy { - ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) - as JavaLanguageServer - } + @Before + fun setup() { + log.debug("Initializing project...") + initProjectIfNeeded() + } - @Before - fun setup() { - log.debug("Initializing project...") - initProjectIfNeeded() - } + override fun registerServer() { + ILanguageServerRegistry.default.register(JavaLanguageServer()) + } - override fun registerServer() { - ILanguageServerRegistry.default.register(JavaLanguageServer()) - } + override fun getServerId() = JavaLanguageServer.SERVER_ID - override fun getServerId() = JavaLanguageServer.SERVER_ID + fun getCompiler(): JavaCompilerService = JavaCompilerProvider.get(findAppModule()!!) - fun getCompiler(): JavaCompilerService { - return JavaCompilerProvider.get(findAppModule()!!) - } - - override fun test() {} + override fun test() {} } diff --git a/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt new file mode 100644 index 0000000000..26512c1cab --- /dev/null +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt @@ -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.actions + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.java.JavaLSPTest +import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction +import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.DEFAULT_VALUE_STRING) +class AddImportTest { + @Before + fun setup() { + JavaLSPTest.setup() + } + + @Suppress("UNCHECKED_CAST") + @Test + fun addImport() { + JavaLSPTest.apply { + openFile("actions/AddImportAction") + val diagnostic = + runBlocking { + // Bypass JavaLanguageServer.analyze() -- it now routes through the DexClassLoader + // carrier (ADFA-5053), which isn't available in this unit test environment. Test the + // isolated provider directly instead, same as before ADFA-5053 for the resident + // JavaLanguageServer.getCompiler()-based path. + JavaDiagnosticProvider().analyze(file!!).diagnostics.firstOrNull { + it.code == "compiler.err.cant.resolve.location" + } + } + + assertThat(diagnostic).isNotNull() + + val file = this.file!!.toFile() + val data = createActionData(diagnostic!!, file, this.file!!, this.server) + + val action = AddImportAction() + action.prepare(data) + assertThat(action.visible).isTrue() + assertThat(action.enabled).isTrue() + + val execResult = runBlocking { action.execAction(data) } + assertThat(execResult::class.java).isAssignableTo(Pair::class.java) + + val result = execResult as Pair, *> + assertThat(result.first).contains("java.util.stream.Stream") + } + } +} diff --git a/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt new file mode 100644 index 0000000000..ebe8ef6fde --- /dev/null +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt @@ -0,0 +1,111 @@ +/* + * 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.truth.Truth.assertThat +import com.itsaky.androidide.lsp.java.JavaLSPTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.time.Instant + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class CompilerTest { + @Before + fun setup() { + JavaLSPTest.setup() + } + + @Test + fun testMultipleThreads() { + JavaLSPTest.apply { + openFile("completion/MembersCompletionTest") + val threads = mutableListOf() + val thread = + Thread { + getCompiler().compile(file!!).run { + println(Thread.currentThread().name) + delay(1000) + } + } + thread.name = "Long running task" + threads.add(thread) + thread.start() + + for (i in 0..300) { + val th = + Thread { + getCompiler().compile(file!!).run { println(Thread.currentThread().name) } + } + th.name = "Thread #$i" + threads.add(th) + th.start() + } + + threads.forEach { it.join() } + } + } + + @Test + fun testClosedFileChannel() { + JavaLSPTest.apply { + openFile("completion/MembersCompletionTest") + + Thread { getCompiler().compile(file!!).run { delay(500) } }.start() + Thread { getCompiler().compile(file!!).run { delay(200) } }.start() + + getCompiler().compile(file!!).run { assertThat(it.diagnostics).isNotEmpty() } + } + } + + private fun delay(millis: Long) { + Thread.sleep(millis) + } + + @Test + fun testConcurrentAccess() { + JavaLSPTest.apply { + openFile("completion/MembersCompletionTest") + + var task = getCompiler().compile(file!!) + var fileObject = SourceFileObject(file!!) + val threads = mutableListOf() + for (i in 1..10) { + threads.add( + Thread { + task.run { + delay(100) + println(Thread.currentThread()) + } + }.apply { name = "CompileTask Acessor #$i" }, + ) + } + + fileObject = SourceFileObject(file!!, fileObject.contents, Instant.now()) + task = getCompiler().compile(listOf(fileObject)) + threads.forEach { it.start() } + + Thread { task.run { println("Writer thread") } }.start() + threads.forEach { it.join() } + } + } +} diff --git a/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt new file mode 100644 index 0000000000..fb25f999c4 --- /dev/null +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt @@ -0,0 +1,168 @@ +/* + * 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.partial + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.eventbus.events.editor.ChangeType.INSERT +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.lsp.java.JavaLSPTest +import com.itsaky.androidide.lsp.java.compiler.SourceFileObject +import com.itsaky.androidide.lsp.java.models.CompilationRequest +import com.itsaky.androidide.lsp.java.models.PartialReparseRequest +import com.itsaky.androidide.lsp.java.visitors.PrintingVisitor +import com.itsaky.androidide.models.Range +import jdkx.lang.model.type.ArrayType +import openjdk.source.tree.ExpressionStatementTree +import openjdk.source.tree.LiteralTree +import openjdk.source.tree.Tree +import openjdk.tools.javac.tree.JCTree.JCCompilationUnit +import openjdk.tools.javac.tree.JCTree.JCMethodDecl +import openjdk.tools.javac.tree.JCTree.JCMethodInvocation +import openjdk.tools.javac.tree.JCTree.JCVariableDecl +import openjdk.tools.javac.tree.TreeScanner +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.DEFAULT_VALUE_STRING) +@Ignore("Partial reparser is currently unused") +class PartialReparserImplTest { + @Before + fun setup() { + JavaLSPTest.setup() + } + + @Test + fun parseMethod() { + JavaLSPTest.apply { + openFile("partial/PartialReparserTest") + getCompiler().compile(file).run { task -> + AssertingScanner().scan(task.root() as JCCompilationUnit) + } + } + } + + @Test + fun testSimpleErrorneousStatement() { + JavaLSPTest.apply { + openFile("partial/PartialErrReparserTest") + getCompiler() + .compile( + CompilationRequest( + listOf(SourceFileObject(file)), + PartialReparseRequest(172, contents.toString()), + ), + ).run { PrintingVisitor().scan(it.root() as JCCompilationUnit) } + val changedText = contents!!.insert(192, "trim().").toString() + dispatchEvent( + DocumentChangeEvent( + file!!, + changedText, + changedText, + 2, + INSERT, + "trim().".length, + Range.NONE, + ), + ) + getCompiler() + .compile( + CompilationRequest( + listOf(SourceFileObject(file)), + PartialReparseRequest(179, contents.toString()), + ), + ) + } + } + + class AssertingScanner : TreeScanner() { + private var methodCount = 0 + + override fun visitMethodDef(tree: JCMethodDecl?) { + assertThat(tree).isNotNull() + tree!! + + if (tree.name.contentEquals("")) { + // Javac automatically adds the deafult constructor + methodCount++ + return super.visitMethodDef(tree) + } + + assertThat(methodCount).isEqualTo(1) + methodCount++ + + val params = tree.params + assertThat(params).isNotNull() + assertThat(params.size).isEqualTo(1) + + val argsParam = params[0] + assertThat(argsParam.name.toString()).isEqualTo("args") + assertThat(argsParam.type).isInstanceOf(ArrayType::class.java) + + val argType = argsParam.type as ArrayType + assertThat(argType.componentType.toString()).isEqualTo("java.lang.String") + + val body = tree.body + assertThat(body).isNotNull() + + val statements = body.statements + assertThat(statements).isNotNull() + assertThat(statements.size).isEqualTo(2) + + val println = statements[0] + assertThat(println).isNotNull() + assertThat(println.kind).isEqualTo(Tree.Kind.EXPRESSION_STATEMENT) + println as ExpressionStatementTree + + val arguments = (println.expression as JCMethodInvocation).arguments + assertThat(arguments).isNotNull() + assertThat(arguments.size).isEqualTo(1) + + val arg = arguments[0] + assertThat(arg).isNotNull() + assertThat(arg.kind).isEqualTo(Tree.Kind.STRING_LITERAL) + arg as LiteralTree + assertThat(arg.value).isEqualTo("Hello World!") + + val varDecl = statements[1] + assertThat(varDecl).isNotNull() + assertThat(varDecl.kind).isEqualTo(Tree.Kind.VARIABLE) + varDecl as JCVariableDecl + assertThat(varDecl.name.toString()).isEqualTo("klass") + + val type = varDecl.type + assertThat(type).isNotNull() + assertThat(type.tsym.qualifiedName.toString()).isEqualTo("java.lang.Class") + + val targs = type.typeArguments + assertThat(targs).isNotNull() + assertThat(targs.size).isEqualTo(1) + + val tOne = targs[0] + assertThat(tOne).isNotNull() + assertThat(tOne.tsym.qualifiedName.toString()).isEqualTo("java.lang.String") + + super.visitMethodDef(tree) + } + } +} diff --git a/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt new file mode 100644 index 0000000000..815ade5642 --- /dev/null +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt @@ -0,0 +1,99 @@ +/* + * 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.google.common.truth.Truth.assertThat +import com.itsaky.androidide.lsp.internal.model.CachedCompletion +import com.itsaky.androidide.lsp.java.JavaLSPTest +import com.itsaky.androidide.lsp.java.models.JavaServerSettings +import com.itsaky.androidide.lsp.models.CompletionParams +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.progress.ICancelChecker +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +class JavaCompletionProviderTest { + @Before + fun setup() { + JavaLSPTest.setup() + } + + @Test + fun locals() { + JavaLSPTest.apply { + openFile("completion/LocalsCompletionTest") + + val pos = cursorPosition() + val items = completionTitles(pos) + assertThat(items).containsAtLeast("aaString", "aaInt", "aaFloat", "aaDouble", "args") + } + } + + @Test + fun members() { + JavaLSPTest.apply { + // Complete members of String + openFile("completion/MembersCompletionTest") + + val pos = cursorPosition() + val items = completionTitles(pos) + assertThat(items) + .containsAtLeast("getClass", "toLowerCase", "toUpperCase", "substring", "charAt") + } + } + + @Test + fun lambdaVariableMemberAccess() { + JavaLSPTest.apply { + // Complete members of Throwable + openFile("completion/LambdaMembersCompletionTest") + + val pos = cursorPosition() + val items = completionTitles(pos) + assertThat(items) + .containsAtLeast("getMessage", "getCause", "getStackTrace", "printStackTrace") + } + } + + @Test + fun staticAccess() { + JavaLSPTest.apply { + // Complete static members of String + openFile("completion/StaticMembersCompletionTest") + + val pos = cursorPosition() + val items = completionTitles(pos) + assertThat(items) + .containsAtLeast("format", "join", "valueOf", "CASE_INSENSITIVE_ORDER", "class") + } + } + + private fun completionTitles(pos: Position): List { + // Bypass JavaLanguageServer.complete() -- it now routes through the DexClassLoader carrier + // (ADFA-5053), which isn't available in this unit test environment. Test the isolated + // provider directly instead, mirroring what JavaCompilerSessionImpl.complete() does. + val params = + CompletionParams(pos, JavaLSPTest.file!!, ICancelChecker.NOOP).apply { prefix = "" } + val provider = CompletionProvider() + provider.reset(JavaLSPTest.getCompiler(), JavaServerSettings.getInstance(), CachedCompletion.EMPTY) {} + return provider.complete(params).items.map { it.ideLabel } + } +} diff --git a/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt new file mode 100644 index 0000000000..20f1780029 --- /dev/null +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt @@ -0,0 +1,131 @@ +/* + * 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.google.common.truth.Truth.assertThat +import com.itsaky.androidide.eventbus.events.editor.ChangeType.NEW_TEXT +import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent +import com.itsaky.androidide.lsp.java.JavaLSPTest +import com.itsaky.androidide.lsp.models.ExpandSelectionParams +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import io.github.rosemoe.sora.text.Content +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +/** @author Akash Yadav */ +@RunWith(RobolectricTestRunner::class) +class JavaSelectionProviderTest { + @Before + fun setup() { + JavaLSPTest.setup() + } + + @Test + fun testSimpleSelectionExpansion() { + JavaLSPTest.apply { + openFile("selection/SimpleSelectionExpansionTest") + cursor = requireCursor() + deleteCursorText() + dispatchEvent( + DocumentChangeEvent( + file!!, + contents.toString(), + contents.toString(), + 1, + NEW_TEXT, + 0, + Range.NONE, + ), + ) + + val range = findRange() + val expanded = + runBlocking { + JavaSelectionProvider(JavaLSPTest.getCompiler()) + .expandSelection(ExpandSelectionParams(file!!, range)) + } + + assertThat(expanded).isEqualTo(Range(Position(4, 27), Position(4, 41))) + } + } + + @Test + fun testMethodSelection() { + JavaLSPTest.apply { + openFile("selection/MethodBodySelectionExpansionTest") + + val start = Position(3, 43) + val end = Position(5, 5) + val range = Range(start, end) + + val expanded = + runBlocking { + JavaSelectionProvider(JavaLSPTest.getCompiler()) + .expandSelection(ExpandSelectionParams(file!!, range)) + } + assertThat(expanded).isEqualTo(Range(Position(3, 4), end)) + } + } + + @Test + fun testTryCatchSelection() { + JavaLSPTest.apply { + openFile("selection/TrySelectionExpansionTest") + + // Test expand selection if catch block is selected + val start = Position(7, 10) + val end = Position(8, 9) + val range = Range(start, end) + + val expanded = + runBlocking { + JavaSelectionProvider(JavaLSPTest.getCompiler()) + .expandSelection(ExpandSelectionParams(file!!, range)) + } + assertThat(expanded).isEqualTo(Range(Position(4, 8), Position(10, 9))) + } + } + + @Test + fun testTryFinallySelection() { + JavaLSPTest.apply { + openFile("selection/TrySelectionExpansionTest") + + // Test expand selection if catch block is selected + val start = Position(8, 18) + val end = Position(10, 9) + val range = Range(start, end) + + val expanded = + runBlocking { + JavaSelectionProvider(JavaLSPTest.getCompiler()) + .expandSelection(ExpandSelectionParams(file!!, range)) + } + assertThat(expanded).isEqualTo(Range(Position(4, 8), Position(10, 9))) + } + } + + private fun findRange(): Range { + val pos = Content(JavaLSPTest.contents!!).indexer.getCharPosition(JavaLSPTest.cursor) + val position = Position(pos.line, pos.column, pos.index) + return Range(position, position) + } +} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt similarity index 61% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt index 33a0a5db36..7f46a89a2b 100644 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt +++ b/lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt @@ -19,6 +19,8 @@ package com.itsaky.androidide.lsp.java.utils import com.google.common.truth.Truth.assertThat import com.itsaky.androidide.lsp.java.JavaLSPTest +import com.itsaky.androidide.lsp.java.models.JavaServerSettings +import com.itsaky.androidide.lsp.java.providers.DefinitionProvider import com.itsaky.androidide.lsp.models.DefinitionParams import com.itsaky.androidide.models.Position import com.itsaky.androidide.progress.ICancelChecker @@ -35,24 +37,27 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.DEFAULT_VALUE_STRING) class FindHelperTest { + @Before + fun setup() { + JavaLSPTest.setup() + } - @Before - fun setup() { - JavaLSPTest.setup() - } + @Test + fun `test FindHelper#findNameIn behavior with on-demand import`() { + JavaLSPTest.apply { + openFile("utils/FindHelperRegexElements") - @Test - fun `test FindHelper#findNameIn behavior with on-demand import`() { - JavaLSPTest.apply { - openFile("utils/FindHelperRegexElements") - - // Find definition for 'field' class of type 'String' - val position = Position(9, 7) - val params = DefinitionParams(file!!, position, ICancelChecker.NOOP) - val definitions = runBlocking { server.findDefinition(params) } - assertThat(definitions).isNotNull() - assertThat(definitions.locations).hasSize(1) - assertThat(definitions.locations[0].range.contains(Position(6, 20))).isTrue() - } - } -} \ No newline at end of file + // Find definition for 'field' class of type 'String' + val position = Position(9, 7) + val params = DefinitionParams(file!!, position, ICancelChecker.NOOP) + val definitions = + runBlocking { + DefinitionProvider(getCompiler(), JavaServerSettings.getInstance(), params.cancelChecker) + .findDefinition(params) + } + assertThat(definitions).isNotNull() + assertThat(definitions.locations).hasSize(1) + assertThat(definitions.locations[0].range.contains(Position(6, 20))).isTrue() + } + } +} diff --git a/lsp/java/src/test/resources/robolectric.properties b/lsp/java-compiler-impl/src/test/resources/robolectric.properties similarity index 100% rename from lsp/java/src/test/resources/robolectric.properties rename to lsp/java-compiler-impl/src/test/resources/robolectric.properties diff --git a/lsp/java/build.gradle.kts b/lsp/java/build.gradle.kts index 70b545f02d..a4fb0b1e8b 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -38,10 +38,8 @@ dependencies { kapt(projects.annotationProcessors) implementation(libs.androidide.ts) - implementation(libs.androidide.ts.java) implementation(platform(libs.sora.bom)) implementation(libs.common.editor) - implementation(libs.common.javaparser) implementation(libs.androidx.annotation) implementation(libs.google.guava) implementation(libs.google.gson) @@ -54,15 +52,12 @@ dependencies { implementation(projects.editorApi) implementation(projects.resources) implementation(projects.lsp.api) + implementation(projects.lsp.javaApi) implementation(projects.lsp.jvmSymbolIndex) implementation(projects.subprojects.libjdwp) - implementation(projects.subprojects.javacServices) + implementation(projects.subprojects.javacFs) implementation(projects.idetooltips) - implementation(libs.composite.javac) - implementation(libs.composite.javapoet) - implementation(libs.composite.googleJavaFormat) - implementation(libs.androidx.core.ktx) implementation(libs.common.kotlin) diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java deleted file mode 100644 index 106d35bf83..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -/** - * 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; - private final Map mCompilers = new ConcurrentHashMap<>(); - - private JavaCompilerProvider() {} - - @NonNull - public static JavaCompilerService get(ModuleProject module) { - return JavaCompilerProvider.getInstance().forModule(module); - } - - public static JavaCompilerProvider getInstance() { - if (sInstance == null) { - sInstance = new JavaCompilerProvider(); - } - - return sInstance; - } - - /** - * 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; - } - - // 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(); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt index fc94ebbc84..a19a883d1a 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.kt @@ -16,7 +16,6 @@ */ package com.itsaky.androidide.lsp.java -import androidx.annotation.RestrictTo import com.itsaky.androidide.app.BaseApplication import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent @@ -24,30 +23,19 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentOpenEvent import com.itsaky.androidide.eventbus.events.editor.DocumentSelectedEvent import com.itsaky.androidide.javac.services.fs.CacheFSInfoSingleton import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCache -import com.itsaky.androidide.javac.services.fs.CachingJarFileSystemProvider.clearCachesForPaths import com.itsaky.androidide.lsp.api.ILanguageClient import com.itsaky.androidide.lsp.api.ILanguageServer import com.itsaky.androidide.lsp.api.IServerSettings import com.itsaky.androidide.lsp.debug.DebugClientConnectionResult import com.itsaky.androidide.lsp.debug.IDebugAdapter import com.itsaky.androidide.lsp.debug.IDebugClient -import com.itsaky.androidide.lsp.internal.model.CachedCompletion -import com.itsaky.androidide.lsp.java.actions.JavaCodeActionsMenu -import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService -import com.itsaky.androidide.lsp.java.compiler.SourceFileManager +import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession import com.itsaky.androidide.lsp.java.debug.JavaDebugAdapter import com.itsaky.androidide.lsp.java.debug.JdwpOptions +import com.itsaky.androidide.lsp.java.loader.JavaCompilerLoader 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.providers.snippet.JavaSnippetRepository import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer -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 @@ -62,12 +50,9 @@ import com.itsaky.androidide.lsp.models.ReferenceParams import com.itsaky.androidide.lsp.models.ReferenceResult import com.itsaky.androidide.lsp.models.SignatureHelp import com.itsaky.androidide.lsp.models.SignatureHelpParams -import com.itsaky.androidide.lsp.util.LSPEditorActions import com.itsaky.androidide.models.Range import com.itsaky.androidide.projects.FileManager.getActiveDocumentCount -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 com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.VMUtils @@ -83,18 +68,34 @@ import org.greenrobot.eventbus.ThreadMode import org.slf4j.LoggerFactory import java.nio.file.Files import java.nio.file.Path -import java.util.Objects +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock class JavaLanguageServer : ILanguageServer { - private val completionProvider: CompletionProvider = CompletionProvider() - private val diagnosticProvider = JavaDiagnosticProvider() + private val loader = JavaCompilerLoader(BaseApplication.baseInstance) + override var client: ILanguageClient? = null private set private var _settings: IServerSettings? = null private var selectedFile: Path? = null private val timer = AnalyzeTimer { analyzeSelected() } - private var cachedCompletion: CachedCompletion + + // Lifecycle of the isolated javac session (extracted + DexClassLoader-loaded lazily via + // `loader`), which setupWithProject() defers instead of loading eagerly (ADFA-5052, + // extended by ADFA-5053 to also gate the carrier-APK load). All reads/writes of + // pendingWorkspace and compilerLifecycle go through compilerLifecycleLock, held for the + // *entire* reset/shutdown, not just the decision to run one -- otherwise a concurrent + // request could use a session mid-teardown, or shutdown() could destroy state a reset is + // still rebuilding. + private enum class CompilerLifecycle { PENDING, RESETTING, INITIALIZED, SHUTDOWN } + + private val compilerLifecycleLock = ReentrantLock() + + // Guarded by compilerLifecycleLock. + private var pendingWorkspace: Workspace? = null + private var compilerLifecycle = CompilerLifecycle.PENDING + private var codeActionsRegistered = false val settings: IServerSettings get() { @@ -113,8 +114,6 @@ class JavaLanguageServer : ILanguageServer { } init { - cachedCompletion = CachedCompletion.EMPTY - applySettings(JavaServerSettings.getInstance()) if (!EventBus.getDefault().isRegistered(this)) { @@ -123,21 +122,41 @@ class JavaLanguageServer : ILanguageServer { val projectManager = ProjectManagerImpl.getInstance() projectManager.indexingServiceManager.register( - service = JvmLibraryIndexingService(context = BaseApplication.baseInstance) + service = JvmLibraryIndexingService(context = BaseApplication.baseInstance), ) projectManager.indexingServiceManager.register( - service = JvmGeneratedIndexingService(context = BaseApplication.baseInstance) + service = JvmGeneratedIndexingService(context = BaseApplication.baseInstance), ) + // Independent of javac -- reads its own snippet assets from lsp/java's (resident) assets, + // so this doesn't need to wait for the carrier. JavaSnippetRepository.init() } override fun shutdown() { (this.debugAdapter as? AutoCloseable?)?.close() - JavaCompilerProvider.getInstance().destroy() - SourceFileManager.clearCache() - CacheFSInfoSingleton.clearCache() - clearCache() + compilerLifecycleLock.withLock { + // Blocks here if a reset is in flight (RESETTING can only be observed by another + // thread while the lock is held, never by us once we've acquired it), so this never + // races ensureProjectReset()'s own destroy/rebuild. Gated on whether a session + // actually exists, not on compilerLifecycle == INITIALIZED: setupWithProject() sets + // PENDING again on every project switch even once the carrier's already loaded (see + // setupWithProject() below), so a switch queued without a .java-file interaction yet + // leaves state at PENDING while loader.currentSession() still holds a live session + // from the previous project -- gating on INITIALIZED alone would skip teardown here + // and leak that session's DexClassLoader. + if (loader.currentSession() != null) { + // Unregister before closing: once closed, loader.currentSession() is null and the + // session's action objects (bound to this session's DexClassLoader) would + // otherwise stay wired into the shared, app-wide editor actions menu. + loader.currentSession()?.unregisterCodeActions() + codeActionsRegistered = false + loader.close() + CacheFSInfoSingleton.clearCache() + clearCache() + } + compilerLifecycle = CompilerLifecycle.SHUTDOWN + } EventBus.getDefault().unregister(this) timer.cancel() } @@ -161,96 +180,125 @@ class JavaLanguageServer : ILanguageServer { } override fun setupWithProject(workspace: Workspace) { - LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu) - - (ProjectManagerImpl.getInstance() - .indexingServiceManager - .getService(JvmLibraryIndexingService.ID) as? JvmLibraryIndexingService?) - ?.refresh() - - // Once we have project initialized - // Destory the NO_MODULE_COMPILER instance - JavaCompilerService.NO_MODULE_COMPILER.destroy() - - // Clear cached file managers - SourceFileManager.clearCache() + ( + ProjectManagerImpl + .getInstance() + .indexingServiceManager + .getService(JvmLibraryIndexingService.ID) as? JvmLibraryIndexingService? + )?.refresh() + + // Deferred to ensureProjectReset(), run on the first real .java-file interaction instead + // of here -- this method runs for every project open regardless of language + // (DefaultLanguageServerRegistry dispatches to all registered servers unconditionally), + // and loading the javac carrier eagerly here would defeat the point of isolating it + // (ADFA-5052, extended by ADFA-5053). + compilerLifecycleLock.withLock { + pendingWorkspace = workspace + // Leave RESETTING alone: ensureProjectReset()'s own finally block re-checks + // pendingWorkspace once it re-acquires the lock, so a project switch mid-reset is + // picked up as another PENDING round rather than raced here. + if (compilerLifecycle != CompilerLifecycle.RESETTING) { + compilerLifecycle = CompilerLifecycle.PENDING + } + } + } - // 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 - clearCachesForPaths { path: String -> path.endsWith("/R.jar") } + /** + * Runs the javac-specific project reset deferred by [setupWithProject], for the most + * recently opened project, the first time a real Java file is actually interacted with -- + * extracting and `DexClassLoader`-loading the carrier APK if this is the first interaction + * of the whole session. No-ops if already up to date. Blocks concurrent callers (and + * [shutdown]) for the entire reset, not just the decision to run one. + */ + private fun ensureProjectReset(): IJavaCompilerSession? = + compilerLifecycleLock.withLock { + if (compilerLifecycle != CompilerLifecycle.PENDING) return@withLock loader.currentSession() + val workspace = pendingWorkspace ?: return@withLock loader.currentSession() + pendingWorkspace = null + compilerLifecycle = CompilerLifecycle.RESETTING + + val session: IJavaCompilerSession + try { + session = loader.getOrCreateSession(workspace) + session.resetProject(workspace) + if (!codeActionsRegistered) { + session.registerCodeActions() + codeActionsRegistered = true + } + startOrRestartAnalyzeTimer() + } catch (e: Exception) { + // Re-queue the workspace so the next real .java-file interaction retries the + // reset, instead of a half-destroyed/half-rebuilt state being silently claimed as + // INITIALIZED (pendingWorkspace is already null by this point). + log.warn("Failed to reset javac project state; will retry on next interaction", e) + pendingWorkspace = workspace + compilerLifecycle = CompilerLifecycle.PENDING + throw e + } - // Clear cached module-specific compilers - JavaCompilerProvider.getInstance().destroy() + // A newer setupWithProject() may have queued another workspace while we were + // resetting (see the RESETTING guard above); if so, go back to PENDING instead of + // claiming INITIALIZED for a project we didn't actually reset for. + compilerLifecycle = + if (pendingWorkspace != null) { + CompilerLifecycle.PENDING + } else { + CompilerLifecycle.INITIALIZED + } - // Cache classpath locations - for (subModule in workspace.subProjects) { - if (subModule !is ModuleProject || subModule.path == workspace.rootProject.path) { - continue - } - SourceFileManager.forModule(subModule) + session } - startOrRestartAnalyzeTimer() - } + // complete() and formatCode() aren't suspend (fixed by the ILanguageServer contract), so -- + // like onContentChange() below -- they can hold compilerLifecycleLock across both + // ensureProjectReset() and the actual use of the session it returns: releasing the lock in + // between would let a concurrent reset destroy() the session's compilers right after this + // thread resolved it but before it's used. override fun complete(params: CompletionParams?): CompletionResult { - val compiler = getCompiler(params!!.file) - if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file) - ) { + if (params == null || !settings.completionsEnabled()) { return CompletionResult.EMPTY } - - if (diagnosticProvider.isAnalyzing()) { - log.warn("Cancelling source code analysis due to completion request") - diagnosticProvider.cancel() - } - - completionProvider.reset( - compiler, - settings, - cachedCompletion, - ) { cachedCompletion: CachedCompletion -> - updateCachedCompletion(cachedCompletion) + return compilerLifecycleLock.withLock { + ensureProjectReset()?.complete(params) ?: CompletionResult.EMPTY } - - return completionProvider.complete(params) } + // findReferences/findDefinition/expandSelection/signatureHelp/analyze are suspend (also + // fixed by the contract), so they can't use the same withLock pattern: the Kotlin compiler + // rejects a suspension point inside a Lock-based critical section outright (risk of blocking + // a thread pool while suspended), regardless of whether the call actually suspends. The + // residual window between ensureProjectReset() and use is narrower than it looks, though: + // JavaCompilerProvider.forModule()/destroy() (what getCompiler() and resetProject() actually + // touch) are already mutually `synchronized`, so a concurrent reset can't corrupt the + // provider map underneath a call started here -- it can only race the destruction of the one + // JavaCompilerService instance already in use, a narrower, pre-existing hazard (predates + // ADFA-5053) left as-is rather than papered over with a lock the compiler won't allow anyway. override suspend fun findReferences(params: ReferenceParams): ReferenceResult { - val compiler = getCompiler(params.file) - return if (!settings.referencesEnabled()) { - ReferenceResult(emptyList()) - } else { - ReferenceProvider(compiler, params.cancelChecker).findReferences(params) + if (!settings.referencesEnabled()) { + return ReferenceResult(emptyList()) } + return ensureProjectReset()?.findReferences(params) ?: ReferenceResult(emptyList()) } override suspend fun findDefinition(params: DefinitionParams): DefinitionResult { - val compiler = getCompiler(params.file) - return if (!settings.definitionsEnabled()) { - DefinitionResult(emptyList()) - } else { - DefinitionProvider(compiler, settings, params.cancelChecker).findDefinition(params) + if (!settings.definitionsEnabled()) { + return DefinitionResult(emptyList()) } + return ensureProjectReset()?.findDefinition(params) ?: DefinitionResult(emptyList()) } override suspend fun expandSelection(params: ExpandSelectionParams): Range { - val compiler = getCompiler(params.file) - return if (!settings.smartSelectionsEnabled()) { - params.selection - } else { - JavaSelectionProvider(compiler).expandSelection(params) + if (!settings.smartSelectionsEnabled()) { + return params.selection } + return ensureProjectReset()?.expandSelection(params) ?: params.selection } override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { - val compiler = getCompiler(params.file) - return if (!settings.signatureHelpEnabled()) { - SignatureHelp(emptyList(), -1, -1) - } else { - SignatureProvider(compiler, params.cancelChecker).signatureHelp(params) + if (!settings.signatureHelpEnabled()) { + return SignatureHelp(emptyList(), -1, -1) } + return ensureProjectReset()?.signatureHelp(params) ?: SignatureHelp(emptyList(), -1, -1) } override suspend fun analyze(file: Path): DiagnosticResult { @@ -258,43 +306,31 @@ class JavaLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } + // analyze() is often the first real .java-file interaction in a session (auto-triggered + // on file open, ahead of any completion request) -- without this gate, the javac carrier + // (and the R.jar/file-manager caches its reset clears) would never load for this project, + // and diagnostics could resolve against a stale previous project's classpath. + val session = ensureProjectReset() ?: return DiagnosticResult.NO_UPDATE + return if (!settings.codeAnalysisEnabled()) { DiagnosticResult.NO_UPDATE } else { - diagnosticProvider.analyze(file) + session.analyze(file) } } override fun formatCode(params: FormatCodeParams?): CodeFormatResult = - CodeFormatProvider(settings).format(params) - - override fun handleFailure(failure: LSPFailure?): Boolean { - return when (failure!!.type) { - FailureType.COMPLETION -> { - if (isCancelled(failure.error)) { - return true - } - JavaCompilerProvider.getInstance().destroy() - true - } + compilerLifecycleLock.withLock { + ensureProjectReset()?.formatCode(params) ?: CodeFormatResult.NONE } - } - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - fun getCompiler(file: Path?): JavaCompilerService { - if (!DocumentUtils.isJavaFile(file)) { - return JavaCompilerService.NO_MODULE_COMPILER + override fun handleFailure(failure: LSPFailure?): Boolean = + when (failure!!.type) { + FailureType.COMPLETION -> loader.currentSession()?.handleCompletionFailure(failure.error) ?: true } - val module = - ProjectManagerImpl.getInstance().findModuleForFile(file!!) - ?: return JavaCompilerService.NO_MODULE_COMPILER - return JavaCompilerProvider.get(module) - } - private fun updateCachedCompletion(cachedCompletion: CachedCompletion) { - Objects.requireNonNull(cachedCompletion) - this.cachedCompletion = cachedCompletion - } + /** For [JavaDebugAdapter]'s source-location resolution -- null if the carrier hasn't loaded yet. */ + internal fun currentCompilerSession(): IJavaCompilerSession? = loader.currentSession() private fun startOrRestartAnalyzeTimer() { if (VMUtils.isJvm) { @@ -314,14 +350,12 @@ class JavaLanguageServer : ILanguageServer { return } - // 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) { - val compiler = JavaCompilerProvider.get(module) - compiler.onDocumentChange(event) + // Held across the reset *and* the actual onContentChange call (ReentrantLock is + // reentrant, so ensureProjectReset()'s own withLock nests fine): otherwise a concurrent + // reset for a newer project could destroy() the session's compilers in the gap between + // this thread's reset finishing and its use. + compilerLifecycleLock.withLock { + ensureProjectReset()?.onContentChange(event) } startOrRestartAnalyzeTimer() } @@ -342,7 +376,7 @@ class JavaLanguageServer : ILanguageServer { @Subscribe(threadMode = ThreadMode.ASYNC) @Suppress("unused") fun onFileClosed(event: DocumentCloseEvent) { - diagnosticProvider.clearTimestamp(event.closedFile) + loader.currentSession()?.onFileClosed(event.closedFile) if (getActiveDocumentCount() == 0) { selectedFile = null diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt deleted file mode 100644 index fd9c7647b8..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 { - return ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) - as JavaLanguageServer - } - - protected fun ActionData.getLanguageClient(): ILanguageClient? { - return 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/common/OrganizeImportsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt deleted file mode 100644 index f1850d6e26..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.itsaky.androidide.lsp.java.actions.common - -import com.google.googlejavaformat.java.FormatterException -import com.google.googlejavaformat.java.ImportOrderer -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 output = ImportOrderer.reorderImports(content.toString(), settings.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/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt deleted file mode 100644 index 00c6f92af4..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt +++ /dev/null @@ -1,62 +0,0 @@ -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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt deleted file mode 100644 index de002a09bd..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt deleted file mode 100644 index f52e48134b..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt deleted file mode 100644 index b00001c582..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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 { - return 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/ImplementAbstractMethodsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt deleted file mode 100644 index 76971f321f..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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/RemoveMethodAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt deleted file mode 100644 index b0aa7ef5a1..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt deleted file mode 100644 index b1c3d35c33..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt deleted file mode 100644 index b831e43def..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt deleted file mode 100644 index f15b38cac6..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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/GenerateMissingConstructorAction.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt deleted file mode 100644 index e4fb0a058e..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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/compiler/CompileTask.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java deleted file mode 100644 index dac24ba968..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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; - } - - public CompilationUnitTree root() { - if (roots.size() != 1) { - throw new RuntimeException("No compilation units found. Roots: " + roots.size()); - } - return roots.get(0); - } - - 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"); - } - - 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"); - } - - @Override - public void close() {} -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt deleted file mode 100644 index 960ccc74bc..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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) - - return@withStopWatch TSJavaParser.parse(file).use { parseResult -> - - prune( - contentBuilder, - parseResult.tree, - compilerConfig.completionInfo?.cursor?.index ?: -1 - ) - - watch.log() - - return@use 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/SourceFileObject.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java deleted file mode 100644 index af91d0c00b..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.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.Files; -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 { - /** path is the absolute path to this file on disk */ - final Path path; - /** 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.contents = contents; - this.modified = modified; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this).add("path", this.path.toString()).toString(); - } - - @Override - public Kind getKind() { - String name = path.getFileName().toString(); - return kindFromExtension(name); - } - - private static Kind kindFromExtension(String name) { - for (Kind candidate : Kind.values()) { - if (name.endsWith(candidate.extension)) { - return candidate; - } - } - return null; - } - - @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - return path.getFileName().toString().equals(simpleName + kind.extension); - } - - @Override - public NestingKind getNestingKind() { - return null; - } - - @Override - public Modifier getAccessLevel() { - return null; - } - - @Override - public URI toUri() { - return this.path.toAbsolutePath().toUri(); - } - - @Override - public String getName() { - return path.toString(); - } - - @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 CharSequence getCharContent(boolean ignoreEncodingErrors) { - if (contents != null) { - return contents; - } - return FileManager.INSTANCE.getDocumentContents(this.path); - } - - @Override - public Writer openWriter() { - throw new UnsupportedOperationException(); - } - - @Override - public long getLastModified() { - if (contents != null) { - return modified.toEpochMilli(); - } - return FileManager.INSTANCE.getLastModified(this.path).toEpochMilli(); - } - - @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; - try { - return this.path != null && that.path != null - && Files.isSameFile(this.path, that.path) - && Objects.equals(contents, that.contents) - && Objects.equals(modified, that.modified); - } catch (Exception e) { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(path, contents, modified); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt deleted file mode 100644 index 102a38471c..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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 . - */ -/* - * 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) - } -} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt index a0ba24f34a..d0f2a6220d 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt @@ -24,6 +24,7 @@ import com.itsaky.androidide.lsp.debug.model.ThreadInfoResult import com.itsaky.androidide.lsp.debug.model.ThreadListRequestParams import com.itsaky.androidide.lsp.debug.model.ThreadListResponse import com.itsaky.androidide.lsp.java.JavaLanguageServer +import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession import com.itsaky.androidide.lsp.java.debug.spec.BreakpointSpec import com.itsaky.androidide.lsp.java.debug.utils.asDepthInt import com.itsaky.androidide.lsp.java.debug.utils.asJdiInt @@ -108,6 +109,12 @@ internal class JavaDebugAdapter : ): JavaDebugAdapter = checkNotNull(currentInstance(), message) } + /** The current javac session, or null if the carrier hasn't been loaded yet this session. */ + private fun currentCompilerSession(): IJavaCompilerSession? { + val lsp = ILanguageServerRegistry.default.getServer(JavaLanguageServer.SERVER_ID) + return (lsp as? JavaLanguageServer?)?.currentCompilerSession() + } + private fun connVm(): VmConnection { checkIsConnected() return this.vms.first() @@ -146,7 +153,7 @@ internal class JavaDebugAdapter : _listenerState?.invalidate() listenerThread?.interrupt() - + _listenerState = ListenerState( client = client, @@ -154,19 +161,20 @@ internal class JavaDebugAdapter : args = args, ) - val failure = withContext(Dispatchers.IO) { - try { - logger.debug("startListening") - listenerState.startListening() - null - } catch (e: Throwable) { - if (e is CancellationException) { - throw e + val failure = + withContext(Dispatchers.IO) { + try { + logger.debug("startListening") + listenerState.startListening() + null + } catch (e: Throwable) { + if (e is CancellationException) { + throw e + } + logger.error("Failed to listen for incoming JDWP connections", e) + return@withContext DebugClientConnectionResult.Failure(cause = e) } - logger.error("Failed to listen for incoming JDWP connections", e) - return@withContext DebugClientConnectionResult.Failure(cause = e) } - } if (failure != null) { return failure @@ -354,7 +362,7 @@ internal class JavaDebugAdapter : val spec = when (breakpoint) { - is PositionalBreakpoint -> + is PositionalBreakpoint -> { specList.createBreakpoint( source = breakpoint.source, // +1 because we receive 0-indexed line numbers from the IDE @@ -363,8 +371,9 @@ internal class JavaDebugAdapter : qualifiedName = qualifiedName, suspendPolicy = breakpoint.suspendPolicy.asJdiInt(), ) + } - is MethodBreakpoint -> + is MethodBreakpoint -> { specList.createBreakpoint( source = breakpoint.source, methodId = breakpoint.methodId, @@ -372,8 +381,11 @@ internal class JavaDebugAdapter : qualifiedName = qualifiedName, suspendPolicy = breakpoint.suspendPolicy.asJdiInt(), ) + } - else -> throw IllegalArgumentException("Unsupported breakpoint type: $breakpoint") + else -> { + throw IllegalArgumentException("Unsupported breakpoint type: $breakpoint") + } } val result = @@ -385,19 +397,23 @@ internal class JavaDebugAdapter : val resolveSuccess = result.getOrDefault(false) when { - resolveSuccess && spec.isResolved -> + resolveSuccess && spec.isResolved -> { BreakpointResult.Success( breakpoint, false, ) + } - resolveSuccess && !spec.isResolved -> + resolveSuccess && !spec.isResolved -> { BreakpointResult.Success( breakpoint, true, ) + } - else -> BreakpointResult.Failure(breakpoint, failure) + else -> { + BreakpointResult.Failure(breakpoint, failure) + } } }, ) @@ -530,7 +546,7 @@ internal class JavaDebugAdapter : event = BreakpointHitEvent( remoteClient = vm.client, - location = location.asLspLocation(), + location = location.asLspLocation(session = currentCompilerSession()), threadId = thread.uniqueID().toString(), ), ) @@ -548,7 +564,7 @@ internal class JavaDebugAdapter : event = LspStepEvent( remoteClient = vm.client, - location = location.asLspLocation(), + location = location.asLspLocation(session = currentCompilerSession()), threadId = thread.uniqueID().toString(), ), ) @@ -631,8 +647,10 @@ internal class JDWPListenerThread( override fun run() { logger.debug("run::start") if (!listenerState.isListening && !listenerState.isInvalidated) { - logger.warn("Listener should've been listening at this point, but it's not. " + - "Trying to start listening...") + logger.warn( + "Listener should've been listening at this point, but it's not. " + + "Trying to start listening...", + ) listenerState.startListening() } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/utils/ModelUtils.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/utils/ModelUtils.kt index a89dbfc565..360c24e315 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/utils/ModelUtils.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/utils/ModelUtils.kt @@ -1,14 +1,12 @@ package com.itsaky.androidide.lsp.java.debug.utils import com.itsaky.androidide.lsp.debug.model.Source -import com.itsaky.androidide.lsp.java.JavaCompilerProvider -import com.itsaky.androidide.lsp.java.compiler.SourceFileObject +import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.api.ModuleProject import com.sun.jdi.Location -import jdkx.tools.JavaFileObject import org.slf4j.LoggerFactory -import kotlin.jvm.optionals.getOrNull +import java.io.File import com.itsaky.androidide.lsp.debug.model.Location as LspLocation private val logger = LoggerFactory.getLogger("ModelUtilsKt") @@ -18,55 +16,70 @@ private val logger = LoggerFactory.getLogger("ModelUtilsKt") * * @param useDeclTypeName Whether to the [Location.declaringType] to get the name of the declaring * type of this location. + * @param session The current javac session, or null if the carrier hasn't been loaded yet (e.g. + * no `.java` file has been touched this session) -- source-path resolution is skipped in that + * case, same as when no matching source is found. */ -fun Location.asLspLocation(useDeclTypeName: Boolean = true): LspLocation { +fun Location.asLspLocation( + useDeclTypeName: Boolean = true, + session: IJavaCompilerSession?, +): LspLocation { val projectManager = ProjectManagerImpl.getInstance() - val fo = - projectManager.workspace - ?.subProjects - ?.filterIsInstance() - ?.mapNotNull { moduleProject -> - val service = JavaCompilerProvider.get(moduleProject) - var fo: JavaFileObject? = null - if (useDeclTypeName) { - val className = declaringType().name() - logger.debug("finding source file for decl class: '{}'", className) - fo = service.findAnywhere(declaringType().name()).getOrNull() - } + val path = + session?.let { s -> + projectManager.workspace + ?.subProjects + ?.filterIsInstance() + ?.mapNotNull { moduleProject -> + var path: String? = null + if (useDeclTypeName) { + val className = declaringType().name() + logger.debug("finding source file for decl class: '{}'", className) + path = s.findSourceFilePath(moduleProject, className) + } - if (fo == null) { - val className = - this - .sourcePath() - .replace('/', '.') - .substringBeforeLast(".java") - logger.debug("finding source file for class: '{}'", className) - fo = service.findAnywhere(className).getOrNull() - } + if (path == null) { + val className = + this + .sourcePath() + .replace('/', '.') + .substringBeforeLast(".java") + logger.debug("finding source file for class: '{}'", className) + path = s.findSourceFilePath(moduleProject, className) + } - if (fo != null && (fo.kind != JavaFileObject.Kind.SOURCE || fo !is SourceFileObject)) { - logger.debug("FileObject {} ({}) is not a source file", fo, fo.javaClass) - fo = null - } + if (path == null) { + logger.info("No source found for location: {}", this) + } - if (fo == null) { - logger.info("No source found for location: {}", this) - } - - return@mapNotNull fo as SourceFileObject? - }?.firstOrNull() // TODO: Maybe allow the user to choose which source file to open? + path + }?.firstOrNull() // TODO: Maybe allow the user to choose which source file to open? + } val source = - if (fo != null) { + if (path != null) { Source( - name = fo.name.substringAfterLast('/'), - path = fo.name, + name = path.substringAfterLast('/'), + path = path, ) } else { - Source( - name = sourceName(), - path = sourcePath(), - ) + // sourcePath() is JDI-synthetic (package-relative, e.g. "com/example/Foo.java"), not a + // filesystem path -- resolving it against each module's compile source directories + // works even without a session (e.g. the very first breakpoint hit before any + // .java-file interaction has loaded the carrier), unlike findSourceFilePath() above. + val relativePath = sourcePath().replace('/', File.separatorChar) + val resolvedPath = findSourceFileByRelativePath(relativePath) + if (resolvedPath != null) { + Source(name = sourceName(), path = resolvedPath) + } else { + logger.warn( + "Could not resolve a real source file for location {} (relative path '{}'); " + + "navigating to it will silently fail since this isn't a filesystem path.", + this, + relativePath, + ) + Source(name = sourceName(), path = sourcePath()) + } } return LspLocation( @@ -77,3 +90,15 @@ fun Location.asLspLocation(useDeclTypeName: Boolean = true): LspLocation { column = null, ) } + +private fun findSourceFileByRelativePath(relativePath: String): String? = + ProjectManagerImpl + .getInstance() + .workspace + ?.subProjects + ?.filterIsInstance() + ?.asSequence() + ?.flatMap { it.getCompileSourceDirectories() } + ?.map { File(it, relativePath) } + ?.firstOrNull { it.isFile } + ?.absolutePath diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt deleted file mode 100644 index 8b9886bb34..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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) - } - } - com.itsaky.androidide.lsp.util.RewriteHelper.performEdits(edits, editor) - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoader.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoader.kt new file mode 100644 index 0000000000..066bf67a1c --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoader.kt @@ -0,0 +1,114 @@ +/* + * 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.loader + +import android.content.Context +import com.itsaky.androidide.lsp.java.api.IJavaCompilerSession +import com.itsaky.androidide.lsp.java.api.IJavaCompilerSessionFactory +import com.itsaky.androidide.projects.api.Workspace +import dalvik.system.DexClassLoader +import org.slf4j.LoggerFactory +import java.io.File + +/** + * Extracts the javac carrier APK from assets and loads it via [DexClassLoader] on first use, + * so the vendored javac fork (ADFA-5053) is never resident in the main app dex or + * classloaded until a real `.java`-file interaction actually needs it. Mirrors + * `plugin-manager`'s `PluginLoader` construction -- the same lazy-`DexClassLoader` pattern + * ADFA-5010's `KotlinCompilerLoader`/ADR 0011 applies to the Kotlin Analysis API, a concurrent + * sibling effort not yet merged into `stage` as of this change (see docs/adr/0012). + */ +class JavaCompilerLoader( + private val context: Context, +) { + @Volatile + private var session: IJavaCompilerSession? = null + + private val carrierApk: File by lazy { extractCarrierApk() } + + private fun extractCarrierApk(): File { + val dir = context.getDir(CARRIER_DIR_NAME, Context.MODE_PRIVATE) + val dest = File(dir, CARRIER_APK_FILE_NAME) + val markerFile = File(dir, "$CARRIER_APK_FILE_NAME.marker") + + // The main APK's own mtime as a cheap "has the app been updated/reinstalled since we + // last extracted" marker -- avoids re-extracting on every process start. + val currentMarker = File(context.applicationInfo.sourceDir).lastModified().toString() + val previousMarker = runCatching { markerFile.readText() }.getOrNull() + + if (!dest.exists() || previousMarker != currentMarker) { + logger.info("Extracting Java compiler carrier APK to {}", dest) + context.assets.open(CARRIER_APK_ASSET_PATH).use { input -> + dest.outputStream().use { output -> input.copyTo(output) } + } + markerFile.writeText(currentMarker) + } + + return dest + } + + /** + * Returns the current session, creating one (extracting the carrier APK and + * `DexClassLoader`-loading it if needed) on first call. Blocking, like the + * `JavaCompilerService`/`SourceFileManager` construction it replaces was already + * blocking when it ran inside `ensureProjectReset()`. + */ + fun getOrCreateSession(workspace: Workspace): IJavaCompilerSession { + session?.let { return it } + + synchronized(this) { + session?.let { return it } + + val optimizedDir = File(context.codeCacheDir, "java_compiler_dex").apply { mkdirs() } + val classLoader = + DexClassLoader( + carrierApk.absolutePath, + optimizedDir.absolutePath, + null, + this::class.java.classLoader, + ) + + val factory = + classLoader + .loadClass(FACTORY_CLASS_NAME) + .getDeclaredConstructor() + .newInstance() as IJavaCompilerSessionFactory + + val created = factory.create(workspace) + session = created + return created + } + } + + fun currentSession(): IJavaCompilerSession? = session + + fun close() { + synchronized(this) { + session?.close() + session = null + } + } + + companion object { + private const val CARRIER_DIR_NAME = "java_compiler" + private const val CARRIER_APK_FILE_NAME = "java-compiler-carrier.apk" + private const val CARRIER_APK_ASSET_PATH = "data/common/$CARRIER_APK_FILE_NAME" + private const val FACTORY_CLASS_NAME = + "com.itsaky.androidide.lsp.java.compiler.JavaCompilerSessionFactoryImpl" + private val logger = LoggerFactory.getLogger(JavaCompilerLoader::class.java) + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt deleted file mode 100644 index d112687f7f..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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 { - return values().first { id == it.id } - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaServerSettings.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaServerSettings.java index c4e296204e..2e59cabe1f 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaServerSettings.java +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaServerSettings.java @@ -1,75 +1,63 @@ -/* - * 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; - -import androidx.annotation.NonNull; -import com.google.googlejavaformat.java.JavaFormatterOptions; -import com.itsaky.androidide.lsp.util.PrefBasedServerSettings; -import com.itsaky.androidide.managers.PreferenceManager; -import com.itsaky.androidide.preferences.internal.JavaPreferences; -import com.itsaky.androidide.utils.VMUtils; - -/** - * Server settings for the java language server. - * - * @author Akash Yadav - */ -public class JavaServerSettings extends PrefBasedServerSettings { - - public static final String KEY_JAVA_PREF_GOOGLE_CODE_STYLE = JavaPreferences.GOOGLE_CODE_STYLE; - public static final int CODE_STYLE_AOSP = 0; - public static final int CODE_STYLE_GOOGLE = 1; - private static JavaServerSettings instance; - - @NonNull - public static JavaServerSettings getInstance() { - if (instance == null) { - instance = new JavaServerSettings(); - } - - return instance; - } - - @Override - public boolean diagnosticsEnabled() { - return true; - } - - public JavaFormatterOptions getFormatterOptions() { - return JavaFormatterOptions.builder().formatJavadoc(true).style(getStyle()).build(); - } - - public JavaFormatterOptions.Style getStyle() { - if (getCodeStyle() == JavaServerSettings.CODE_STYLE_AOSP) { - - return JavaFormatterOptions.Style.AOSP; - } - - return JavaFormatterOptions.Style.GOOGLE; - } - - private int getCodeStyle() { - final PreferenceManager prefs = getPrefs(); - if (prefs != null) { - if (prefs.getBoolean(KEY_JAVA_PREF_GOOGLE_CODE_STYLE, false)) { - return CODE_STYLE_GOOGLE; - } - } - - return CODE_STYLE_AOSP; - } -} +/* + * 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; + +import androidx.annotation.NonNull; +import com.itsaky.androidide.lsp.util.PrefBasedServerSettings; +import com.itsaky.androidide.managers.PreferenceManager; +import com.itsaky.androidide.preferences.internal.JavaPreferences; + +/** + * Server settings for the java language server. + * + * @author Akash Yadav + */ +public class JavaServerSettings extends PrefBasedServerSettings { + + public static final String KEY_JAVA_PREF_GOOGLE_CODE_STYLE = JavaPreferences.GOOGLE_CODE_STYLE; + public static final int CODE_STYLE_AOSP = 0; + public static final int CODE_STYLE_GOOGLE = 1; + private static JavaServerSettings instance; + + @NonNull + public static JavaServerSettings getInstance() { + if (instance == null) { + instance = new JavaServerSettings(); + } + + return instance; + } + + @Override + public boolean diagnosticsEnabled() { + return true; + } + + /** + * {@link #CODE_STYLE_AOSP} or {@link #CODE_STYLE_GOOGLE}. Plain data rather than a google-java-format {@code JavaFormatterOptions}/{@code Style} value: this settings class stays resident, but google-java-format -- like javac -- is isolated in the DexClassLoader carrier (ADFA-5053), so the isolated {@code CodeFormatProvider} builds the real {@code JavaFormatterOptions} itself from this code. + */ + public int getCodeStyle() { + final PreferenceManager prefs = getPrefs(); + if (prefs != null) { + if (prefs.getBoolean(KEY_JAVA_PREF_GOOGLE_CODE_STYLE, false)) { + return CODE_STYLE_GOOGLE; + } + } + + return CODE_STYLE_AOSP; + } +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java deleted file mode 100644 index 8a6ba9cc7d..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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 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); - } - - /** - * 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)); - } - - private static void ignoreError(jdkx.tools.Diagnostic __) { - // Too noisy, this only comes up in parse tasks which tend to be less important - // LOG.warning(err.getMessage(Locale.getDefault())); - } - - public static Parser parseFile(Path file) { - return parseJavaFileObject(new SourceFileObject(file)); - } - - public static Parser parseJavaFileObject(JavaFileObject file) { - if (needsParse(file)) { - loadParse(file); - } - return cachedParse; - } - - private static boolean needsParse(JavaFileObject file) { - if (cachedParse == null) { - return true; - } - if (!cachedParse.file.equals(file)) { - return true; - } - - return file.getLastModified() > cachedModified; - } - - private static void loadParse(JavaFileObject file) { - cachedParse = new Parser(file); - cachedModified = file.getLastModified(); - } - - 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)); - } - - 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; - } - - 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 ""; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt deleted file mode 100644 index 9f9f277c27..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt deleted file mode 100644 index 921e638fe5..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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/providers/CodeFormatProvider.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java deleted file mode 100644 index f73e2ab3c5..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.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 Formatter formatter = new Formatter(settings.getFormatterOptions()); - - 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/DefinitionProvider.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java deleted file mode 100644 index ae62e81758..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * 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; - } - - @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); - } - - 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); - } - - 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/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt deleted file mode 100644 index abb6a33168..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt +++ /dev/null @@ -1,318 +0,0 @@ -/* - * 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 { - return 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 { - return 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/ReferenceProvider.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java deleted file mode 100644 index b799787b16..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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; - } - - @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); - } - - 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(); - } - - 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); - } - - 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; - } -} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java deleted file mode 100644 index de4b9a3afc..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java +++ /dev/null @@ -1,391 +0,0 @@ -/* - * 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(@NonNull SignatureHelpParams params) { - return signatureHelp( - params.getFile(), params.getPosition().getLine(), params.getPosition().getColumn()); - } - - @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; - }); - } - - 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 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; - } - - @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 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; - } - - @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; - } - - @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 ParameterInformation parameter(@NonNull VariableElement p) { - abortIfCancelled(); - ParameterInformation info = new ParameterInformation(); - info.setLabel(ShortTypePrinter.NO_PACKAGE.print(p.asType())); - return info; - } - - 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)); - } - - 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 + ")"); - } - - @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; - } - - private int activeParameter( - @NonNull CompileTask task, @NonNull List 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 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 boolean isCompatible( - CompileTask task, - TreePath invocation, - List 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; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt deleted file mode 100644 index 0b2f57916e..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt deleted file mode 100644 index bf43a77cec..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt +++ /dev/null @@ -1,403 +0,0 @@ -/* - * 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 { - return 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 { - return if (name.contains(".")) { - name.subSequence(name.lastIndexOf('.') + 1, name.length) - } else name - } - - private fun packageName(name: CharSequence): CharSequence { - return 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 { - return 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt deleted file mode 100644 index 3986cea4f4..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt deleted file mode 100644 index b8434e9b6c..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt +++ /dev/null @@ -1,446 +0,0 @@ -/* - * 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 { - return isType(element.kind) - } - - internal fun isType(kind: ElementKind): Boolean { - return 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt deleted file mode 100644 index 779bba385d..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt deleted file mode 100644 index 82ece497ba..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt deleted file mode 100644 index b6d1ed99cf..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 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 { - return 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt deleted file mode 100644 index e6c48e71ac..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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 - val parentPath = path.parentPath /*method*/.parentPath /*class*/ - 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt deleted file mode 100644 index fae50b8c27..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt deleted file mode 100644 index ba3a66dd96..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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 { - return (staticImport.contentEquals("*") || matchLevel(staticImport, partial) != NO_MATCH) - } - - private fun memberMatchesImport(staticImport: Name, member: Element): Boolean { - return staticImport.contentEquals("*") || staticImport.contentEquals(member.simpleName) - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt deleted file mode 100644 index b66912219c..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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/rewrite/AddException.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java deleted file mode 100644 index b2c7f5d6d7..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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/AddSuppressWarningAnnotation.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java deleted file mode 100644 index 4fab2520ec..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java deleted file mode 100644 index 5369291828..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java deleted file mode 100644 index 6f23dc1fce..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 { - 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); - } - - 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; - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java deleted file mode 100644 index bd3bffa87b..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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; - MethodTree currentMethod = surroundingMethod(path); - final var insertTextBuilder = new StringBuilder("\n"); - - final var indent = EditorUtilKt.getIndentationString(); - - final var isStatic = 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 { - compilationUnit = task.root(); - enclosingClass = surroundingClass(path); - insertPoint = insertAfter(task.task, compilationUnit, surroundingMethod(path)); - } - - 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 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"); - } - - private MethodTree surroundingMethod(TreePath call) { - while (call != null) { - if (call.getLeaf() instanceof MethodTree) { - return (MethodTree) call.getLeaf(); - } - call = call.getParentPath(); - } - throw new RuntimeException("No surrounding method"); - } - - 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 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; - Name name = declared.asElement().getSimpleName(); - return "" + Character.toLowerCase(name.charAt(0)) + name.subSequence(1, name.length()); - } else { - return ""; - } - } - - 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"; - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java deleted file mode 100644 index 2d4ba69ec3..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * 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); - ClassTree typeTree = Trees.instance(task.task).getTree(typeElement); - 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 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 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 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 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 simpleName(String className) { - int dot = className.lastIndexOf('.'); - if (dot != -1) { - return className.substring(dot + 1); - } - return className; - } - - 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); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java deleted file mode 100644 index 5524ee3ef9..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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])); - }); - } - - 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); - } - } - } - - private Set getFileImports(@NonNull CompileTask task, Path file) { - return task.root(file).getImports().stream() - .map(ImportTree::getQualifiedIdentifier) - .map(Tree::toString) - .collect(Collectors.toSet()); - } - - @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; - } - - @Override - protected void finalizeCodeAction(@NonNull CodeActionItem action) { - action.setCommand(new Command("Format code", Command.FORMAT_CODE)); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java deleted file mode 100644 index e98aeafcb3..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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"); - final String className, methodName; - final String[] erasedParameterTypes; - final String exceptionType; - - private static final Logger LOG = LoggerFactory.getLogger(RemoveException.class); - - 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 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 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 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 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 int removeTrailingComma(CompilationUnitTree root, long end) { - CharSequence contents = contents(root); - for (int i = (int) end; i < contents.length(); i++) { - if (contents.charAt(i) == ',') { - if (contents.charAt(i + 1) == ' ') { - return i + 2; - } else { - return i + 1; - } - } - } - return -1; - } - - private CharSequence contents(CompilationUnitTree root) { - try { - return root.getSourceFile().getCharContent(true); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt deleted file mode 100644 index c9ecc85d13..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 var CANCELLED = emptyMap>() - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java deleted file mode 100644 index 3fbf10daaf..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 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)); - } - } - - 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)); - } - } - } - - public static class Edit { - private static final Ordering REVERSE_INSERTION = - Ordering.natural().onResultOf(Edit::getPos).reverse(); - - 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; - } - - @NonNull - @Contract(value = "_, _ -> new", pure = true) - public static Edit create(int pos, String text) { - return new Edit(pos, text); - } - - @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; - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java deleted file mode 100644 index e7267416c9..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 boolean isInMethod(@NonNull CompileTask task, long cursor) { - MethodTree method = new FindMethodDeclarationAt(task.task).scan(task.root(), cursor); - return method != null; - } - - 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 int findPosition(@NonNull CompileTask task, @NonNull Position position) { - final LineMap lines = task.root().getLineMap(); - return (int) lines.getPosition(position.getLine() + 1, position.getColumn() + 1); - } - - @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 - @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 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 isConstructor(CompileTask task, @NonNull MethodTree method) { - return method.getName().contentEquals("") && !synthetic(task, method); - } - - public static boolean synthetic(@NonNull CompileTask task, MethodTree method) { - return Trees.instance(task.task).getSourcePositions().getStartPosition(task.root(), method) - != -1; - } - - @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 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); - } - - 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); - } - - @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 - @Contract(pure = true) - public static JCDiagnostic unwrapJCDiagnostic(Diagnostic diagnostic) { - return JavaDiagnosticUtils.asJCDiagnostic(diagnostic); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java deleted file mode 100644 index a49b46d6f9..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 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, ""); - } - - 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 int indent(final JavacTask task, final CompilationUnitTree root, final Tree leaf) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - return indent(root, leaf, pos); - } - - 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); - } - - 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 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)); - } - - 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 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/FindHelper.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java deleted file mode 100644 index 3e7a2a066f..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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; - } - - /** - * 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 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 ClassTree findType(ParseTask task, String className) { - return new FindTypeDeclarationNamed().scan(task.root, className); - } - - 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; - } - - 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; - } - - 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); - } - - 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; - } - - 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/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt deleted file mode 100644 index 7f0c5c83fb..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt +++ /dev/null @@ -1,789 +0,0 @@ -/* - * 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 { - return 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? { - return 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? { - return 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 { - return 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? { - return StaticJavaParser.parseTypeParameter(type.toString()) - } - - fun toTypeParameter(typeVariable: TypeVariable): TypeParameter? { - return 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 { - return 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/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt deleted file mode 100644 index 4a2ec418e9..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 { - return 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/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java deleted file mode 100644 index fa687be4cc..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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 MarkupContent asMarkupContent(DocCommentTree comment) { - String markdown = asMarkdown(comment); - MarkupContent content = new MarkupContent(); - content.setKind(MarkupKind.MARKDOWN); - content.setValue(markdown); - return content; - } - - public static String asMarkdown(DocCommentTree comment) { - List 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; - } - - private static String asMarkdown(List lines) { - StringJoiner join = new StringJoiner("\n"); - for (DocTree l : lines) join.add(l.toString()); - String html = join.toString(); - return asMarkdown(html); - } - - 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 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 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 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 char peek(CharBuffer in) { - return in.get(in.position()); - } - - 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 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 void parse(CharBuffer in, StringBuilder out) { - while (!empty(in)) { - parseInner(in, out); - } - } - - private static String replaceTags(String in) { - StringBuilder out = new StringBuilder(); - parse(CharBuffer.wrap(in), out); - return out.toString(); - } - - 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("", tag); - int findClose = text.indexOf(close, tags.end()); - if (findClose != -1) return true; - } - return false; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java deleted file mode 100644 index 803a3c1fb1..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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); - } - } - - 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(); - } - } - - @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(); - } - - @Override - public int hashCode() { - int result = Objects.hash(className, methodName); - result = 31 * result + Arrays.hashCode(erasedParameterTypes); - result = 31 * result + Arrays.hashCode(simplifiedErasedParameterTypes); - return result; - } - - @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); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java deleted file mode 100644 index b7990091e5..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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 { - 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; - } - - // 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); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java deleted file mode 100644 index 14a63d33b0..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java +++ /dev/null @@ -1,107 +0,0 @@ -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; - } - - @Override - public String visitIntersection(IntersectionType t, Void aVoid) { - return t.getBounds().stream().map(this::print).collect(Collectors.joining(" & ")); - } - - public String print(TypeMirror type) { - return type.accept(new ShortTypePrinter(packageContext), null); - } - - @Override - public String visitUnion(UnionType t, Void aVoid) { - return t.getAlternatives().stream().map(this::print).collect(Collectors.joining(" | ")); - } - - @Override - public String visitPrimitive(PrimitiveType t, Void aVoid) { - return t.toString(); - } - - @Override - public String visitNull(NullType t, Void aVoid) { - return t.toString(); - } - - @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 visitTypeVariable(TypeVariable t, Void aVoid) { - return t.asElement().toString(); - } - - @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; - } - - @Override - public String visitExecutable(ExecutableType t, Void aVoid) { - return t.toString(); - } - - @Override - public String visitNoType(NoType t, Void aVoid) { - return t.toString(); - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java deleted file mode 100644 index 3c5385c9d8..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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 { - - // 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 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 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 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; - } - - // type mirrors - - 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 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; - } - - 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 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; - } - - 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 String getSimpleName(Type type) { - PrinterConfiguration configuration = new DefaultPrinterConfiguration(); - PrettyPrintingVisitor visitor = new PrettyPrintingVisitor(configuration); - DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(t -> visitor, configuration); - return prettyPrinter.print(type); - } - - 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()); - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt deleted file mode 100644 index bf9d3383ce..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt +++ /dev/null @@ -1,400 +0,0 @@ -/* - * 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 field was injected while forming the AST - unused.removeIf { it.toString() == "" } - return unused - } - - private fun foundPrivateDeclaration() { - abortIfCancelled() - val element = trees.getElement(path) ?: return - privateDeclarations[element] = path!! - } - - private fun foundLocalVariable() { - abortIfCancelled() - val element = trees.getElement(path) ?: return - localVariables[element] = path!! - } - - private fun foundReference() { - abortIfCancelled() - val toEl = trees.getElement(path) ?: return - if (toEl.asType().kind == TypeKind.ERROR) { - foundPseudoReference(toEl) - return - } - - sweep(toEl) - } - - private fun foundPseudoReference(toEl: Element) { - abortIfCancelled() - val parent = toEl.enclosingElement as? TypeElement ?: return - val memberName = toEl.simpleName - for (member in parent.enclosedElements) { - if (member.simpleName.contentEquals(memberName)) { - sweep(member) - } - } - } - - private fun sweep(toEl: Element) { - abortIfCancelled() - val firstUse = used.add(toEl) - val notScanned = firstUse && privateDeclarations.containsKey(toEl) - if (notScanned) { - scanPath(privateDeclarations[toEl]!!) - } - } - - private fun isReachable(path: TreePath): Boolean { - abortIfCancelled() - // Check if t is reachable because it's public - val leaf = path.leaf - if (leaf is VariableTree) { - val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) - if (!isPrivate || isLocalVariable(path)) { - return true - } - } - - if (leaf is MethodTree) { - val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) - val isEmptyConstructor = leaf.parameters.isEmpty() && leaf.returnType == null - if (!isPrivate || isEmptyConstructor) { - return true - } - } - - if (leaf is ClassTree) { - val isPrivate = leaf.modifiers.flags.contains(Modifier.PRIVATE) - if (!isPrivate) { - return true - } - } - - abortIfCancelled() - - // Check if t has been referenced by a reachable element - val el = trees.getElement(path) - return used.contains(el) - } - - private fun isLocalVariable(path: TreePath): Boolean { - abortIfCancelled() - val kind = path.leaf.kind - if (kind != Tree.Kind.VARIABLE) { - return false - } - - val parent = path.parentPath.leaf.kind - if (parent == Tree.Kind.CLASS || parent == Tree.Kind.INTERFACE) { - return false - } - - if (parent == Tree.Kind.METHOD) { - val method = path.parentPath.leaf as MethodTree - return method.body != null - } - - return true - } - - private fun declared(t: MethodTree): MutableMap { - abortIfCancelled() - val names = mutableMapOf() - for (e in t.throws) { - val path = TreePath(path, e) - val to = trees.getElement(path) as? TypeElement ?: continue - val name = to.qualifiedName.toString() - names[name] = path - } - return names - } - - override fun visitCompilationUnit( - t: CompilationUnitTree, - notThrown: MutableMap - ): Void? { - abortIfCancelled() - return super.visitCompilationUnit(t, notThrown) - } - - override fun visitVariable(t: VariableTree?, notThrown: MutableMap?): Void? { - when { - isLocalVariable(path!!) -> { - foundLocalVariable() - super.visitVariable(t, notThrown) - } - - isReachable(path!!) -> super.visitVariable(t, notThrown) - else -> foundPrivateDeclaration() - } - return null - } - - override fun visitMethod(t: MethodTree?, notThrown: MutableMap?): Void? { - abortIfCancelled() - if (t == null || notThrown == null) { - return null - } - - // Create a new method scope - val pushDeclared = declaredExceptions - val pushObserved = observedExceptions - declaredExceptions = declared(t) - observedExceptions = HashSet() - - abortIfCancelled() - // Recursively scan for 'throw' and method calls - super.visitMethod(t, notThrown) - abortIfCancelled() - - // Check for exceptions that were never thrown - for (exception in declaredExceptions.keys) { - if (!observedExceptions.contains(exception)) { - notThrown[declaredExceptions[exception]] = exception - } - } - declaredExceptions = pushDeclared - observedExceptions = pushObserved - if (!isReachable(path!!)) { - foundPrivateDeclaration() - } - return null - } - - override fun visitClass(t: ClassTree?, notThrown: MutableMap?): Void? { - if (isReachable(path!!)) { - super.visitClass(t, notThrown) - } else { - foundPrivateDeclaration() - } - return null - } - - override fun visitIdentifier( - t: IdentifierTree?, - notThrown: MutableMap? - ): Void? { - foundReference() - return super.visitIdentifier(t, notThrown) - } - - override fun visitMemberSelect( - t: MemberSelectTree?, - notThrown: MutableMap? - ): Void? { - foundReference() - return super.visitMemberSelect(t, notThrown) - } - - override fun visitMemberReference( - t: MemberReferenceTree?, - notThrown: MutableMap? - ): Void? { - foundReference() - return super.visitMemberReference(t, notThrown) - } - - override fun visitNewClass(t: NewClassTree?, notThrown: MutableMap?): Void? { - foundReference() - return super.visitNewClass(t, notThrown) - } - - override fun visitThrow(t: ThrowTree?, notThrown: MutableMap?): Void? { - abortIfCancelled() - if (t == null) { - return null - } - - val path = TreePath(path, t.expression) - val type = trees.getTypeMirror(path) - addThrown(type) - return super.visitThrow(t, notThrown) - } - - override fun visitMethodInvocation( - t: MethodInvocationTree?, - notThrown: MutableMap? - ): Void? { - abortIfCancelled() - val target = trees.getElement(path) - if (target is ExecutableElement) { - for (type in target.thrownTypes) { - addThrown(type) - } - } - - return super.visitMethodInvocation(t, notThrown) - } - - override fun visitBlock(node: BlockTree?, p: MutableMap?): Void? { - abortIfCancelled() - if (node != null && node.statements.isEmpty()) { - val name: String? = - when (val parent = path!!.parentPath.leaf) { - is IfTree -> fromIfTree(node, parent) - is TryTree -> fromTryTree(node, parent) - is ForLoopTree -> fromForTree(parent, node) - is WhileLoopTree -> fromWhileTree(parent, node) - is DoWhileLoopTree -> fromDoWhileTree(parent, node) - else -> null - } - - if (name != null) { - emptyBlocks[path!!] = name - } - } - return super.visitBlock(node, p) - } - - private fun fromDoWhileTree(parent: DoWhileLoopTree, node: BlockTree?) = - if (parent.statement == node) { - "do" - } else { - null - } - - private fun fromWhileTree(parent: WhileLoopTree, node: BlockTree?) = - if (parent.statement == node) { - "while" - } else { - null - } - - private fun fromForTree(parent: ForLoopTree, node: BlockTree?) = - if (parent.statement == node) { - "for" - } else { - null - } - - private fun fromTryTree(node: BlockTree, parent: TryTree) = - when (node) { - parent.block -> "try" - parent.finallyBlock -> "finally" - else -> { - val catch = - if (parent.catches.find { it.block == node } != null) { - "catch" - } else { - null - } - catch - } - } - - private fun fromIfTree(node: BlockTree, parent: IfTree) = - when (node) { - parent.thenStatement -> "if" - parent.elseStatement -> "else" - else -> null - } - - private fun addThrown(type: TypeMirror) { - abortIfCancelled() - if (type is DeclaredType) { - val el = type.asElement() as TypeElement - val name = el.qualifiedName.toString() - observedExceptions.add(name) - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java deleted file mode 100644 index fc973ac116..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.itsaky.androidide.models.Position; -import com.itsaky.androidide.models.Range; -import openjdk.source.tree.ClassTree; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.LineMap; -import openjdk.source.tree.MethodTree; -import openjdk.source.tree.PackageTree; -import openjdk.source.tree.Tree; -import openjdk.source.tree.TryTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -/** - * @author Akash Yadav - */ -public class FindBiggerRange extends TreePathScanner { - - private final SourcePositions positions; - private final CompilationUnitTree root; - private final LineMap lineMap; - private final Range rootRange; - - public FindBiggerRange(JavacTask task, @NonNull CompilationUnitTree root) { - this.positions = Trees.instance(task).getSourcePositions(); - this.root = root; - this.lineMap = root.getLineMap(); - - this.rootRange = getRange(root); - } - - @Override - public Range scan(Tree tree, Range range) { - if (range.equals(rootRange)) { - // if whole file content selected, no need to scan the tree - return null; - } - - final Range smallerThanThis = super.scan(tree, range); - if (smallerThanThis != null) { - return smallerThanThis; - } - - final Range treeRange = getRange(tree); - if (treeRange != null && range.isSmallerThan(treeRange)) { - return treeRange; - } - - return null; - } - - @Override - public Range reduce(Range r1, Range r2) { - return r1 == null ? r2 : r1; - } - - @Override - public Range visitPackage(PackageTree node, Range range) { - final var packageRange = getRange(node); - if (range.equals(packageRange)) { - final var parentPath = getCurrentPath().getParentPath(); - if (parentPath != null && parentPath.getLeaf() instanceof CompilationUnitTree) { - return rootRange; - } - } - return super.visitPackage(node, range); - } - - @Override - public Range visitClass(ClassTree node, Range range) { - final var classRange = getRange(node); - if (range.equals(classRange)) { - final var parentPath = getCurrentPath().getParentPath(); - if (parentPath != null && parentPath.getLeaf() instanceof CompilationUnitTree) { - return rootRange; - } - } - return super.visitClass(node, range); - } - - @Override - public Range visitMethod(MethodTree node, Range range) { - - // If this methods body is selected, then select the entire method - final Range methodRange = getRange(node); - final Range blockRange = getRange(node.getBody()); - if (range.equals(blockRange) && methodRange != null) { - return methodRange; - } - - return super.visitMethod(node, range); - } - - @Override - public Range visitTry(TryTree node, Range range) { - - // If this try's body or finally block is selected, then select the entire try - final Range methodRange = getRange(node); - final Range blockRange = getRange(node.getBlock()); - final Range finallyRange = getRange(node.getFinallyBlock()); - if ((range.equals(blockRange) || range.equals(finallyRange)) && methodRange != null) { - return methodRange; - } - - return super.visitTry(node, range); - } - - @Nullable - private Range getRange(Tree leaf) { - final Range range = new Range(); - final Position start = new Position(0, 0); - final Position end = new Position(0, 0); - - final long startPos = positions.getStartPosition(root, leaf); - final long endPos = positions.getEndPosition(root, leaf); - - if (startPos == -1 || endPos == -1) { - return null; - } - - start.setLine((int) lineMap.getLineNumber(startPos) - 1); - start.setColumn((int) lineMap.getColumnNumber(startPos) - 1); - - end.setLine((int) lineMap.getLineNumber(endPos) - 1); - end.setColumn((int) lineMap.getColumnNumber(endPos) - 1); - - range.setStart(start); - range.setEnd(end); - - return range; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java deleted file mode 100644 index 41deeb3b72..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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 openjdk.source.tree.CaseTree; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.ErroneousTree; -import openjdk.source.tree.IdentifierTree; -import openjdk.source.tree.ImportTree; -import openjdk.source.tree.MemberReferenceTree; -import openjdk.source.tree.MemberSelectTree; -import openjdk.source.tree.Tree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -public class FindCompletionsAt extends TreePathScanner { - - // private static final ILogger LOG = ILogger.newInstance("FindCompletionsAt"); - private final JavacTask task; - private CompilationUnitTree root; - - public FindCompletionsAt(JavacTask task) { - this.task = task; - } - - @Override - public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - return reduce(super.visitCompilationUnit(t, find), getCurrentPath()); - } - - @Override - public TreePath visitIdentifier(IdentifierTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getStartPosition(root, t); - long end = pos.getEndPosition(root, t); - if (start <= find && find <= end) { - return getCurrentPath(); - } - return super.visitIdentifier(t, find); - } - - @Override - public TreePath visitMemberSelect(MemberSelectTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getEndPosition(root, t.getExpression()) + 1; - long end = pos.getEndPosition(root, t); - if (start <= find && find <= end) { - return getCurrentPath(); - } - return super.visitMemberSelect(t, find); - } - - @Override - public TreePath visitMemberReference(MemberReferenceTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getEndPosition(root, t.getQualifierExpression()) + 2; - long end = pos.getEndPosition(root, t); - if (start <= find && find <= end) { - return getCurrentPath(); - } - return super.visitMemberReference(t, find); - } - - @Override - public TreePath visitCase(CaseTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - - // check if the cursor is in the case expression - // default statements have null expression - // In case of an identifier tree, we have to check for both, variables and switch constants - // in - // CompletionProvider - if (t.getExpression() != null && !(t.getExpression() instanceof IdentifierTree)) { - long start = pos.getStartPosition(root, t.getExpression()); - long end = pos.getEndPosition(root, t.getExpression()); - if (start <= find && find <= end) { - return new TreePath(getCurrentPath(), t.getExpression()); - } - } - - long start = pos.getStartPosition(root, t) + "case".length(); - long end = pos.getEndPosition(root, t.getExpression()); - if (start <= find && find <= end) { - return getCurrentPath().getParentPath(); - } - - return super.visitCase(t, find); - } - - @Override - public TreePath visitImport(ImportTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getStartPosition(root, t.getQualifiedIdentifier()); - long end = pos.getEndPosition(root, t.getQualifiedIdentifier()); - if (start <= find && find <= end) { - return getCurrentPath(); - } - return super.visitImport(t, find); - } - - @Override - public TreePath visitErroneous(ErroneousTree t, Long find) { - if (t.getErrorTrees() == null) { - return null; - } - for (Tree e : t.getErrorTrees()) { - TreePath found = scan(e, find); - if (found != null) { - return found; - } - } - return null; - } - - @Override - public TreePath reduce(TreePath a, TreePath b) { - if (a != null) { - return a; - } - return b; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java deleted file mode 100644 index e90303922e..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.ICancelChecker; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.MethodInvocationTree; -import openjdk.source.tree.NewClassTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -public class FindInvocationAt extends TreePathScanner { - - private final JavacTask task; - private final ICancelChecker cancelChecker; - private CompilationUnitTree root; - - public FindInvocationAt(JavacTask task, ICancelChecker cancelChecker) { - this.task = task; - this.cancelChecker = cancelChecker; - } - - @Override - public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { - cancelChecker.abortIfCancelled(); - root = t; - return reduce(super.visitCompilationUnit(t, find), getCurrentPath()); - } - - @Override - public TreePath visitMethodInvocation(MethodInvocationTree t, Long find) { - cancelChecker.abortIfCancelled(); - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getEndPosition(root, t.getMethodSelect()) + 1; - long end = pos.getEndPosition(root, t) - 1; - if (start <= find && find <= end) { - return reduce(super.visitMethodInvocation(t, find), getCurrentPath()); - } - return super.visitMethodInvocation(t, find); - } - - @Override - public TreePath visitNewClass(NewClassTree t, Long find) { - cancelChecker.abortIfCancelled(); - SourcePositions pos = Trees.instance(task).getSourcePositions(); - long start = pos.getEndPosition(root, t.getIdentifier()) + 1; - long end = pos.getEndPosition(root, t) - 1; - if (start <= find && find <= end) { - return reduce(super.visitNewClass(t, find), getCurrentPath()); - } - return super.visitNewClass(t, find); - } - - @Override - public TreePath reduce(TreePath a, TreePath b) { - cancelChecker.abortIfCancelled(); - if (a != null) { - return a; - } - return b; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java deleted file mode 100644 index 734b119482..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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 jdkx.lang.model.element.Element; -import jdkx.lang.model.element.NestingKind; -import jdkx.lang.model.element.TypeElement; -import jdkx.lang.model.type.TypeKind; -import jdkx.lang.model.type.TypeMirror; -import openjdk.source.tree.AssignmentTree; -import openjdk.source.tree.ClassTree; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.MemberSelectTree; -import openjdk.source.tree.MethodInvocationTree; -import openjdk.source.tree.Tree; -import openjdk.source.tree.VariableTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -public class FindMethodCallAt extends TreePathScanner { - - private final Trees trees; - private final SourcePositions pos; - private CompilationUnitTree root; - private boolean isMemberSelect; - private boolean isStatic; - private String returnType; - private ClassTree enclosingTree; - private TreePath enclosingTreePath; - private TypeElement declaredInTopLevel; - private TypeElement enclosingElement; - - public FindMethodCallAt(JavacTask task) { - this.trees = Trees.instance(task); - this.pos = trees.getSourcePositions(); - } - - public String getReturnType() { - return returnType; - } - - public TypeElement getDeclaringType() { - return declaredInTopLevel; - } - - public TypeElement getEnclosingElement() { - return enclosingElement; - } - - public ClassTree getEnclosingClass() { - return enclosingTree; - } - - public TreePath getEnclosingTreePath() { - return enclosingTreePath; - } - - public boolean isMemberSelect() { - return isMemberSelect; - } - - public boolean isStaticAccess() { - return isStatic; - } - - @Override - public MethodInvocationTree scan(Tree tree, Integer find) { - final var result = super.scan(tree, find); - if (result != null && result.getMethodSelect() instanceof MemberSelectTree) { - initProperties(((MemberSelectTree) result.getMethodSelect()), find); - } - return result; - } - - @Override - public MethodInvocationTree reduce(MethodInvocationTree r1, MethodInvocationTree r2) { - if (r1 != null) { - return r1; - } - return r2; - } - - @Override - public MethodInvocationTree visitCompilationUnit(CompilationUnitTree t, Integer find) { - root = t; - return super.visitCompilationUnit(t, find); - } - - /** - * Find method invocation while declaring a variable E.g. - * - *
 int x = foo(); 
- */ - @Override - public MethodInvocationTree visitVariable(VariableTree tree, Integer find) { - if (tree != null) { - long start = pos.getStartPosition(root, tree); - long end = pos.getEndPosition(root, tree); - if (start <= find && find <= end && tree.getInitializer() instanceof MethodInvocationTree) { - returnType = tree.getType().toString(); - return visitMethodInvocation((MethodInvocationTree) tree.getInitializer(), find); - } - } - return super.visitVariable(tree, find); - } - - /** - * A method invocation - * - *
-   * 	foo();
-   * 	field.foo();
-   * 	Class.foo();
-   * 	Class.field.foo();
-   * 
- */ - @Override - public MethodInvocationTree visitMethodInvocation(MethodInvocationTree t, Integer find) { - MethodInvocationTree smaller = super.visitMethodInvocation(t, find); - if (smaller != null) { - return smaller; - } - - if (pos.getStartPosition(root, t) <= find && find < pos.getEndPosition(root, t)) { - return t; - } - - return null; - } - - /** - * Find method invocation while initializing a variable E.g. - * - *
-   * 	int x;
-   * 	...
-   * 	x = foo();
-   * 
- */ - @Override - public MethodInvocationTree visitAssignment(AssignmentTree tree, Integer find) { - if (tree != null) { - long start = pos.getStartPosition(root, tree); - long end = pos.getEndPosition(root, tree); - if (start <= find && find <= end && tree.getExpression() instanceof MethodInvocationTree) { - returnType = findType(); - return visitMethodInvocation((MethodInvocationTree) tree.getExpression(), find); - } - } - return super.visitAssignment(tree, find); - } - - private String findType() { - if (this.trees != null && getCurrentPath() != null) { - TypeMirror typeMirror = this.trees.getTypeMirror(getCurrentPath()); - if (typeMirror != null - && typeMirror.getKind() != TypeKind.NONE - && typeMirror.getKind() != TypeKind.ERROR) { - return typeMirror.toString(); - } - } - return null; - } - - private void initProperties(MemberSelectTree tree, Integer find) { - if (tree != null) { - long start = pos.getStartPosition(root, tree); - long end = pos.getEndPosition(root, tree); - if (start <= find && find <= end) { - Element element = trees.getElement(trees.getPath(root, tree)); - - // Is this a static access? - this.isStatic = element instanceof TypeElement; - - // Find enclosing element to get the TypeElement from which this method is being - // called - this.enclosingElement = enclosingType(element); - - // find top level declaration to get the qualified name of the class - this.declaredInTopLevel = enclosingTopLevelType(enclosingElement); - - // Get the tree path of the enclosing element - this.enclosingTreePath = trees.getPath(enclosingElement); - - // Get the ClassTree of the enclosing element - // Will be needed in CreateMissingMethod.java for EditHelper - this.enclosingTree = enclosingClass(this.enclosingTreePath); - - this.isMemberSelect = - enclosingElement != null - && declaredInTopLevel != null - && enclosingTreePath != null - && enclosingTree != null; - } - } - } - - private ClassTree enclosingClass(TreePath path) { - while (path != null) { - if (path.getLeaf() instanceof ClassTree) { - return (ClassTree) path.getLeaf(); - } - - path = path.getParentPath(); - } - return null; - } - - private TypeElement enclosingTopLevelType(Element element) { - while (element != null) { - if (element instanceof TypeElement) { - TypeElement type = (TypeElement) element; - if (type.getNestingKind() == NestingKind.TOP_LEVEL) { - return type; - } - } - - element = element.getEnclosingElement(); - } - return null; - } - - private TypeElement enclosingType(Element element) { - while (element != null) { - if (element instanceof TypeElement) { - TypeElement type = (TypeElement) element; - final ClassTree tree = trees.getTree(type); - if (tree != null) { - return type; - } - } - - element = element.getEnclosingElement(); - } - return null; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java deleted file mode 100644 index c5a76f3bfe..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.lsp.java.compiler.CompileTask; -import com.itsaky.androidide.lsp.java.utils.FindHelper; -import jdkx.lang.model.element.Name; -import openjdk.source.tree.ClassTree; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.IdentifierTree; -import openjdk.source.tree.MemberReferenceTree; -import openjdk.source.tree.MemberSelectTree; -import openjdk.source.tree.MethodTree; -import openjdk.source.tree.NewClassTree; -import openjdk.source.tree.Tree; -import openjdk.source.tree.VariableTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreePath; -import openjdk.source.util.TreePathScanner; -import openjdk.source.util.Trees; - -public class FindNameAt extends TreePathScanner { - - private final JavacTask task; - private CompilationUnitTree root; - private ClassTree surroundingClass; - - public FindNameAt(CompileTask task) { - this.task = task.task; - } - - @Override - public TreePath reduce(TreePath r1, TreePath r2) { - if (r1 != null) return r1; - return r2; - } - - @Override - public TreePath visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - return super.visitCompilationUnit(t, find); - } - - @Override - public TreePath visitClass(ClassTree t, Long find) { - ClassTree push = surroundingClass; - surroundingClass = t; - if (contains(t, t.getSimpleName(), find)) { - surroundingClass = push; - return getCurrentPath(); - } - TreePath result = super.visitClass(t, find); - surroundingClass = push; - return result; - } - - @Override - public TreePath visitMethod(MethodTree t, Long find) { - Name name = t.getName(); - if (name.contentEquals("")) { - name = surroundingClass.getSimpleName(); - } - if (contains(t, name, find)) { - return getCurrentPath(); - } - return super.visitMethod(t, find); - } - - @Override - public TreePath visitVariable(VariableTree t, Long find) { - if (contains(t, t.getName(), find)) { - return getCurrentPath(); - } - return super.visitVariable(t, find); - } - - @Override - public TreePath visitNewClass(NewClassTree t, Long find) { - long start = Trees.instance(task).getSourcePositions().getStartPosition(root, t); - long end = start + "new".length(); - if (start <= find && find < end) { - return getCurrentPath(); - } - return super.visitNewClass(t, find); - } - - @Override - public TreePath visitMemberSelect(MemberSelectTree t, Long find) { - if (contains(t, t.getIdentifier(), find)) { - return getCurrentPath(); - } - return super.visitMemberSelect(t, find); - } - - @Override - public TreePath visitMemberReference(MemberReferenceTree t, Long find) { - if (contains(t, t.getName(), find)) { - return getCurrentPath(); - } - return super.visitMemberReference(t, find); - } - - @Override - public TreePath visitIdentifier(IdentifierTree t, Long find) { - if (contains(t, t.getName(), find)) { - return getCurrentPath(); - } - return super.visitIdentifier(t, find); - } - - private boolean contains(Tree t, CharSequence name, long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - int start = (int) pos.getStartPosition(root, t); - int end = (int) pos.getEndPosition(root, t); - if (start == -1 || end == -1) return false; - start = FindHelper.findNameIn(root, name, start, end); - end = start + name.length(); - if (start == -1 || end == -1) return false; - return start <= find && find < end; - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt deleted file mode 100644 index 9313f2ccb4..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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 androidx.core.util.Pair -import com.itsaky.androidide.models.Position -import com.itsaky.androidide.models.Range -import openjdk.source.tree.CompilationUnitTree -import openjdk.source.tree.LineMap -import openjdk.source.tree.MethodTree -import openjdk.source.util.TreePath -import openjdk.source.util.TreePathScanner -import openjdk.source.util.Trees -import openjdk.tools.javac.api.JavacTaskImpl -import org.slf4j.LoggerFactory - -/** - * Visits all methods and adds them to the given list of pair of method range and its tree. - * - * @author Akash Yadav - */ -class MethodRangeScanner(val task: JavacTaskImpl) : - TreePathScanner>>() { - - var root: CompilationUnitTree? = null - var lines: LineMap? = null - val pos = Trees.instance(task).sourcePositions - - companion object { - - private val log = LoggerFactory.getLogger(MethodRangeScanner::class.java) - } - - override fun visitCompilationUnit( - node: CompilationUnitTree?, - p: MutableList>? - ) { - this.root = node - this.lines = node?.lineMap - return super.visitCompilationUnit(node, p) - } - - override fun visitMethod(node: MethodTree?, list: MutableList>) { - // Do not call super.visitMethod - // We only want methods defined directly in declared (not anonymous) classes. - if (node == null || this.root == null) { - return - } - - val start = getStartPosition(node) - val end = getEndPosition(node) - - if (start == null || end == null) { - log.warn("Method '{}' skipped. Invalid position.", node.name) - return - } - - list.add(Pair.create(Range(start, end), currentPath)) - } - - fun getStartPosition(node: MethodTree): Position? { - val position = this.pos.getStartPosition(this.root!!, node) - if (position.toInt() == -1) { - return null - } - return getPosition(position) - } - - fun getEndPosition(node: MethodTree): Position? { - val position = this.pos.getEndPosition(this.root!!, node) - if (position.toInt() == -1) { - return null - } - return getPosition(position) - } - - fun getPosition(position: Long): Position { - val line = lines!!.getLineNumber(position).toInt() - val column = lines!!.getColumnNumber(position).toInt() - return Position(line, column).apply { index = position.toInt() } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java deleted file mode 100644 index ecb1ef5932..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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 static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; -import static com.itsaky.androidide.lsp.java.utils.JavaParserUtils.getSimpleName; - -import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.comments.Comment; -import com.github.javaparser.ast.expr.Name; -import com.github.javaparser.ast.expr.SimpleName; -import com.github.javaparser.ast.type.ClassOrInterfaceType; -import com.github.javaparser.printer.DefaultPrettyPrinterVisitor; -import com.github.javaparser.printer.SourcePrinter; -import com.github.javaparser.printer.configuration.ConfigurationOption; -import com.github.javaparser.printer.configuration.DefaultConfigurationOption; -import com.github.javaparser.printer.configuration.DefaultPrinterConfiguration; -import com.github.javaparser.printer.configuration.PrinterConfiguration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public class PrettyPrintingVisitor extends DefaultPrettyPrinterVisitor { - - public PrettyPrintingVisitor(PrinterConfiguration configuration) { - super(configuration); - } - - public PrettyPrintingVisitor(PrinterConfiguration configuration, SourcePrinter printer) { - super(configuration, printer); - } - - @Override - public void visit(Name n, Void arg) { - printOrphanCommentsBeforeThisChildNode(n); - printComment(n.getComment(), arg); - printer.print(n.getIdentifier()); - printOrphanCommentsEnding(n); - } - - @Override - public void visit(SimpleName n, Void arg) { - printOrphanCommentsBeforeThisChildNode(n); - printComment(n.getComment(), arg); - - String identifier = n.getIdentifier(); - printer.print(getSimpleName(identifier)); - } - - @Override - public void visit(ClassOrInterfaceType n, Void arg) { - printOrphanCommentsBeforeThisChildNode(n); - printComment(n.getComment(), arg); - - printAnnotations(n.getAnnotations(), false, arg); - - n.getName().accept(this, arg); - - if (n.isUsingDiamondOperator()) { - printer.print("<>"); - } else { - printTypeArgs(n, arg); - } - } - - protected void printOrphanCommentsBeforeThisChildNode(final Node node) { - if (!getOption(DefaultPrinterConfiguration.ConfigOption.PRINT_COMMENTS).isPresent()) return; - if (node instanceof Comment) return; - - Node parent = node.getParentNode().orElse(null); - if (parent == null) return; - List everything = new ArrayList<>(parent.getChildNodes()); - sortByBeginPosition(everything); - int positionOfTheChild = -1; - for (int i = 0; i < everything.size(); ++i) { // indexOf is by equality, so this - // is used to index by identity - if (everything.get(i) == node) { - positionOfTheChild = i; - break; - } - } - if (positionOfTheChild == -1) { - throw new AssertionError("I am not a child of my parent."); - } - int positionOfPreviousChild = -1; - for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { - if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; - } - for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { - Node nodeToPrint = everything.get(i); - if (!(nodeToPrint instanceof Comment)) - throw new RuntimeException( - "Expected comment, instead " - + nodeToPrint.getClass() - + ". Position of previous child: " - + positionOfPreviousChild - + ", position of child " - + positionOfTheChild); - nodeToPrint.accept(this, null); - } - } - - private Optional getOption( - DefaultPrinterConfiguration.ConfigOption cOption) { - return configuration.get(new DefaultConfigurationOption(cOption)); - } - - protected void printOrphanCommentsEnding(final Node node) { - if (!getOption(DefaultPrinterConfiguration.ConfigOption.PRINT_COMMENTS).isPresent()) return; - - List everything = new ArrayList<>(node.getChildNodes()); - sortByBeginPosition(everything); - if (everything.isEmpty()) { - return; - } - - int commentsAtEnd = 0; - boolean findingComments = true; - while (findingComments && commentsAtEnd < everything.size()) { - Node last = everything.get(everything.size() - 1 - commentsAtEnd); - findingComments = (last instanceof Comment); - if (findingComments) { - commentsAtEnd++; - } - } - for (int i = 0; i < commentsAtEnd; i++) { - everything.get(everything.size() - commentsAtEnd + i).accept(this, null); - } - } -} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java deleted file mode 100644 index f4e72ef228..0000000000 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 java.io.IOException; -import openjdk.source.tree.CompilationUnitTree; -import openjdk.source.tree.MethodTree; -import openjdk.source.util.JavacTask; -import openjdk.source.util.SourcePositions; -import openjdk.source.util.TreeScanner; -import openjdk.source.util.Trees; - -public class PruneMethodBodies extends TreeScanner { - private final JavacTask task; - private final StringBuilder buf = new StringBuilder(); - private CompilationUnitTree root; - - public PruneMethodBodies(JavacTask task) { - this.task = task; - } - - @Override - public StringBuilder reduce(StringBuilder a, StringBuilder b) { - return buf; - } - - @Override - public StringBuilder visitCompilationUnit(CompilationUnitTree t, Long find) { - root = t; - try { - CharSequence contents = t.getSourceFile().getCharContent(true); - buf.setLength(0); - buf.append(contents); - } catch (IOException e) { - throw new RuntimeException(e); - } - super.visitCompilationUnit(t, find); - return buf; - } - - @Override - public StringBuilder visitMethod(MethodTree t, Long find) { - SourcePositions pos = Trees.instance(task).getSourcePositions(); - if (t.getBody() == null) { - return buf; - } - long start = pos.getStartPosition(root, t.getBody()); - long end = pos.getEndPosition(root, t.getBody()); - if (!(start <= find && find < end)) { - for (int i = (int) start + 1; i < end - 1; i++) { - if (!Character.isWhitespace(buf.charAt(i))) { - buf.setCharAt(i, ' '); - } - } - return buf; - } - super.visitMethod(t, find); - return buf; - } -} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt deleted file mode 100644 index a0bd8e7e86..0000000000 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 com.google.common.truth.Truth.assertThat -import com.itsaky.androidide.lsp.java.JavaLSPTest -import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.DEFAULT_VALUE_STRING) -class AddImportTest { - - @Before - fun setup() { - JavaLSPTest.setup() - } - - @Suppress("UNCHECKED_CAST") - @Test - fun addImport() { - JavaLSPTest.apply { - openFile("actions/AddImportAction") - val diagnostic = - runBlocking { - server.analyze(file!!).diagnostics.firstOrNull { - it.code == "compiler.err.cant.resolve.location" - } - } - - assertThat(diagnostic).isNotNull() - - val file = this.file!!.toFile() - val data = createActionData(diagnostic!!, file, this.file!!, this.server) - - val action = AddImportAction() - action.prepare(data) - assertThat(action.visible).isTrue() - assertThat(action.enabled).isTrue() - - val execResult = runBlocking { action.execAction(data) } - assertThat(execResult::class.java).isAssignableTo(Pair::class.java) - - val result = execResult as Pair, *> - assertThat(result.first).contains("java.util.stream.Stream") - } - } -} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt deleted file mode 100644 index 0de93074bb..0000000000 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.truth.Truth.assertThat -import com.itsaky.androidide.lsp.java.JavaLSPTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import java.time.Instant - -/** @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) -class CompilerTest { - - @Before - fun setup() { - JavaLSPTest.setup() - } - - @Test - fun testMultipleThreads() { - JavaLSPTest.apply { - openFile("completion/MembersCompletionTest") - val threads = mutableListOf() - val thread = Thread { - getCompiler().compile(file!!).run { - println(Thread.currentThread().name) - delay(1000) - } - } - thread.name = "Long running task" - threads.add(thread) - thread.start() - - for (i in 0..300) { - val th = Thread { - getCompiler().compile(file!!).run { println(Thread.currentThread().name) } - } - th.name = "Thread #$i" - threads.add(th) - th.start() - } - - threads.forEach { it.join() } - } - } - - @Test - fun testClosedFileChannel() { - JavaLSPTest.apply { - openFile("completion/MembersCompletionTest") - - Thread { getCompiler().compile(file!!).run { delay(500) } }.start() - Thread { getCompiler().compile(file!!).run { delay(200) } }.start() - - getCompiler().compile(file!!).run { assertThat(it.diagnostics).isNotEmpty() } - } - } - - private fun delay(millis: Long) { - Thread.sleep(millis) - } - - @Test - fun testConcurrentAccess() { - JavaLSPTest.apply { - openFile("completion/MembersCompletionTest") - - var task = getCompiler().compile(file!!) - var fileObject = SourceFileObject(file!!) - val threads = mutableListOf() - for (i in 1..10) { - threads.add( - Thread { - task.run { - delay(100) - println(Thread.currentThread()) - } - } - .apply { name = "CompileTask Acessor #$i" } - ) - } - - fileObject = SourceFileObject(file!!, fileObject.contents, Instant.now()) - task = getCompiler().compile(listOf(fileObject)) - threads.forEach { it.start() } - - Thread { task.run { println("Writer thread") } }.start() - threads.forEach { it.join() } - } - } -} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoaderTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoaderTest.kt new file mode 100644 index 0000000000..8e72918c65 --- /dev/null +++ b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoaderTest.kt @@ -0,0 +1,53 @@ +/* + * 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.loader + +import android.content.Context +import io.mockk.mockk +import org.junit.Assert.assertNull +import org.junit.Test + +// getOrCreateSession() extracts a real carrier APK and DexClassLoader-loads it, neither of +// which works in this JVM unit-test environment (no carrier APK asset is present, and there's +// no on-device ART to load it into -- see JavaCompletionProviderTest's own comment for the same +// constraint). So this only covers close()/currentSession()'s contract when no session was ever +// created, not the getOrCreateSession()-vs-close() race ADFA-5053's review fixed by synchronizing +// close() -- that needs either a DI seam for the classloader construction or an on-device test. +class JavaCompilerLoaderTest { + private fun newLoader() = JavaCompilerLoader(mockk(relaxed = true)) + + @Test + fun `currentSession is null before any session is created`() { + assertNull(newLoader().currentSession()) + } + + @Test + fun `close before any session is created is a safe no-op`() { + val loader = newLoader() + loader.close() + assertNull(loader.currentSession()) + } + + @Test + fun `close is idempotent`() { + val loader = newLoader() + loader.close() + loader.close() + assertNull(loader.currentSession()) + } +} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt deleted file mode 100644 index 75ad2cb63e..0000000000 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * 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.partial - -import com.google.common.truth.Truth.assertThat -import com.itsaky.androidide.eventbus.events.editor.ChangeType.INSERT -import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent -import com.itsaky.androidide.lsp.java.JavaLSPTest -import com.itsaky.androidide.lsp.java.compiler.SourceFileObject -import com.itsaky.androidide.lsp.java.models.CompilationRequest -import com.itsaky.androidide.lsp.java.models.PartialReparseRequest -import com.itsaky.androidide.lsp.java.visitors.PrintingVisitor -import com.itsaky.androidide.models.Range -import jdkx.lang.model.type.ArrayType -import openjdk.source.tree.ExpressionStatementTree -import openjdk.source.tree.LiteralTree -import openjdk.source.tree.Tree -import openjdk.tools.javac.tree.JCTree.JCCompilationUnit -import openjdk.tools.javac.tree.JCTree.JCMethodDecl -import openjdk.tools.javac.tree.JCTree.JCMethodInvocation -import openjdk.tools.javac.tree.JCTree.JCVariableDecl -import openjdk.tools.javac.tree.TreeScanner -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config - -/** @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.DEFAULT_VALUE_STRING) -@Ignore("Partial reparser is currently unused") -class PartialReparserImplTest { - - @Before - fun setup() { - JavaLSPTest.setup() - } - - @Test - fun parseMethod() { - JavaLSPTest.apply { - openFile("partial/PartialReparserTest") - getCompiler().compile(file).run { task -> - AssertingScanner().scan(task.root() as JCCompilationUnit) - } - } - } - - @Test - fun testSimpleErrorneousStatement() { - JavaLSPTest.apply { - openFile("partial/PartialErrReparserTest") - getCompiler() - .compile( - CompilationRequest( - listOf(SourceFileObject(file)), - PartialReparseRequest(172, contents.toString()) - ) - ) - .run { PrintingVisitor().scan(it.root() as JCCompilationUnit) } - val changedText = contents!!.insert(192, "trim().").toString() - dispatchEvent( - DocumentChangeEvent( - file!!, - changedText, - changedText, - 2, - INSERT, - "trim().".length, - Range.NONE - ) - ) - getCompiler() - .compile( - CompilationRequest( - listOf(SourceFileObject(file)), - PartialReparseRequest(179, contents.toString()) - ) - ) - } - } - - class AssertingScanner : TreeScanner() { - private var methodCount = 0 - override fun visitMethodDef(tree: JCMethodDecl?) { - assertThat(tree).isNotNull() - tree!! - - if (tree.name.contentEquals("")) { - // Javac automatically adds the deafult constructor - methodCount++ - return super.visitMethodDef(tree) - } - - assertThat(methodCount).isEqualTo(1) - methodCount++ - - val params = tree.params - assertThat(params).isNotNull() - assertThat(params.size).isEqualTo(1) - - val argsParam = params[0] - assertThat(argsParam.name.toString()).isEqualTo("args") - assertThat(argsParam.type).isInstanceOf(ArrayType::class.java) - - val argType = argsParam.type as ArrayType - assertThat(argType.componentType.toString()).isEqualTo("java.lang.String") - - val body = tree.body - assertThat(body).isNotNull() - - val statements = body.statements - assertThat(statements).isNotNull() - assertThat(statements.size).isEqualTo(2) - - val println = statements[0] - assertThat(println).isNotNull() - assertThat(println.kind).isEqualTo(Tree.Kind.EXPRESSION_STATEMENT) - println as ExpressionStatementTree - - val arguments = (println.expression as JCMethodInvocation).arguments - assertThat(arguments).isNotNull() - assertThat(arguments.size).isEqualTo(1) - - val arg = arguments[0] - assertThat(arg).isNotNull() - assertThat(arg.kind).isEqualTo(Tree.Kind.STRING_LITERAL) - arg as LiteralTree - assertThat(arg.value).isEqualTo("Hello World!") - - val varDecl = statements[1] - assertThat(varDecl).isNotNull() - assertThat(varDecl.kind).isEqualTo(Tree.Kind.VARIABLE) - varDecl as JCVariableDecl - assertThat(varDecl.name.toString()).isEqualTo("klass") - - val type = varDecl.type - assertThat(type).isNotNull() - assertThat(type.tsym.qualifiedName.toString()).isEqualTo("java.lang.Class") - - val targs = type.typeArguments - assertThat(targs).isNotNull() - assertThat(targs.size).isEqualTo(1) - - val tOne = targs[0] - assertThat(tOne).isNotNull() - assertThat(tOne.tsym.qualifiedName.toString()).isEqualTo("java.lang.String") - - super.visitMethodDef(tree) - } - } -} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt deleted file mode 100644 index 70510ec5a1..0000000000 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.google.common.truth.Truth.assertThat -import com.itsaky.androidide.lsp.java.JavaLSPTest -import com.itsaky.androidide.lsp.models.CompletionParams -import com.itsaky.androidide.models.Position -import com.itsaky.androidide.progress.ICancelChecker -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -/** @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -class JavaCompletionProviderTest { - - @Before - fun setup() { - JavaLSPTest.setup() - } - - @Test - fun locals() { - JavaLSPTest.apply { - openFile("completion/LocalsCompletionTest") - - val pos = cursorPosition() - val items = completionTitles(pos) - assertThat(items).containsAtLeast("aaString", "aaInt", "aaFloat", "aaDouble", "args") - } - } - - fun members() { - JavaLSPTest.apply { - // Complete members of String - openFile("completion/MembersCompletionTest") - - val pos = cursorPosition() - val items = completionTitles(pos) - assertThat(items) - .containsAtLeast("getClass", "toLowerCase", "toUpperCase", "substring", "charAt") - } - } - - @Test - fun lambdaVariableMemberAccess() { - JavaLSPTest.apply { - // Complete members of Throwable - openFile("completion/LambdaMembersCompletionTest") - - val pos = cursorPosition() - val items = completionTitles(pos) - assertThat(items) - .containsAtLeast("getMessage", "getCause", "getStackTrace", "printStackTrace") - } - } - - @Test - fun staticAccess() { - JavaLSPTest.apply { - // Complete static members of String - openFile("completion/StaticMembersCompletionTest") - - val pos = cursorPosition() - val items = completionTitles(pos) - assertThat(items) - .containsAtLeast("format", "join", "valueOf", "CASE_INSENSITIVE_ORDER", "class") - } - } - - private fun completionTitles(pos: Position): List { - return JavaLSPTest.server - .complete( - CompletionParams(pos, JavaLSPTest.file!!, ICancelChecker.NOOP).apply { prefix = "" }) - .items - .map { it.ideLabel } - } -} diff --git a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt b/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt deleted file mode 100644 index cb891d9f14..0000000000 --- a/lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * 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.google.common.truth.Truth.assertThat -import com.itsaky.androidide.eventbus.events.editor.ChangeType.NEW_TEXT -import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent -import com.itsaky.androidide.lsp.java.JavaLSPTest -import com.itsaky.androidide.lsp.models.ExpandSelectionParams -import com.itsaky.androidide.models.Position -import com.itsaky.androidide.models.Range -import io.github.rosemoe.sora.text.Content -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -/** @author Akash Yadav */ -@RunWith(RobolectricTestRunner::class) -class JavaSelectionProviderTest { - - @Before - fun setup() { - JavaLSPTest.setup() - } - - @Test - fun testSimpleSelectionExpansion() { - JavaLSPTest.apply { - openFile("selection/SimpleSelectionExpansionTest") - cursor = requireCursor() - deleteCursorText() - dispatchEvent( - DocumentChangeEvent(file!!, contents.toString(), contents.toString(), 1, NEW_TEXT, 0, - Range.NONE)) - - val range = findRange() - val expanded = runBlocking { server.expandSelection(ExpandSelectionParams(file!!, range)) } - - assertThat(expanded).isEqualTo(Range(Position(4, 27), Position(4, 41))) - } - } - - @Test - fun testMethodSelection() { - JavaLSPTest.apply { - openFile("selection/MethodBodySelectionExpansionTest") - - val start = Position(3, 43) - val end = Position(5, 5) - val range = Range(start, end) - - val expanded = runBlocking { server.expandSelection(ExpandSelectionParams(file!!, range)) } - assertThat(expanded).isEqualTo(Range(Position(3, 4), end)) - } - } - - @Test - fun testTryCatchSelection() { - JavaLSPTest.apply { - openFile("selection/TrySelectionExpansionTest") - - // Test expand selection if catch block is selected - val start = Position(7, 10) - val end = Position(8, 9) - val range = Range(start, end) - - val expanded = runBlocking { server.expandSelection(ExpandSelectionParams(file!!, range)) } - assertThat(expanded).isEqualTo(Range(Position(4, 8), Position(10, 9))) - } - } - - @Test - fun testTryFinallySelection() { - JavaLSPTest.apply { - openFile("selection/TrySelectionExpansionTest") - - // Test expand selection if catch block is selected - val start = Position(8, 18) - val end = Position(10, 9) - val range = Range(start, end) - - val expanded = runBlocking { server.expandSelection(ExpandSelectionParams(file!!, range)) } - assertThat(expanded).isEqualTo(Range(Position(4, 8), Position(10, 9))) - } - } - - private fun findRange(): Range { - val pos = Content(JavaLSPTest.contents!!).indexer.getCharPosition(JavaLSPTest.cursor) - val position = Position(pos.line, pos.column, pos.index) - return Range(position, position) - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 29fb8afcd8..ddd8cddb9c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -133,6 +133,8 @@ include( ":lsp:models", ":lsp:indexing", ":lsp:java", + ":lsp:java-api", + ":lsp:java-compiler-impl", ":lsp:jvm-symbol-index", ":lsp:jvm-symbol-models", ":lsp:kotlin", @@ -146,6 +148,8 @@ include( ":subprojects:framework-stubs", ":subprojects:hidden-apis", ":subprojects:hidden-apis-compat", + ":subprojects:java-compiler-carrier", + ":subprojects:javac-fs", ":subprojects:javac-services", ":subprojects:kotlin-analysis-api", ":subprojects:libjdwp", diff --git a/subprojects/java-compiler-carrier/build.gradle.kts b/subprojects/java-compiler-carrier/build.gradle.kts new file mode 100644 index 0000000000..365069ec6a --- /dev/null +++ b/subprojects/java-compiler-carrier/build.gradle.kts @@ -0,0 +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 . + */ + +import com.android.build.api.artifact.SingleArtifact +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.application") + id("kotlin-android") +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.javacompilercarrier" + + // This APK is never installed -- only its classes.dex is read, via DexClassLoader, by + // JavaCompilerLoader in lsp:java. Shrinking/obfuscating it would require porting over the + // javac fork's own keep rules for no benefit (see ADFA-3604 -- the same "ship intact rather + // than shrink" call already made for the analogous Kotlin carrier, ADFA-5010). + buildTypes { + release { + isMinifyEnabled = false + isShrinkResources = false + } + } +} + +dependencies { + implementation(projects.lsp.javaCompilerImpl) +} + +// Exposes the release variant's real APK output directory as a Provider, for app's +// copyJavaCompilerCarrierToAssets task -- consuming this via AGP's variant artifacts API (rather +// than a hardcoded path guessing the output filename) ties Gradle's dependency tracking to the +// actual producing task (packageV8Release), not just dependsOn ordering, which intermittently +// raced the file's own write-to-disk on some CI runs (ADFA-5053). +androidComponents { + onVariants(selector().withBuildType("release")) { variant -> + extensions.extraProperties["releaseApkOutputDir"] = variant.artifacts.get(SingleArtifact.APK) + } +} diff --git a/subprojects/java-compiler-carrier/src/main/AndroidManifest.xml b/subprojects/java-compiler-carrier/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..da447d7adb --- /dev/null +++ b/subprojects/java-compiler-carrier/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/subprojects/javac-fs/build.gradle.kts b/subprojects/javac-fs/build.gradle.kts new file mode 100644 index 0000000000..89a8308778 --- /dev/null +++ b/subprojects/javac-fs/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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}.javac.fs" +} + +dependencies { + implementation(libs.common.kotlin) + implementation(projects.common) + implementation(projects.logger) + + // Resident, shared across the resident app and the isolated javac carrier -- this module + // must never be duplicated into the carrier's own dex. See docs/adr/0012. + api(libs.composite.javaCompiler) +} diff --git a/subprojects/javac-fs/src/main/AndroidManifest.xml b/subprojects/javac-fs/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..91bff55d11 --- /dev/null +++ b/subprojects/javac-fs/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt similarity index 80% rename from subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt rename to subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt index 360d933927..be49dea750 100644 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt +++ b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt @@ -22,16 +22,13 @@ import java.nio.file.spi.FileSystemProvider /** @author Akash Yadav */ object AndroidFsProviderImpl : AndroidFsProvider() { + init { + INSTANCE = this + } - init { - INSTANCE = this - } + fun init() { + // Used to instantiate this class and override the AndroidFsProvider instance + } - fun init() { - // Used to instantiate this class and override the AndroidFsProvider instance - } - - override fun zipFsProvider(): FileSystemProvider { - return CachingJarFileSystemProvider - } + override fun zipFsProvider(): FileSystemProvider = CachingJarFileSystemProvider } diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt similarity index 58% rename from subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt rename to subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt index aa7f660b32..e109540ddd 100644 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt +++ b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt @@ -28,35 +28,36 @@ import java.nio.file.Path * @author Akash Yadav */ object CacheFSInfoSingleton : CacheFSInfo() { - - const val TEST_PROP_ENABLED_ON_JVM = "ide.testing.javac.fsCache.isEnabledOnJVM" - private val log = LoggerFactory.getLogger(CacheFSInfoSingleton::class.java) - - /** - * Caches information about the given [Path]. - */ - @JvmOverloads - fun cache(file: Path, cacheJarClasspath: Boolean = true) { - - if (System.getProperty(TEST_PROP_ENABLED_ON_JVM, null) != "true") { - if (VMUtils.isJvm) { - return - } - } - - try { - // Cache canonical path - getCanonicalFile(file) - - // Cache attributes - getAttributes(file) - - // Cache jar classpath if requested - if (cacheJarClasspath) { - getJarClassPath(file) - } - } catch (err: Throwable) { - log.warn("Failed to cache jar file: {}", file, err) - } - } -} \ No newline at end of file + const val TEST_PROP_ENABLED_ON_JVM = "ide.testing.javac.fsCache.isEnabledOnJVM" + private val log = LoggerFactory.getLogger(CacheFSInfoSingleton::class.java) + + /** + * Caches information about the given [Path]. + */ + @JvmOverloads + fun cache( + file: Path, + cacheJarClasspath: Boolean = true, + ) { + if (System.getProperty(TEST_PROP_ENABLED_ON_JVM, null) != "true") { + if (VMUtils.isJvm) { + return + } + } + + try { + // Cache canonical path + getCanonicalFile(file) + + // Cache attributes + getAttributes(file) + + // Cache jar classpath if requested + if (cacheJarClasspath) { + getJarClassPath(file) + } + } catch (err: Throwable) { + log.warn("Failed to cache jar file: {}", file, err) + } + } +} diff --git a/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt new file mode 100644 index 0000000000..c2cf7d3f8f --- /dev/null +++ b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt @@ -0,0 +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.javac.services.fs + +import com.itsaky.androidide.zipfs2.ZipFileSystem +import com.itsaky.androidide.zipfs2.ZipFileSystemProvider +import jdkx.lang.model.SourceVersion +import openjdk.tools.javac.file.RelativePath.RelativeDirectory +import org.slf4j.LoggerFactory +import java.io.IOException +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap + +/** + * A cached file system for JAR files. + * + * @author Akash Yadav + */ +class CachedJarFileSystem( + provider: ZipFileSystemProvider?, + zfpath: Path?, + env: MutableMap?, +) : ZipFileSystem(provider, zfpath, env) { + companion object { + private val log = LoggerFactory.getLogger(CachedJarFileSystem::class.java) + } + + // ConcurrentHashMap: written by resident classpath indexing (JarFsClasspathReader) and read + // by the isolated compiler (JarPackageProviderImpl.getPackages, which returns this same live + // map) through the shared CachingJarFileSystemProvider singleton -- across both threads and, + // since ADFA-5053, the resident/isolated classloader boundary too. + internal val packages: MutableMap = ConcurrentHashMap() + + override fun close() { + // Do nothing + // This is called manually by the Java LSP + } + + @Throws(IOException::class) + fun doClose() { + try { + super.close() + } catch (e: IOException) { + log.warn("IOException during CachedJarFileSystem close", e) + } catch (e: java.io.UncheckedIOException) { + log.warn("UncheckedIOException during CachedJarFileSystem close", e) + } + } + + fun storeJARPackageDir(dir: Path?): Boolean { + if (isValid(dir?.fileName)) { + packages[RelativeDirectory(rootDir.relativize(dir!!).toString())] = dir + return true + } + + return false + } + + private fun isValid(fileName: Path?): Boolean = + if (fileName == null) { + true + } else { + var name = fileName.toString() + if (name.endsWith("/")) { + name = name.substring(0, name.length - 1) + } + SourceVersion.isIdentifier(name) + } +} diff --git a/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt new file mode 100644 index 0000000000..6194028655 --- /dev/null +++ b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt @@ -0,0 +1,103 @@ +/* + * 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.javac.services.fs + +import com.itsaky.androidide.zipfs2.JarFileSystemProvider +import com.itsaky.androidide.zipfs2.ZipFileSystem +import org.slf4j.LoggerFactory +import java.nio.file.FileSystem +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap +import kotlin.io.path.pathString + +/** + * An implementation of [JarFileSystemProvider] that caches the created [CachedJarFileSystem] so + * that it can be (re)used in multiple compilations. + * + * @author Akash Yadav + */ +object CachingJarFileSystemProvider : JarFileSystemProvider() { + private val cachedFs = ConcurrentHashMap() + + private val log = LoggerFactory.getLogger(CachingJarFileSystemProvider::class.java) + + override fun createFs( + path: Path, + env: MutableMap?, + ): ZipFileSystem { + val cached = cachedFs[path.normalize().pathString] + if (cached != null) { + return cached + } + return createAndCache(path, env) + } + + fun newFileSystem(path: Path): FileSystem? = newFileSystem(path, mutableMapOf()) + + fun clearCache() { + cachedFs.values.forEach(this::closeFs) + cachedFs.clear() + } + + fun clearCaches(predicate: (Path) -> Boolean) = clearCachesForPaths { predicate(Paths.get(it)) } + + fun clearCachesForPaths(predicate: (String) -> Boolean) { + val toRemove = + this.cachedFs.keys.mapNotNull { + return@mapNotNull if (predicate(it)) { + it + } else { + null + } + } + + if (toRemove.isNotEmpty()) { + toRemove.forEach(this::clearCache) + } + } + + fun clearCache(path: Path) { + clearCache(path.normalize().pathString) + } + + fun clearCache(path: String) { + val fs = cachedFs.remove(path) + if (fs != null) { + log.debug("Clearing cached JAR file system for path: {}", path) + closeFs(fs) + } + } + + private fun closeFs(fs: CachedJarFileSystem) { + try { + fs.doClose() + } catch (err: Throwable) { + log.error("Failed to close cached zip file system: {}", fs, err) + } + } + + private fun createAndCache( + path: Path, + env: MutableMap?, + ): CachedJarFileSystem { + val fs = CachedJarFileSystem(this, path, env) + cachedFs[path.normalize().pathString] = fs + return fs + } +} diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt similarity index 83% rename from subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt rename to subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt index 0606a5b523..3c9aea08da 100644 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt +++ b/subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt @@ -27,8 +27,8 @@ import java.nio.file.Path * @author Akash Yadav */ object JarPackageProviderImpl : JarPackageProvider { - override fun getPackages(archivePath: Path): MutableMap { - val fs = CachingJarFileSystemProvider.newFileSystem(archivePath) as CachedJarFileSystem - return fs.packages - } + override fun getPackages(archivePath: Path): MutableMap { + val fs = CachingJarFileSystemProvider.newFileSystem(archivePath) as CachedJarFileSystem + return fs.packages + } } diff --git a/subprojects/javac-services/build.gradle.kts b/subprojects/javac-services/build.gradle.kts index e4660205e8..ab698259d4 100644 --- a/subprojects/javac-services/build.gradle.kts +++ b/subprojects/javac-services/build.gradle.kts @@ -21,7 +21,15 @@ dependencies { implementation(projects.common) implementation(projects.logger) - api(libs.composite.javac) + // The actual javac fork this module wraps -- bundled with this module wherever it ends up + // (isolated carrier, per ADFA-5053). + api(libs.composite.jdkCompiler) + + // Resident (see docs/adr/0012): must never be duplicated into the isolated carrier, so + // these are compileOnly even though this module's own code (ReusableContext.kt, etc.) + // references their types directly. + compileOnly(libs.composite.javaCompiler) + compileOnly(projects.subprojects.javacFs) testImplementation(libs.tests.junit) testImplementation(libs.tests.google.truth) diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/compiler/ReusableContext.kt b/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/compiler/ReusableContext.kt index 68c039ea2c..e31bd078d9 100644 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/compiler/ReusableContext.kt +++ b/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/compiler/ReusableContext.kt @@ -61,90 +61,100 @@ import java.net.URI /** * Reusable [Context] for [ReusableCompiler]. * + * This class (isolated, carrier-dexed) extends [Context] (resident, per ADR 0012's leaf-class + * relocation) and accesses its `protected ht`/`key()` members below via implicit `this` -- + * unlike the three same-package-*sibling* cross-classloader accesses ADR 0012 found and fixed + * (`CacheFSInfo.getAttributes`/`RelativeFile.forClass`/`RelativeDirectory.forPackage`), this is + * protected access via *inheritance*, governed by a different rule (JVMS 5.4.4's second + * `protected` bullet: access is allowed when made through a reference typed as the accessing + * subclass itself, with no runtime-package/classloader-identity condition attached at all) -- + * so, unlike those three, this one doesn't need `Context.ht`/`key()` widened to `public`. + * * @author Akash Yadav */ -class ReusableContext(cancelService: CancelService) : Context(), TaskListener { - - private val flowCompleted = mutableSetOf() - - init { - put(Log.logKey, ReusableLog.factory) - put(FSInfo::class.java, if (VMUtils.isJvm) CacheFSInfo() else CacheFSInfoSingleton) - put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory) - put(JavacFlowListener.flowListenerKey, JavacFlowListener { this.hasFlowCompleted(it) }) - put(JarPackageProvider::class.java, JarPackageProviderImpl) - - NBAttr.preRegister(this) - NBParserFactory.preRegister(this) - NBTreeMaker.preRegister(this) - NBJavacTrees.preRegister(this) - NBResolve.preRegister(this) - NBEnter.preRegister(this) - NBMemberEnter.preRegister(this, false) - NBClassFinder.preRegister(this) - NBClassReader.preRegister(this) - CancelService.preRegister(this, cancelService) - } - - @DefinedBy(COMPILER_TREE) - override fun started(e: TaskEvent) { - // log.debug("Started: $e") - // Do nothing - } - - @DefinedBy(COMPILER_TREE) - override fun finished(e: TaskEvent) { - if (e.kind == ANALYZE) { - val cu = e.compilationUnit as JCCompilationUnit - if (cu.sourcefile != null) { - flowCompleted.add(cu.sourcefile.toUri()) - } - } - } - - fun clear() { - drop(Arguments.argsKey) - drop(DiagnosticListener::class.java) - drop(Log.outKey) - drop(Log.errKey) - drop(JavaFileManager::class.java) - drop(JavacTask::class.java) - drop(JavacTrees::class.java) - drop(JavacElements::class.java) - - if (ht[Log.logKey] is ReusableLog) { - // log already init-ed - not first round - (Log.instance(this) as ReusableLog).clear() - Enter.instance(this).newRound() - (JavaCompiler.instance(this) as ReusableJavaCompiler).clear() - Types.instance(this).newRound() - Check.instance(this).newRound() - Modules.instance(this).newRound() - Annotate.instance(this).newRound() - CompileStates.instance(this).clear() - MultiTaskListener.instance(this).clear() - } - } - - /** **FOR INTERNAL USE ONLY!** */ - fun drop(k: Key?) { - ht.remove(k) - } - - /** **FOR INTERNAL USE ONLY!** */ - fun drop(c: Class?) { - drop(key(c)) - } - - private fun hasFlowCompleted(fo: JavaFileObject?): Boolean { - return if (fo == null) { - false - } else { - try { - this.flowCompleted.contains(fo.toUri()) - } catch (e: Exception) { - false - } - } - } +class ReusableContext( + cancelService: CancelService, +) : Context(), + TaskListener { + private val flowCompleted = mutableSetOf() + + init { + put(Log.logKey, ReusableLog.factory) + put(FSInfo::class.java, if (VMUtils.isJvm) CacheFSInfo() else CacheFSInfoSingleton) + put(JavaCompiler.compilerKey, ReusableJavaCompiler.factory) + put(JavacFlowListener.flowListenerKey, JavacFlowListener { this.hasFlowCompleted(it) }) + put(JarPackageProvider::class.java, JarPackageProviderImpl) + + NBAttr.preRegister(this) + NBParserFactory.preRegister(this) + NBTreeMaker.preRegister(this) + NBJavacTrees.preRegister(this) + NBResolve.preRegister(this) + NBEnter.preRegister(this) + NBMemberEnter.preRegister(this, false) + NBClassFinder.preRegister(this) + NBClassReader.preRegister(this) + CancelService.preRegister(this, cancelService) + } + + @DefinedBy(COMPILER_TREE) + override fun started(e: TaskEvent) { + // log.debug("Started: $e") + // Do nothing + } + + @DefinedBy(COMPILER_TREE) + override fun finished(e: TaskEvent) { + if (e.kind == ANALYZE) { + val cu = e.compilationUnit as JCCompilationUnit + if (cu.sourcefile != null) { + flowCompleted.add(cu.sourcefile.toUri()) + } + } + } + + fun clear() { + drop(Arguments.argsKey) + drop(DiagnosticListener::class.java) + drop(Log.outKey) + drop(Log.errKey) + drop(JavaFileManager::class.java) + drop(JavacTask::class.java) + drop(JavacTrees::class.java) + drop(JavacElements::class.java) + + if (ht[Log.logKey] is ReusableLog) { + // log already init-ed - not first round + (Log.instance(this) as ReusableLog).clear() + Enter.instance(this).newRound() + (JavaCompiler.instance(this) as ReusableJavaCompiler).clear() + Types.instance(this).newRound() + Check.instance(this).newRound() + Modules.instance(this).newRound() + Annotate.instance(this).newRound() + CompileStates.instance(this).clear() + MultiTaskListener.instance(this).clear() + } + } + + /** **FOR INTERNAL USE ONLY!** */ + fun drop(k: Key?) { + ht.remove(k) + } + + /** **FOR INTERNAL USE ONLY!** */ + fun drop(c: Class?) { + drop(key(c)) + } + + private fun hasFlowCompleted(fo: JavaFileObject?): Boolean = + if (fo == null) { + false + } else { + try { + this.flowCompleted.contains(fo.toUri()) + } catch (e: Exception) { + false + } + } } diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt b/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt deleted file mode 100644 index e11f4b3187..0000000000 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.javac.services.fs - -import com.itsaky.androidide.zipfs2.ZipFileSystem -import com.itsaky.androidide.zipfs2.ZipFileSystemProvider -import jdkx.lang.model.SourceVersion -import openjdk.tools.javac.file.RelativePath.RelativeDirectory -import org.slf4j.LoggerFactory -import java.io.IOException -import java.nio.file.Path - -/** - * A cached file system for JAR files. - * - * @author Akash Yadav - */ -class CachedJarFileSystem( - provider: ZipFileSystemProvider?, - zfpath: Path?, - env: MutableMap? -) : ZipFileSystem(provider, zfpath, env) { - - companion object { - private val log = LoggerFactory.getLogger(CachedJarFileSystem::class.java) - } - - internal val packages = mutableMapOf() - - override fun close() { - // Do nothing - // This is called manually by the Java LSP - } - - @Throws(IOException::class) - fun doClose() { - try { - super.close() - } catch (e: IOException) { - log.warn("IOException during CachedJarFileSystem close", e) - } catch (e: java.io.UncheckedIOException) { - log.warn("UncheckedIOException during CachedJarFileSystem close", e) - } - } - - fun storeJARPackageDir(dir: Path?): Boolean { - if (isValid(dir?.fileName)) { - packages[RelativeDirectory(rootDir.relativize(dir!!).toString())] = dir - return true - } - - return false - } - - private fun isValid(fileName: Path?): Boolean { - return if (fileName == null) { - true - } else { - var name = fileName.toString() - if (name.endsWith("/")) { - name = name.substring(0, name.length - 1) - } - SourceVersion.isIdentifier(name) - } - } -} diff --git a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt b/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt deleted file mode 100644 index 8edfaf87a0..0000000000 --- a/subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.javac.services.fs - -import com.itsaky.androidide.zipfs2.JarFileSystemProvider -import com.itsaky.androidide.zipfs2.ZipFileSystem -import org.slf4j.LoggerFactory -import java.nio.file.FileSystem -import java.nio.file.Path -import java.nio.file.Paths -import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.pathString - -/** - * An implementation of [JarFileSystemProvider] that caches the created [CachedJarFileSystem] so - * that it can be (re)used in multiple compilations. - * - * @author Akash Yadav - */ -object CachingJarFileSystemProvider : JarFileSystemProvider() { - private val cachedFs = ConcurrentHashMap() - - private val log = LoggerFactory.getLogger(CachingJarFileSystemProvider::class.java) - - override fun createFs(path: Path, env: MutableMap?): ZipFileSystem { - val cached = cachedFs[path.normalize().pathString] - if (cached != null) { - return cached - } - return createAndCache(path, env) - } - - fun newFileSystem(path: Path): FileSystem? { - return newFileSystem(path, mutableMapOf()) - } - - fun clearCache() { - cachedFs.values.forEach(this::closeFs) - cachedFs.clear() - } - - fun clearCaches(predicate: (Path) -> Boolean) { - return clearCachesForPaths { predicate(Paths.get(it)) } - } - - fun clearCachesForPaths(predicate: (String) -> Boolean) { - val toRemove = - this.cachedFs.keys.mapNotNull { - return@mapNotNull if (predicate(it)) { - it - } else null - } - - if (toRemove.isNotEmpty()) { - toRemove.forEach(this::clearCache) - } - } - - fun clearCache(path: Path) { - clearCache(path.normalize().pathString) - } - - fun clearCache(path: String) { - val fs = cachedFs.remove(path) - if (fs != null) { - log.debug("Clearing cached JAR file system for path: {}", path) - closeFs(fs) - } - } - - private fun closeFs(fs: CachedJarFileSystem) { - try { - fs.doClose() - } catch (err: Throwable) { - log.error("Failed to close cached zip file system: {}", fs, err) - } - } - - private fun createAndCache(path: Path, env: MutableMap?): CachedJarFileSystem { - val fs = CachedJarFileSystem(this, path, env) - cachedFs[path.normalize().pathString] = fs - return fs - } -} diff --git a/subprojects/projects/build.gradle.kts b/subprojects/projects/build.gradle.kts index 9d2683ae80..79f68d6e26 100644 --- a/subprojects/projects/build.gradle.kts +++ b/subprojects/projects/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { implementation(projects.logger) implementation(projects.lookup) implementation(projects.shared) - implementation(projects.subprojects.javacServices) + implementation(projects.subprojects.javacFs) implementation(projects.subprojects.xmlUtils) implementation(libs.common.io)