From 6e6351aff6d21225a4ebd8e42d8a920bca03e651 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 17:53:25 -0700 Subject: [PATCH 01/21] ADFA-5053: Move javac's fs-adjacent leaf classes into java-compiler CacheFSInfo/FSInfo/RelativePath/Context/PlatformUtils/Assert (package unchanged) need to stay resident so subprojects/projects can keep using them for classpath-jar indexing, while the rest of jdk-compiler's ~400 files (parser/Attr/Resolve/Symtab/Types/codegen) move into an isolated, DexClassLoader-loaded carrier in a later commit. Mechanical move only: jdk-compiler already depends on java-compiler (api(projects.buildDeps.javaCompiler)), so relocating files the other direction can't create a cycle, and every dependency of these six classes (java.util/nio, jdkx.tools.JavaFileObject, zipfs2's AndroidFsProvider) is already satisfiable from java-compiler alone. Without this move, isolating the rest of jdk-compiler would either duplicate these classes (a resident copy plus a carrier-dex copy) or strand them where subprojects/projects can't reach them. Verified :build-deps:java-compiler, :build-deps:jdk-compiler, :subprojects:javac-services, :subprojects:projects, and :lsp:java all still compile. Co-Authored-By: Claude Sonnet 5 --- .../src/main/java/openjdk/tools/javac/file/CacheFSInfo.java | 0 .../src/main/java/openjdk/tools/javac/file/FSInfo.java | 0 .../src/main/java/openjdk/tools/javac/file/RelativePath.java | 0 .../src/main/java/openjdk/tools/javac/util/Assert.java | 0 .../src/main/java/openjdk/tools/javac/util/Context.java | 0 .../src/main/java/openjdk/tools/javac/util/PlatformUtils.java | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/file/CacheFSInfo.java (100%) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/file/FSInfo.java (100%) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/file/RelativePath.java (100%) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/util/Assert.java (100%) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/util/Context.java (100%) rename composite-builds/build-deps/{jdk-compiler => java-compiler}/src/main/java/openjdk/tools/javac/util/PlatformUtils.java (100%) 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 100% 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 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 100% 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 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 From 87b158151db80e44744722e45d5926e3b6540052 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 17:59:30 -0700 Subject: [PATCH 02/21] ADFA-5053: Extract subprojects/javac-fs as its own resident module subprojects/projects (the foundational, always-resident project-model module used by every project regardless of language) depends on javac's file-system caching wrappers (CacheFSInfoSingleton, CachedJarFileSystem, CachingJarFileSystemProvider, JarPackageProviderImpl, AndroidFsProviderImpl) for live classpath-jar indexing. That has to stay resident even after the rest of javac's fork moves into an isolated DexClassLoader carrier in a later commit, so it needs its own module rather than living inside javac-services (which is becoming the heavy, isolated payload). Package name (com.itsaky.androidide.javac.services.fs) is unchanged, so no source outside build.gradle.kts files needed touching. javac-services depends on it directly now (previously that was implicit, via the two sharing one module); subprojects/projects and lsp/java depend on it directly too instead of transitively through javac-services. Verified :subprojects:javac-fs, :subprojects:javac-services, :subprojects:projects, and :lsp:java all compile, and existing unit tests for :subprojects:projects and :subprojects:javac-services pass. Co-Authored-By: Claude Sonnet 5 --- gradle/libs.versions.toml | 2 + lsp/java/build.gradle.kts | 1 + settings.gradle.kts | 1 + subprojects/javac-fs/build.gradle.kts | 37 +++++++++++++++++++ .../javac-fs/src/main/AndroidManifest.xml | 18 +++++++++ .../services/fs/AndroidFsProviderImpl.kt | 0 .../javac/services/fs/CacheFSInfoSingleton.kt | 0 .../javac/services/fs/CachedJarFileSystem.kt | 0 .../fs/CachingJarFileSystemProvider.kt | 0 .../services/fs/JarPackageProviderImpl.kt | 0 subprojects/javac-services/build.gradle.kts | 1 + subprojects/projects/build.gradle.kts | 2 +- 12 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 subprojects/javac-fs/build.gradle.kts create mode 100644 subprojects/javac-fs/src/main/AndroidManifest.xml rename subprojects/{javac-services => javac-fs}/src/main/java/com/itsaky/androidide/javac/services/fs/AndroidFsProviderImpl.kt (100%) rename subprojects/{javac-services => javac-fs}/src/main/java/com/itsaky/androidide/javac/services/fs/CacheFSInfoSingleton.kt (100%) rename subprojects/{javac-services => javac-fs}/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt (100%) rename subprojects/{javac-services => javac-fs}/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt (100%) rename subprojects/{javac-services => javac-fs}/src/main/java/com/itsaky/androidide/javac/services/fs/JarPackageProviderImpl.kt (100%) 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/java/build.gradle.kts b/lsp/java/build.gradle.kts index 70b545f02d..cba21e3e77 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(projects.lsp.api) implementation(projects.lsp.jvmSymbolIndex) implementation(projects.subprojects.libjdwp) + implementation(projects.subprojects.javacFs) implementation(projects.subprojects.javacServices) implementation(projects.idetooltips) diff --git a/settings.gradle.kts b/settings.gradle.kts index 29fb8afcd8..7592064e23 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -146,6 +146,7 @@ include( ":subprojects:framework-stubs", ":subprojects:hidden-apis", ":subprojects:hidden-apis-compat", + ":subprojects:javac-fs", ":subprojects:javac-services", ":subprojects:kotlin-analysis-api", ":subprojects:libjdwp", 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 100% 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 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 100% 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 diff --git a/subprojects/javac-services/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 similarity index 100% rename from subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt rename to subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachedJarFileSystem.kt diff --git a/subprojects/javac-services/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 similarity index 100% rename from subprojects/javac-services/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt rename to subprojects/javac-fs/src/main/java/com/itsaky/androidide/javac/services/fs/CachingJarFileSystemProvider.kt 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 100% 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 diff --git a/subprojects/javac-services/build.gradle.kts b/subprojects/javac-services/build.gradle.kts index e4660205e8..f80811b9e6 100644 --- a/subprojects/javac-services/build.gradle.kts +++ b/subprojects/javac-services/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(libs.google.guava) implementation(projects.common) implementation(projects.logger) + implementation(projects.subprojects.javacFs) api(libs.composite.javac) 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) From 73b9919e4a2bf43eacfe01961e61a312223c7d49 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 15:46:25 -0700 Subject: [PATCH 03/21] ADFA-5052: Defer JavaCompilerService/SourceFileManager construction until a real .java file is touched DefaultLanguageServerRegistry.onProjectInitialized dispatches setupWithProject to every registered language server unconditionally, regardless of project language. JavaLanguageServer.setupWithProject() referenced JavaCompilerService.NO_MODULE_COMPILER and called SourceFileManager.clearCache(), both of which trigger class-init that eagerly constructs real javac Context/ JavacFileManager machinery plus a full android.jar top-level-class scan -- on the first project open in the app's lifetime, Kotlin-only projects included. shutdown() had the same problem in reverse, on every project close. Same eager-load bug pattern ADFA-5010 fixed for the Kotlin Analysis API, and independently confirmed and sized (openjdk.tools.javac ~2,238 classes, ~3.7MB) while researching whether javac could get ADFA-5010's carrier-APK treatment. This fix is scoped to just the eager-construction bug -- no DexClassLoader/ carrier-APK split; javac/jdk-compiler stay in the main dex, just constructed lazily. setupWithProject() now only stashes the workspace; the actual reset (destroy NO_MODULE_COMPILER, clear file-manager/JAR-fs caches, index module classpaths) is deferred to ensureProjectReset(), called from getCompiler() and onContentChange() -- both already gated on DocumentUtils.isJavaFile(), so this now only runs on genuine Java-file interaction. shutdown() skips its javac-specific cleanup entirely if that never happened. Per-file LSP dispatch methods (complete/findReferences/findDefinition/expandSelection/signatureHelp) needed no changes: the editor's IDELanguage already resolves one language server per file before calling any of them, so they were never the source of the cross-language trigger. Verified: :lsp:java unit tests pass, :app:assembleV8Debug builds clean. --- .../androidide/lsp/java/JavaLanguageServer.kt | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) 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..f8af6393f2 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 @@ -96,6 +96,15 @@ class JavaLanguageServer : ILanguageServer { private val timer = AnalyzeTimer { analyzeSelected() } private var cachedCompletion: CachedCompletion + // Set by setupWithProject(), consumed by ensureProjectReset() on the first real .java-file + // interaction after it -- deferred because setupWithProject() is called for every project + // open regardless of language (ADFA-5052). + @Volatile + private var pendingWorkspace: Workspace? = null + + @Volatile + private var javaCompilerInitialized = false + val settings: IServerSettings get() { return _settings ?: JavaServerSettings @@ -123,10 +132,10 @@ 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), ) JavaSnippetRepository.init() @@ -134,10 +143,12 @@ class JavaLanguageServer : ILanguageServer { override fun shutdown() { (this.debugAdapter as? AutoCloseable?)?.close() - JavaCompilerProvider.getInstance().destroy() - SourceFileManager.clearCache() - CacheFSInfoSingleton.clearCache() - clearCache() + if (javaCompilerInitialized) { + JavaCompilerProvider.getInstance().destroy() + SourceFileManager.clearCache() + CacheFSInfoSingleton.clearCache() + clearCache() + } EventBus.getDefault().unregister(this) timer.cancel() } @@ -163,10 +174,35 @@ class JavaLanguageServer : ILanguageServer { override fun setupWithProject(workspace: Workspace) { LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu) - (ProjectManagerImpl.getInstance() - .indexingServiceManager - .getService(JvmLibraryIndexingService.ID) as? JvmLibraryIndexingService?) - ?.refresh() + ( + 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 JavaCompilerService.NO_MODULE_COMPILER / SourceFileManager.NO_MODULE eagerly + // construct real javac machinery plus a full android.jar scan at class-init, merely by + // being referenced (ADFA-5052, mirrors ADFA-5010's KotlinLanguageServer fix). + pendingWorkspace = workspace + } + + /** + * 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. + * No-ops if already up to date. + */ + private fun ensureProjectReset() { + if (pendingWorkspace == null) return + val workspace: Workspace + synchronized(this) { + workspace = pendingWorkspace ?: return + pendingWorkspace = null + javaCompilerInitialized = true + } // Once we have project initialized // Destory the NO_MODULE_COMPILER instance @@ -196,8 +232,7 @@ class JavaLanguageServer : ILanguageServer { override fun complete(params: CompletionParams?): CompletionResult { val compiler = getCompiler(params!!.file) - if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file) - ) { + if (!settings.completionsEnabled() || !completionProvider.canComplete(params.file)) { return CompletionResult.EMPTY } @@ -265,8 +300,7 @@ class JavaLanguageServer : ILanguageServer { } } - override fun formatCode(params: FormatCodeParams?): CodeFormatResult = - CodeFormatProvider(settings).format(params) + override fun formatCode(params: FormatCodeParams?): CodeFormatResult = CodeFormatProvider(settings).format(params) override fun handleFailure(failure: LSPFailure?): Boolean { return when (failure!!.type) { @@ -285,6 +319,7 @@ class JavaLanguageServer : ILanguageServer { if (!DocumentUtils.isJavaFile(file)) { return JavaCompilerService.NO_MODULE_COMPILER } + ensureProjectReset() val module = ProjectManagerImpl.getInstance().findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER @@ -314,6 +349,8 @@ class JavaLanguageServer : ILanguageServer { return } + ensureProjectReset() + // TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event) val module = From 20ddbeba64512a2f7e5085a795f099a1b6d70add Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 16:23:14 -0700 Subject: [PATCH 04/21] ADFA-5052: Serialize the deferred javac reset against concurrent access and shutdown The previous fix's synchronized(this) block only guarded the *decision* to run ensureProjectReset() (claiming pendingWorkspace, flipping javaCompilerInitialized to true) -- not the destroy/rebuild work that followed, which ran unsynchronized. A concurrent getCompiler()/ onContentChange() call on another thread could see javaCompilerInitialized already true and proceed to use JavaCompilerProvider/SourceFileManager while the first thread was still mid-destroy or mid-rebuild. shutdown() didn't synchronize on anything at all, so it could run its own destroy()/ clearCache() concurrently with an in-flight reset, racing two teardown/ rebuild sequences against each other. Replaces the two ad-hoc @Volatile fields with an explicit PENDING/RESETTING/INITIALIZED/SHUTDOWN state machine guarded by a single ReentrantLock (compilerLifecycleLock) held for the *entire* reset or shutdown, not just the state transition. Concurrent callers now genuinely block until an in-flight reset finishes (getCompiler()/onContentChange() already route through ensureProjectReset(), which now can't return early while another thread holds the lock), and shutdown() waits on the same lock before deciding whether there's anything to tear down. setupWithProject() also goes through the lock; if a new project arrives mid-reset, the in-progress reset's own finally block detects the newer pendingWorkspace and reverts to PENDING instead of incorrectly claiming INITIALIZED. Verified: :lsp:java unit tests pass, :app:assembleV8Debug builds clean. --- .../androidide/lsp/java/JavaLanguageServer.kt | 109 ++++++++++++------ 1 file changed, 71 insertions(+), 38 deletions(-) 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 f8af6393f2..763080fc49 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 @@ -84,6 +84,8 @@ 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() @@ -96,14 +98,19 @@ class JavaLanguageServer : ILanguageServer { private val timer = AnalyzeTimer { analyzeSelected() } private var cachedCompletion: CachedCompletion - // Set by setupWithProject(), consumed by ensureProjectReset() on the first real .java-file - // interaction after it -- deferred because setupWithProject() is called for every project - // open regardless of language (ADFA-5052). - @Volatile - private var pendingWorkspace: Workspace? = null + // Lifecycle of the javac-backed compiler state (NO_MODULE_COMPILER, SourceFileManager, + // JavaCompilerProvider), which setupWithProject() defers instead of building eagerly + // (ADFA-5052). 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 getCompiler()/onContentChange() could use a compiler + // mid-teardown, or shutdown() could destroy state a reset is still rebuilding. + private enum class CompilerLifecycle { PENDING, RESETTING, INITIALIZED, SHUTDOWN } + + private val compilerLifecycleLock = ReentrantLock() - @Volatile - private var javaCompilerInitialized = false + // Guarded by compilerLifecycleLock. + private var pendingWorkspace: Workspace? = null + private var compilerLifecycle = CompilerLifecycle.PENDING val settings: IServerSettings get() { @@ -143,11 +150,17 @@ class JavaLanguageServer : ILanguageServer { override fun shutdown() { (this.debugAdapter as? AutoCloseable?)?.close() - if (javaCompilerInitialized) { - 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. + if (compilerLifecycle == CompilerLifecycle.INITIALIZED) { + JavaCompilerProvider.getInstance().destroy() + SourceFileManager.clearCache() + CacheFSInfoSingleton.clearCache() + clearCache() + } + compilerLifecycle = CompilerLifecycle.SHUTDOWN } EventBus.getDefault().unregister(this) timer.cancel() @@ -187,47 +200,67 @@ class JavaLanguageServer : ILanguageServer { // and JavaCompilerService.NO_MODULE_COMPILER / SourceFileManager.NO_MODULE eagerly // construct real javac machinery plus a full android.jar scan at class-init, merely by // being referenced (ADFA-5052, mirrors ADFA-5010's KotlinLanguageServer fix). - pendingWorkspace = workspace + 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 + } + } } /** * 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. - * No-ops if already up to date. + * 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() { - if (pendingWorkspace == null) return - val workspace: Workspace - synchronized(this) { - workspace = pendingWorkspace ?: return + compilerLifecycleLock.withLock { + if (compilerLifecycle != CompilerLifecycle.PENDING) return + val workspace = pendingWorkspace ?: return pendingWorkspace = null - javaCompilerInitialized = true - } + compilerLifecycle = CompilerLifecycle.RESETTING - // Once we have project initialized - // Destory the NO_MODULE_COMPILER instance - JavaCompilerService.NO_MODULE_COMPILER.destroy() + try { + // Once we have project initialized + // Destory the NO_MODULE_COMPILER instance + JavaCompilerService.NO_MODULE_COMPILER.destroy() - // Clear cached file managers - SourceFileManager.clearCache() + // Clear cached file managers + 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 - clearCachesForPaths { path: String -> path.endsWith("/R.jar") } + // 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") } - // Clear cached module-specific compilers - JavaCompilerProvider.getInstance().destroy() + // Clear cached module-specific compilers + JavaCompilerProvider.getInstance().destroy() - // Cache classpath locations - for (subModule in workspace.subProjects) { - if (subModule !is ModuleProject || subModule.path == workspace.rootProject.path) { - continue + // Cache classpath locations + for (subModule in workspace.subProjects) { + if (subModule !is ModuleProject || subModule.path == workspace.rootProject.path) { + continue + } + SourceFileManager.forModule(subModule) + } + startOrRestartAnalyzeTimer() + } finally { + // 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 + } } - SourceFileManager.forModule(subModule) } - startOrRestartAnalyzeTimer() } override fun complete(params: CompletionParams?): CompletionResult { From 6227c610f37b957afe0b393d08777fa39ea6d881 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 17:01:06 -0700 Subject: [PATCH 05/21] ADFA-5052: Fix exception handling, analyze() bypass, and a narrow post-lock race Three issues from /code-review high, verified against the current code: 1. ensureProjectReset() nulled pendingWorkspace before the try block, so any exception during destroy/rebuild (e.g. a bad submodule's classpath) still let the finally claim INITIALIZED -- silently treating a half-torn-down compiler as ready, with no retry, for the rest of the session. Now an exception re-queues the workspace, reverts to PENDING, and rethrows. 2. analyze() never called ensureProjectReset() at all. diagnosticProvider .analyze() builds its own JavaCompilerService directly, bypassing getCompiler(), and analysis is often the *first* real .java-file interaction (auto-triggered on file open, ahead of any completion request) -- so the R.jar/file-manager cache clear this reset performs could be skipped for an entire session, leaving diagnostics resolving against a stale previous project's classpath. Now gated the same way getCompiler()/onContentChange() already are. 3. getCompiler() and onContentChange() released compilerLifecycleLock as soon as ensureProjectReset() returned, then used JavaCompilerProvider unlocked -- a concurrent reset for a newer project could destroy() those compilers in the gap. Both now hold the lock across the reset and the subsequent provider lookup/use (safe: ReentrantLock is reentrant, so ensureProjectReset()'s own internal withLock nests without deadlocking). Two other findings from the same pass were assessed and left as-is: - shutdown() blocking on an in-flight reset with no cancellation is real but performance-only (no crash/corruption), requires disproportionate cancellation plumbing through SourceFileManager/JavaCompilerService for a narrow, bounded-cost edge case. - KotlinLanguageServer's eager construction is a real observation about this branch's current state, but it's already fixed by the separate, not-yet- merged ADFA-5010 (PR #1635) -- out of scope here, not a gap in this PR. Verified: :lsp:java unit tests pass, :app:assembleV8Debug builds clean. --- .../androidide/lsp/java/JavaLanguageServer.kt | 75 ++++++++++++------- 1 file changed, 50 insertions(+), 25 deletions(-) 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 763080fc49..096dc08eba 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 @@ -249,17 +249,25 @@ class JavaLanguageServer : ILanguageServer { SourceFileManager.forModule(subModule) } startOrRestartAnalyzeTimer() - } finally { - // 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 - } + } 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 } + + // 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 + } } } @@ -326,6 +334,13 @@ class JavaLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } + // diagnosticProvider.analyze() builds its own JavaCompilerService directly (bypassing + // getCompiler()), and analysis is often the first real .java-file interaction in a + // session (auto-triggered on file open, ahead of any completion request) -- without this, + // the R.jar/file-manager caches this reset clears would never get cleared for this + // project, and diagnostics could resolve against a stale previous project's classpath. + ensureProjectReset() + return if (!settings.codeAnalysisEnabled()) { DiagnosticResult.NO_UPDATE } else { @@ -352,11 +367,17 @@ class JavaLanguageServer : ILanguageServer { if (!DocumentUtils.isJavaFile(file)) { return JavaCompilerService.NO_MODULE_COMPILER } - ensureProjectReset() - val module = - ProjectManagerImpl.getInstance().findModuleForFile(file!!) - ?: return JavaCompilerService.NO_MODULE_COMPILER - return JavaCompilerProvider.get(module) + // Held across ensureProjectReset() *and* the provider lookup (ReentrantLock is + // reentrant, so ensureProjectReset()'s own withLock nests fine): otherwise a concurrent + // reset for a newer project could destroy() the provider's compilers in the gap between + // this thread's reset finishing and its JavaCompilerProvider.get() call. + return compilerLifecycleLock.withLock { + ensureProjectReset() + val module = + ProjectManagerImpl.getInstance().findModuleForFile(file!!) + ?: return@withLock JavaCompilerService.NO_MODULE_COMPILER + JavaCompilerProvider.get(module) + } } private fun updateCachedCompletion(cachedCompletion: CachedCompletion) { @@ -382,16 +403,20 @@ class JavaLanguageServer : ILanguageServer { return } - ensureProjectReset() - - // 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) + // See getCompiler(): held across the reset *and* the provider lookup/use so a concurrent + // reset can't destroy() these compilers in between. + compilerLifecycleLock.withLock { + ensureProjectReset() + + // 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) + } } startOrRestartAnalyzeTimer() } From e3b96be1b614af3003b1681e49e062b0cb27dc88 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 18:07:22 -0700 Subject: [PATCH 06/21] ADFA-5053: Add lsp/java-api bridge module New IJavaCompilerSession/IJavaCompilerSessionFactory interfaces, mirroring lsp/kotlin-api's role from ADR 0011: this is the only type surface a resident JavaLanguageServer will be allowed to reference once JavaCompilerService and its Provider classes move into an isolated, DexClassLoader-loaded module in a later commit. Exposes the LSP operations directly (complete/findReferences/ findDefinition/expandSelection/signatureHelp/analyze/onContentChange) rather than a getCompiler(): JavaCompilerService accessor, since JavaCompilerService itself won't be a resident type. Confirmed no caller outside JavaLanguageServer.kt uses the current getCompiler() (it's @RestrictTo(LIBRARY_GROUP)), so dropping it from the bridge is safe. Not yet wired up -- JavaLanguageServer.kt still talks to the soon-to-be-isolated types directly; that happens once the isolated implementation module exists. Co-Authored-By: Claude Sonnet 5 --- lsp/java-api/build.gradle.kts | 38 +++++++++ lsp/java-api/src/main/AndroidManifest.xml | 18 +++++ .../lsp/java/api/IJavaCompilerSession.kt | 77 +++++++++++++++++++ .../java/api/IJavaCompilerSessionFactory.kt | 29 +++++++ settings.gradle.kts | 1 + 5 files changed, 163 insertions(+) create mode 100644 lsp/java-api/build.gradle.kts create mode 100644 lsp/java-api/src/main/AndroidManifest.xml create mode 100644 lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSession.kt create mode 100644 lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSessionFactory.kt 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..2dd2ccdbbc --- /dev/null +++ b/lsp/java-api/src/main/java/com/itsaky/androidide/lsp/java/api/IJavaCompilerSession.kt @@ -0,0 +1,77 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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.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.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.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() + + /** Destroys per-module compilers, e.g. after a completion failure. Does not close the session. */ + fun destroyCompilers() + + 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) +} 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/settings.gradle.kts b/settings.gradle.kts index 7592064e23..a58be0c01c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -133,6 +133,7 @@ include( ":lsp:models", ":lsp:indexing", ":lsp:java", + ":lsp:java-api", ":lsp:jvm-symbol-index", ":lsp:jvm-symbol-models", ":lsp:kotlin", From a9b4bf6d7cdb9d0a4deaedf4cc2e54696d60298c Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 18:38:18 -0700 Subject: [PATCH 07/21] ADFA-5053: Split lsp/java into a resident shell and lsp/java-compiler-impl Moves the whole javac-touching surface of lsp/java (compiler/, providers/ except providers/snippet, actions/, edits/, parser/, rewrite/, utils/ except AnalyzeTimer, visitors/, JavaCompilerProvider, and their models) into a new isolated module. JavaLanguageServer.kt stays in lsp/java as a thin resident wrapper: it keeps ADFA-5052's CompilerLifecycle state machine unchanged, but ensureProjectReset() now lazily extracts and DexClassLoader-loads a carrier APK (via the new JavaCompilerLoader, mirroring KotlinCompilerLoader/ADR 0011) instead of directly constructing JavaCompilerService/JavaCompilerProvider. Every LSP operation (complete/findReferences/findDefinition/expandSelection/signatureHelp/ analyze/onContentChange/formatCode/handleFailure) now delegates through the new IJavaCompilerSession bridge (lsp/java-api) instead of touching isolated types directly. Not yet wired to an actual carrier: no carrier module exists yet, so JavaCompilerLoader has nothing to load on-device. That's the next commit. Locally, :lsp:java-compiler-impl:compileV8DebugSources verifies the isolated payload builds standalone. Notable fixes needed along the way, all confirmed via compilation across :app, :editor, :lsp:java, and :lsp:java-compiler-impl: - google-java-format stays a resident dependency (JavaServerSettings' formatter options) rather than moving with javac -- ADFA-4549 never flagged it as bloat, and the isolated module sees it via compileOnly so the type identity still matches. - The debugger's breakpoint/stack-frame source-path resolution (debug/utils/ModelUtils.kt) took a real, if narrow, dependency on JavaCompilerProvider/SourceFileObject. Added IJavaCompilerSession.findSourceFilePath so it resolves through the bridge (returning a plain path, not the isolated SourceFileObject type) instead of needing the isolated module directly. - CancelChecker.kt and CompletableDeferredExts.kt were misplaced under lsp/java/utils despite being genuinely generic (editor's IDEEditor.kt and app's ProjectHandlerActivity.kt use the former for coroutine cancellation logging unrelated to javac; app's Resolvable.kt uses the latter for Deferred completion state). CancelChecker moved with the isolated payload (its CancelAbort check is genuinely javac-specific and classloader-identity-sensitive); the two generic call sites got their own small inline cancellation check instead. CompletableDeferredExts.kt moved back to stay resident. - Applied ADFA-5010's LSPEditorActions fix (replace-not-skip on register, plus a new unregisterCodeActions) here too: Java's own carrier needs the same protection against a stale session's actions outliving its DexClassLoader. - Moved the lsp/java unit tests that exercised isolated types (JavaCompilerService, CompletionProvider, JavaSelectionProvider, DefinitionProvider, etc.) into lsp/java-compiler-impl's own test sourceset, and rewrote their `server.()` calls to construct the isolated providers directly -- going through JavaLanguageServer would now try to load a real carrier APK that doesn't exist in the unit test environment. Verified: :app, :editor, :lsp:java, :lsp:java-compiler-impl, :subprojects:projects all compile; unit tests pass for :lsp:java, :lsp:java-compiler-impl, and :subprojects:projects. Co-Authored-By: Claude Sonnet 5 --- .../editor/ProjectHandlerActivity.kt | 8 +- .../itsaky/androidide/editor/ui/IDEEditor.kt | 9 +- .../androidide/lsp/util/LSPEditorActions.java | 34 ++- .../lsp/java/api/IJavaCompilerSession.kt | 28 ++- lsp/java-compiler-impl/build.gradle.kts | 86 +++++++ .../src/main/AndroidManifest.xml | 18 ++ .../java/CompilationCancellationException.kt | 0 .../lsp/java/JavaCompilerProvider.java | 0 .../lsp/java/actions/BaseJavaCodeAction.kt | 0 .../lsp/java/actions/FieldBasedAction.kt | 0 .../lsp/java/actions/JavaCodeActionsMenu.kt | 0 .../actions/common/FindReferencesAction.kt | 0 .../actions/common/GoToDefinitionAction.kt | 0 .../actions/common/OrganizeImportsAction.kt | 0 .../common/RemoveUnusedImportsAction.kt | 0 .../actions/diagnostics/AddImportAction.kt | 0 .../actions/diagnostics/AddThrowsAction.kt | 0 .../diagnostics/AutoFixImportsAction.kt | 0 .../diagnostics/CreateMissingMethodAction.kt | 0 .../actions/diagnostics/FieldToBlockAction.kt | 0 .../ImplementAbstractMethodsAction.kt | 0 .../actions/diagnostics/RemoveClassAction.kt | 0 .../actions/diagnostics/RemoveMethodAction.kt | 0 .../diagnostics/RemoveUnusedThrowsAction.kt | 0 .../SuppressUncheckedWarningAction.kt | 0 .../diagnostics/VariableToStatementAction.kt | 0 .../generators/GenerateConstructorAction.kt | 0 .../GenerateMissingConstructorAction.kt | 0 .../GenerateSettersAndGettersAction.kt | 0 .../GenerateToStringMethodAction.kt | 0 .../OverrideSuperclassMethodsAction.kt | 0 .../java/compiler/CompilationTaskProcessor.kt | 0 .../lsp/java/compiler/CompileBatch.java | 0 .../lsp/java/compiler/CompileTask.java | 0 .../lsp/java/compiler/CompilerProvider.java | 0 .../DefaultCompilationTaskProcessor.kt | 0 .../lsp/java/compiler/JCReusableCompiler.kt | 0 .../lsp/java/compiler/JavaCompilerConfig.kt | 0 .../lsp/java/compiler/JavaCompilerImpl.kt | 0 .../java/compiler/JavaCompilerService.java | 0 .../JavaCompilerSessionFactoryImpl.kt | 31 +++ .../java/compiler/JavaCompilerSessionImpl.kt | 189 +++++++++++++++ .../lsp/java/compiler/SourceFileManager.java | 0 .../lsp/java/compiler/SourceFileObject.java | 0 .../lsp/java/compiler/SynchronizedTask.kt | 0 .../lsp/java/edits/AdvancedJavaEditHandler.kt | 0 .../lsp/java/edits/BaseJavaEditHandler.kt | 0 .../lsp/java/edits/ClassImportEditHandler.kt | 0 .../edits/MultipleClassImportEditHandler.kt | 0 .../lsp/java/models/CompilationRequest.kt | 0 .../lsp/java/models/DiagnosticCode.kt | 0 .../lsp/java/models/JavaCompletionItem.kt | 0 .../lsp/java/models/PartialReparseRequest.kt | 0 .../androidide/lsp/java/parser/IJavaParser.kt | 0 .../androidide/lsp/java/parser/ParseTask.java | 0 .../androidide/lsp/java/parser/Parser.java | 0 .../lsp/java/parser/ts/TSJavaParser.kt | 0 .../lsp/java/parser/ts/TSMethodPruner.kt | 0 .../lsp/java/parser/ts/TSParseCache.kt | 0 .../lsp/java/parser/ts/TSParseResult.kt | 0 .../java/providers/BaseJavaServiceProvider.kt | 0 .../providers/CancelableServiceProvider.kt | 0 .../java/providers/CodeFormatProvider.java | 0 .../java/providers/CompletionProvider.java | 0 .../java/providers/DefinitionProvider.java | 0 .../lsp/java/providers/DiagnosticsProvider.kt | 0 .../java/providers/JavaDiagnosticProvider.kt | 0 .../java/providers/JavaSelectionProvider.java | 0 .../lsp/java/providers/ReferenceProvider.java | 0 .../lsp/java/providers/SignatureProvider.java | 0 .../ClassNamesCompletionProvider.kt | 0 .../completion/IJavaCompletionProvider.kt | 0 .../IdentifierCompletionProvider.kt | 0 .../completion/ImportCompletionProvider.kt | 0 .../completion/KeywordCompletionProvider.kt | 0 .../MemberReferenceCompletionProvider.kt | 0 .../MemberSelectCompletionProvider.kt | 0 .../completion/ScopeCompletionProvider.kt | 0 .../completion/SnippetCompletionProvider.kt | 0 .../StaticImportCompletionProvider.kt | 0 .../SwitchConstantCompletionProvider.kt | 0 .../definition/ErroneousDefinitionProvider.kt | 0 .../definition/IJavaDefinitionProvider.kt | 0 .../definition/LocalDefinitionProvider.kt | 0 .../definition/RemoteDefinitionProvider.kt | 0 .../lsp/java/rewrite/AddException.java | 0 .../lsp/java/rewrite/AddImport.java | 0 .../rewrite/AddSuppressWarningAnnotation.java | 0 .../lsp/java/rewrite/ConvertFieldToBlock.java | 0 .../rewrite/ConvertVariableToStatement.java | 0 .../lsp/java/rewrite/CreateMissingMethod.java | 0 .../rewrite/GenerateRecordConstructor.java | 0 .../rewrite/ImplementAbstractMethods.java | 0 .../lsp/java/rewrite/RemoveClass.java | 0 .../lsp/java/rewrite/RemoveException.java | 0 .../lsp/java/rewrite/RemoveMethod.java | 0 .../androidide/lsp/java/rewrite/Rewrite.kt | 0 .../androidide/lsp/java/utils/ASTFixer.java | 0 .../lsp/java/utils/CancelChecker.kt | 0 .../lsp/java/utils/CodeActionUtils.java | 0 .../androidide/lsp/java/utils/EditHelper.java | 0 .../androidide/lsp/java/utils/Extractors.java | 0 .../androidide/lsp/java/utils/FindHelper.java | 0 .../lsp/java/utils/JavaParserUtils.kt | 0 .../lsp/java/utils/JavaPoetUtils.kt | 0 .../lsp/java/utils/MarkdownHelper.java | 0 .../androidide/lsp/java/utils/MethodPtr.java | 0 .../lsp/java/utils/NavigationHelper.java | 0 .../lsp/java/utils/ScopeHelper.java | 0 .../lsp/java/utils/ShortTypePrinter.java | 0 .../androidide/lsp/java/utils/TestUtils.kt | 0 .../androidide/lsp/java/utils/TreeUtils.kt | 0 .../androidide/lsp/java/utils/TypeUtils.java | 0 .../androidide/lsp/java/utils/insertUtils.kt | 0 .../lsp/java/visitors/DiagnosticVisitor.kt | 0 .../FindAnonymousTypeDeclaration.java | 0 .../lsp/java/visitors/FindBiggerRange.java | 0 .../lsp/java/visitors/FindCompletionsAt.java | 0 .../lsp/java/visitors/FindInvocationAt.java | 0 .../lsp/java/visitors/FindMethodAt.kt | 0 .../lsp/java/visitors/FindMethodCallAt.java | 0 .../visitors/FindMethodDeclarationAt.java | 0 .../lsp/java/visitors/FindNameAt.java | 0 .../lsp/java/visitors/FindReferences.java | 0 .../java/visitors/FindTypeDeclarationAt.java | 0 .../visitors/FindTypeDeclarationNamed.java | 0 .../java/visitors/FindTypeDeclarations.java | 0 .../java/visitors/FindVariableAtCursor.java | 0 .../java/visitors/FindVariablesBetween.java | 0 .../lsp/java/visitors/MethodRangeScanner.kt | 0 .../java/visitors/PrettyPrintingVisitor.java | 0 .../lsp/java/visitors/PrintingVisitor.kt | 0 .../lsp/java/visitors/PruneMethodBodies.java | 0 .../lsp/java/JavaCompilerProviderTest.kt | 0 .../itsaky/androidide/lsp/java/JavaLSPTest.kt | 0 .../lsp/java/actions/AddImportTest.kt | 7 +- .../lsp/java/compiler/CompilerTest.kt | 0 .../java/partial/PartialReparserImplTest.kt | 0 .../providers/JavaCompletionProviderTest.kt | 15 +- .../providers/JavaSelectionProviderTest.kt | 216 ++++++++--------- .../lsp/java/utils/FindHelperTest.kt | 8 +- .../src/test/resources/robolectric.properties | 0 lsp/java/build.gradle.kts | 9 +- .../androidide/lsp/java/JavaLanguageServer.kt | 227 ++++++------------ .../lsp/java/debug/JavaDebugAdapter.kt | 11 +- .../lsp/java/debug/utils/ModelUtils.kt | 74 +++--- .../lsp/java/loader/JavaCompilerLoader.kt | 110 +++++++++ settings.gradle.kts | 1 + 148 files changed, 754 insertions(+), 327 deletions(-) create mode 100644 lsp/java-compiler-impl/build.gradle.kts create mode 100644 lsp/java-compiler-impl/src/main/AndroidManifest.xml rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/CompilationCancellationException.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/FieldBasedAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/common/FindReferencesAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/common/GoToDefinitionAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/CreateMissingMethodAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/FieldToBlockAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveClassAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateConstructorAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateSettersAndGettersAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateToStringMethodAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/OverrideSuperclassMethodsAction.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilationTaskProcessor.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileBatch.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompilerProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/DefaultCompilationTaskProcessor.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/JCReusableCompiler.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerConfig.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerService.java (100%) create mode 100644 lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionFactoryImpl.kt create mode 100644 lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionImpl.kt rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileManager.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/edits/AdvancedJavaEditHandler.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/edits/BaseJavaEditHandler.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/edits/ClassImportEditHandler.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/models/CompilationRequest.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/models/JavaCompletionItem.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/models/PartialReparseRequest.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/IJavaParser.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/ParseTask.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseCache.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSParseResult.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/BaseJavaServiceProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/CancelableServiceProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/CompletionProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaDiagnosticProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/ErroneousDefinitionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/IJavaDefinitionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/LocalDefinitionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/providers/definition/RemoteDefinitionProvider.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddImport.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveClass.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveMethod.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/CancelChecker.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/Extractors.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/NavigationHelper.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/TestUtils.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/TreeUtils.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/utils/insertUtils.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindAnonymousTypeDeclaration.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodAt.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodDeclarationAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindReferences.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationAt.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarationNamed.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindTypeDeclarations.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariableAtCursor.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindVariablesBetween.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrintingVisitor.kt (100%) rename lsp/{java => java-compiler-impl}/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java (100%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/JavaCompilerProviderTest.kt (100%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/JavaLSPTest.kt (100%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt (79%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt (100%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt (100%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt (76%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt (85%) rename lsp/{java => java-compiler-impl}/src/test/java/com/itsaky/androidide/lsp/java/utils/FindHelperTest.kt (85%) rename lsp/{java => java-compiler-impl}/src/test/resources/robolectric.properties (100%) create mode 100644 lsp/java/src/main/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoader.kt 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/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/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..173d31e94a 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 @@ -41,10 +41,40 @@ public static void ensureActionsMenuRegistered(IActionsMenuProvider provider) { final var editorActions = (ActionMenu) action; for (final var item : provider.getActions()) { - if (editorActions.findAction(item.getId()) != null) { - continue; + // 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/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 index 2dd2ccdbbc..4f115bc73b 100644 --- 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 @@ -17,17 +17,20 @@ 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 @@ -58,9 +61,6 @@ interface IJavaCompilerSession : AutoCloseable { */ fun unregisterCodeActions() - /** Destroys per-module compilers, e.g. after a completion failure. Does not close the session. */ - fun destroyCompilers() - fun complete(params: CompletionParams): CompletionResult suspend fun findReferences(params: ReferenceParams): ReferenceResult @@ -74,4 +74,26 @@ interface IJavaCompilerSession : AutoCloseable { 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-compiler-impl/build.gradle.kts b/lsp/java-compiler-impl/build.gradle.kts new file mode 100644 index 0000000000..ff29766021 --- /dev/null +++ b/lsp/java-compiler-impl/build.gradle.kts @@ -0,0 +1,86 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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) + + 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) + implementation(libs.androidx.core.ktx) + implementation(libs.common.kotlin) + + // The actual javac fork -- this is the payload this module exists to isolate. + implementation(libs.composite.javac) + implementation(libs.composite.javapoet) + implementation(projects.subprojects.javacServices) + + // Resident (kept in lsp/java -- not part of javac's dex bloat); see lsp/java/build.gradle.kts. + compileOnly(libs.composite.googleJavaFormat) + + // 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; compileOnly (main + // sourceset) doesn't extend to the test compile classpath, so this needs its own entry. + testImplementation(projects.lsp.java) +} 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/JavaCompilerProvider.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/BaseJavaCodeAction.kt 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/OrganizeImportsAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/common/RemoveUnusedImportsAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddImportAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AddThrowsAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/AutoFixImportsAction.kt 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/ImplementAbstractMethodsAction.kt 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveMethodAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/RemoveUnusedThrowsAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/SuppressUncheckedWarningAction.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/diagnostics/VariableToStatementAction.kt 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/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/actions/generators/GenerateMissingConstructorAction.kt 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 100% 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 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/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/CompileTask.java 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerImpl.kt 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..a6fd0d1431 --- /dev/null +++ b/lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/JavaCompilerSessionImpl.kt @@ -0,0 +1,189 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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.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 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() { + 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 { + val compiler = getCompiler(params.file) + return ReferenceProvider(compiler, params.cancelChecker).findReferences(params) + } + + override suspend fun findDefinition(params: DefinitionParams): DefinitionResult { + val compiler = getCompiler(params.file) + return DefinitionProvider(compiler, settings, params.cancelChecker).findDefinition(params) + } + + override suspend fun expandSelection(params: ExpandSelectionParams): Range { + val compiler = getCompiler(params.file) + return JavaSelectionProvider(compiler).expandSelection(params) + } + + override suspend fun signatureHelp(params: SignatureHelpParams): SignatureHelp { + val compiler = getCompiler(params.file) + return SignatureProvider(compiler, params.cancelChecker).signatureHelp(params) + } + + override suspend fun analyze(file: Path): DiagnosticResult = diagnosticProvider.analyze(file) + + 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/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SourceFileObject.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/compiler/SynchronizedTask.kt 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/edits/MultipleClassImportEditHandler.kt 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/models/DiagnosticCode.kt 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/Parser.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/CodeFormatProvider.java 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/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DefinitionProvider.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/DiagnosticsProvider.kt 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/ReferenceProvider.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/SignatureProvider.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ClassNamesCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IdentifierCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ImportCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/KeywordCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberReferenceCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/MemberSelectCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/ScopeCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/StaticImportCompletionProvider.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SwitchConstantCompletionProvider.kt 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddException.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/AddSuppressWarningAnnotation.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertFieldToBlock.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ConvertVariableToStatement.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/CreateMissingMethod.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/GenerateRecordConstructor.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/ImplementAbstractMethods.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/RemoveException.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/rewrite/Rewrite.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ASTFixer.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/CodeActionUtils.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/EditHelper.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/FindHelper.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaParserUtils.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/JavaPoetUtils.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MarkdownHelper.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/MethodPtr.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ScopeHelper.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/ShortTypePrinter.java 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/utils/TypeUtils.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/DiagnosticVisitor.kt 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindBiggerRange.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindCompletionsAt.java diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindInvocationAt.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindMethodCallAt.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/FindNameAt.java 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/MethodRangeScanner.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PrettyPrintingVisitor.java 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 100% 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 diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java rename to lsp/java-compiler-impl/src/main/java/com/itsaky/androidide/lsp/java/visitors/PruneMethodBodies.java 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 100% 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 diff --git a/lsp/java/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 similarity index 79% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/actions/AddImportTest.kt index a0bd8e7e86..c40a2e9cbf 100644 --- a/lsp/java/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 @@ -20,6 +20,7 @@ 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 @@ -44,7 +45,11 @@ class AddImportTest { openFile("actions/AddImportAction") val diagnostic = runBlocking { - server.analyze(file!!).diagnostics.firstOrNull { + // 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" } } diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/compiler/CompilerTest.kt diff --git a/lsp/java/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 similarity index 100% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/partial/PartialReparserImplTest.kt diff --git a/lsp/java/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 similarity index 76% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaCompletionProviderTest.kt index 70510ec5a1..7a508f8e94 100644 --- a/lsp/java/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 @@ -17,7 +17,9 @@ 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 @@ -85,10 +87,13 @@ class JavaCompletionProviderTest { } private fun completionTitles(pos: Position): List { - return JavaLSPTest.server - .complete( - CompletionParams(pos, JavaLSPTest.file!!, ICancelChecker.NOOP).apply { prefix = "" }) - .items - .map { it.ideLabel } + // 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/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 similarity index 85% rename from lsp/java/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt rename to lsp/java-compiler-impl/src/test/java/com/itsaky/androidide/lsp/java/providers/JavaSelectionProviderTest.kt index cb891d9f14..976afb669e 100644 --- a/lsp/java/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 @@ -1,108 +1,108 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU 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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 85% 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..5eb17a0747 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 @@ -49,7 +51,11 @@ class FindHelperTest { // 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) } + 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 cba21e3e77..2cde31ffde 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,14 +52,15 @@ 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.javacFs) - implementation(projects.subprojects.javacServices) implementation(projects.idetooltips) - implementation(libs.composite.javac) - implementation(libs.composite.javapoet) + // JavaServerSettings' formatter options are resident config, not part of javac's dex + // bloat (ADFA-4549 didn't flag google-java-format) -- kept resident like javac-fs, with + // lsp-java-compiler-impl seeing it via compileOnly so the type identity matches. implementation(libs.composite.googleJavaFormat) implementation(libs.androidx.core.ktx) 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 096dc08eba..915938fc0e 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,27 +68,26 @@ 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 javac-backed compiler state (NO_MODULE_COMPILER, SourceFileManager, - // JavaCompilerProvider), which setupWithProject() defers instead of building eagerly - // (ADFA-5052). 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 getCompiler()/onContentChange() could use a compiler - // mid-teardown, or shutdown() could destroy state a reset is still rebuilding. + + // 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() @@ -111,6 +95,7 @@ class JavaLanguageServer : ILanguageServer { // Guarded by compilerLifecycleLock. private var pendingWorkspace: Workspace? = null private var compilerLifecycle = CompilerLifecycle.PENDING + private var codeActionsRegistered = false val settings: IServerSettings get() { @@ -129,8 +114,6 @@ class JavaLanguageServer : ILanguageServer { } init { - cachedCompletion = CachedCompletion.EMPTY - applySettings(JavaServerSettings.getInstance()) if (!EventBus.getDefault().isRegistered(this)) { @@ -145,6 +128,8 @@ class JavaLanguageServer : ILanguageServer { 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() } @@ -155,8 +140,12 @@ class JavaLanguageServer : ILanguageServer { // thread while the lock is held, never by us once we've acquired it), so this never // races ensureProjectReset()'s own destroy/rebuild. if (compilerLifecycle == CompilerLifecycle.INITIALIZED) { - JavaCompilerProvider.getInstance().destroy() - SourceFileManager.clearCache() + // 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() } @@ -185,8 +174,6 @@ class JavaLanguageServer : ILanguageServer { } override fun setupWithProject(workspace: Workspace) { - LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu) - ( ProjectManagerImpl .getInstance() @@ -197,9 +184,8 @@ class JavaLanguageServer : ILanguageServer { // 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 JavaCompilerService.NO_MODULE_COMPILER / SourceFileManager.NO_MODULE eagerly - // construct real javac machinery plus a full android.jar scan at class-init, merely by - // being referenced (ADFA-5052, mirrors ADFA-5010's KotlinLanguageServer fix). + // 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 @@ -213,40 +199,25 @@ class JavaLanguageServer : ILanguageServer { /** * 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. - * No-ops if already up to date. Blocks concurrent callers (and [shutdown]) for the entire - * reset, not just the decision to run one. + * 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() { + private fun ensureProjectReset(): IJavaCompilerSession? = compilerLifecycleLock.withLock { - if (compilerLifecycle != CompilerLifecycle.PENDING) return - val workspace = pendingWorkspace ?: return + if (compilerLifecycle != CompilerLifecycle.PENDING) return@withLock loader.currentSession() + val workspace = pendingWorkspace ?: return@withLock loader.currentSession() pendingWorkspace = null compilerLifecycle = CompilerLifecycle.RESETTING + val session: IJavaCompilerSession try { - // Once we have project initialized - // Destory the NO_MODULE_COMPILER instance - JavaCompilerService.NO_MODULE_COMPILER.destroy() - - // Clear cached file managers - 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 - clearCachesForPaths { path: String -> path.endsWith("/R.jar") } - - // Clear cached module-specific compilers - JavaCompilerProvider.getInstance().destroy() - - // Cache classpath locations - for (subModule in workspace.subProjects) { - if (subModule !is ModuleProject || subModule.path == workspace.rootProject.path) { - continue - } - SourceFileManager.forModule(subModule) + session = loader.getOrCreateSession(workspace) + session.resetProject(workspace) + if (!codeActionsRegistered) { + session.registerCodeActions() + codeActionsRegistered = true } startOrRestartAnalyzeTimer() } catch (e: Exception) { @@ -268,65 +239,43 @@ class JavaLanguageServer : ILanguageServer { } else { CompilerLifecycle.INITIALIZED } + + session } - } 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 completionProvider.complete(params) + return ensureProjectReset()?.complete(params) ?: CompletionResult.EMPTY } 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 { @@ -334,56 +283,28 @@ class JavaLanguageServer : ILanguageServer { return DiagnosticResult.NO_UPDATE } - // diagnosticProvider.analyze() builds its own JavaCompilerService directly (bypassing - // getCompiler()), and analysis is often the first real .java-file interaction in a - // session (auto-triggered on file open, ahead of any completion request) -- without this, - // the R.jar/file-manager caches this reset clears would never get cleared for this - // project, and diagnostics could resolve against a stale previous project's classpath. - ensureProjectReset() + // 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 formatCode(params: FormatCodeParams?): CodeFormatResult = ensureProjectReset()?.formatCode(params) ?: CodeFormatResult.NONE - override fun handleFailure(failure: LSPFailure?): Boolean { - return when (failure!!.type) { - FailureType.COMPLETION -> { - if (isCancelled(failure.error)) { - return true - } - JavaCompilerProvider.getInstance().destroy() - true - } + override fun handleFailure(failure: LSPFailure?): Boolean = + when (failure!!.type) { + FailureType.COMPLETION -> loader.currentSession()?.handleCompletionFailure(failure.error) ?: true } - } - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - fun getCompiler(file: Path?): JavaCompilerService { - if (!DocumentUtils.isJavaFile(file)) { - return JavaCompilerService.NO_MODULE_COMPILER - } - // Held across ensureProjectReset() *and* the provider lookup (ReentrantLock is - // reentrant, so ensureProjectReset()'s own withLock nests fine): otherwise a concurrent - // reset for a newer project could destroy() the provider's compilers in the gap between - // this thread's reset finishing and its JavaCompilerProvider.get() call. - return compilerLifecycleLock.withLock { - ensureProjectReset() - val module = - ProjectManagerImpl.getInstance().findModuleForFile(file!!) - ?: return@withLock JavaCompilerService.NO_MODULE_COMPILER - 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) { @@ -403,20 +324,12 @@ class JavaLanguageServer : ILanguageServer { return } - // See getCompiler(): held across the reset *and* the provider lookup/use so a concurrent - // reset can't destroy() these compilers in between. + // 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() - - // 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) - } + ensureProjectReset()?.onContentChange(event) } startOrRestartAnalyzeTimer() } @@ -437,7 +350,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/debug/JavaDebugAdapter.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/debug/JavaDebugAdapter.kt index a0ba24f34a..bbec566f97 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() @@ -530,7 +537,7 @@ internal class JavaDebugAdapter : event = BreakpointHitEvent( remoteClient = vm.client, - location = location.asLspLocation(), + location = location.asLspLocation(session = currentCompilerSession()), threadId = thread.uniqueID().toString(), ), ) @@ -548,7 +555,7 @@ internal class JavaDebugAdapter : event = LspStepEvent( remoteClient = vm.client, - location = location.asLspLocation(), + location = location.asLspLocation(session = currentCompilerSession()), threadId = thread.uniqueID().toString(), ), ) 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..1335aac259 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,11 @@ 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 com.itsaky.androidide.lsp.debug.model.Location as LspLocation private val logger = LoggerFactory.getLogger("ModelUtilsKt") @@ -18,49 +15,48 @@ 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( 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..fef1fb9730 --- /dev/null +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoader.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.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 + * `KotlinCompilerLoader`'s construction (ADR 0011), which mirrors `PluginLoader`'s. + */ +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() { + 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/settings.gradle.kts b/settings.gradle.kts index a58be0c01c..408e9ff536 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -134,6 +134,7 @@ include( ":lsp:indexing", ":lsp:java", ":lsp:java-api", + ":lsp:java-compiler-impl", ":lsp:jvm-symbol-index", ":lsp:jvm-symbol-models", ":lsp:kotlin", From 881dabff636ecbd58e6ff69472555b6b2d30ab89 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 19:10:32 -0700 Subject: [PATCH 08/21] ADFA-5053: Add the java-compiler-carrier module, fix real duplicate-class leaks it exposed New subprojects/java-compiler-carrier: a never-installed com.android.application shell (isMinifyEnabled=false, mirrors subprojects/kotlin-compiler-carrier) that exists only so AGP produces a real classes.dex from lsp:java-compiler-impl, for JavaCompilerLoader to DexClassLoader at runtime. Assembling it for the first time surfaced that the isolation from the previous commits wasn't actually leak-proof -- verified with dexdump against both the carrier's and the main app's dex, not just by reading the Gradle config: - jdk-compiler's own build.gradle.kts had `api(projects.buildDeps.javaCompiler)` -- an api dependency propagates to every consumer's runtime/packaging classpath regardless of how *they* declare their own dependency on jdk-compiler, so no amount of compileOnly on the consumer side could stop java-compiler (CacheFSInfo, Context, etc.) from being duplicated into the carrier. Changed to compileOnly; the javac aggregate module still api's both itself for its own resident-only consumers. - lsp-java-compiler-impl itself directly depended on the libs.composite.javac aggregate (both jdk-compiler and java-compiler) instead of jdk-compiler alone -- switched to the split dependency. - javac-services depended on javac-fs via implementation instead of compileOnly, bundling the resident fs wrappers into the carrier too. - google-java-format's own build.gradle.kts had the same api(javac aggregate) problem as jdk-compiler -- fixed the same way. But google-java-format actually runs javac's real parser at runtime to reformat source, so unlike javapoet it has to move with javac into the isolated module, not stay resident: JavaServerSettings (resident) now exposes only a plain code-style int instead of a google-java-format JavaFormatterOptions/Style value, and the two isolated call sites (CodeFormatProvider, OrganizeImportsAction) build the real options object themselves. - javapoet, by contrast, turned out to be needed unconditionally and resident-side too (templates-api/templates-impl's "New Project" wizard, a completely separate use from the Java LSP's code-generation actions) and is lightweight (no jdk-compiler dependency at all) -- kept it fully resident, with lsp-java-compiler-impl seeing it via compileOnly. - app/build.gradle.kts had a stray direct `implementation(projects.subprojects .javacServices)` with zero actual source usage in app/ -- the same class of leftover ADFA-5010 found and removed for kotlin-analysis-api. This was the very last leak keeping the full heavy javac fork in the main app's dex even after every other fix above. Verified end-to-end via dexdump class-descriptor listings (not just `checkDuplicateClasses`, which only catches conflicts within one module's own build): CacheFSInfo/Context/Assert/PlatformUtils/CacheFSInfoSingleton/JavaPoet appear exactly once, in the main app's dex, never the carrier's; NBAttr/ ReusableCompiler/JavaCompilerService/JavaCompilerSessionImpl/google-java- format's Formatter appear exactly once, in the carrier's dex, never the main app's. :app:assembleV8Debug and :subprojects:java-compiler-carrier:assembleV8Debug both succeed; :lsp:java, :lsp:java-compiler-impl, :subprojects:projects, and :subprojects:javac-services unit tests all pass. Co-Authored-By: Claude Sonnet 5 --- app/build.gradle.kts | 1 - .../google-java-format/build.gradle.kts | 9 +++- .../build-deps/javapoet/build.gradle.kts | 4 ++ .../build-deps/jdk-compiler/build.gradle.kts | 8 +++- lsp/java-compiler-impl/build.gradle.kts | 30 +++++++++---- .../actions/common/OrganizeImportsAction.kt | 9 +++- .../java/providers/CodeFormatProvider.java | 8 +++- lsp/java/build.gradle.kts | 5 --- .../lsp/java/models/JavaServerSettings.java | 24 ++++------- settings.gradle.kts | 1 + .../java-compiler-carrier/build.gradle.kts | 42 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 24 +++++++++++ subprojects/javac-services/build.gradle.kts | 11 ++++- 13 files changed, 141 insertions(+), 35 deletions(-) create mode 100644 subprojects/java-compiler-carrier/build.gradle.kts create mode 100644 subprojects/java-compiler-carrier/src/main/AndroidManifest.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2f4fdf7ddc..62202f490e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -288,7 +288,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) 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..6ce5cfb17d 100644 --- a/composite-builds/build-deps/google-java-format/build.gradle.kts +++ b/composite-builds/build-deps/google-java-format/build.gradle.kts @@ -28,7 +28,14 @@ dependencies { implementation(libs.google.guava) implementation(libs.google.auto.value.annotations) implementation(libs.google.auto.service.annotations) - implementation(projects.buildDeps.javac) + + // 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/javapoet/build.gradle.kts b/composite-builds/build-deps/javapoet/build.gradle.kts index fe99059f36..79ac62ee32 100644 --- a/composite-builds/build-deps/javapoet/build.gradle.kts +++ b/composite-builds/build-deps/javapoet/build.gradle.kts @@ -20,5 +20,9 @@ plugins { } dependencies { + // 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) } \ No newline at end of file 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/lsp/java-compiler-impl/build.gradle.kts b/lsp/java-compiler-impl/build.gradle.kts index ff29766021..b222370660 100644 --- a/lsp/java-compiler-impl/build.gradle.kts +++ b/lsp/java-compiler-impl/build.gradle.kts @@ -53,13 +53,27 @@ dependencies { implementation(libs.androidx.core.ktx) implementation(libs.common.kotlin) - // The actual javac fork -- this is the payload this module exists to isolate. - implementation(libs.composite.javac) - implementation(libs.composite.javapoet) + // 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) - // Resident (kept in lsp/java -- not part of javac's dex bloat); see lsp/java/build.gradle.kts. - compileOnly(libs.composite.googleJavaFormat) + // 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 @@ -80,7 +94,9 @@ dependencies { compileOnly(projects.subprojects.projects) testImplementation(projects.testing.lsp) - // The moved tests construct/drive a resident JavaLanguageServer directly; compileOnly (main - // sourceset) doesn't extend to the test compile classpath, so this needs its own entry. + // 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) } 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 index f1850d6e26..5419438978 100644 --- 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 @@ -2,6 +2,7 @@ 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 @@ -49,7 +50,13 @@ class OrganizeImportsAction : BaseJavaCodeAction() { val content = editor.text val server = data[JavaLanguageServer::class.java] val settings = server!!.settings as JavaServerSettings - val output = ImportOrderer.reorderImports(content.toString(), settings.style) + 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) { 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 index f73e2ab3c5..a3f8c61690 100644 --- 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 @@ -23,6 +23,7 @@ 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; @@ -55,7 +56,12 @@ 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()); + 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; diff --git a/lsp/java/build.gradle.kts b/lsp/java/build.gradle.kts index 2cde31ffde..a4fb0b1e8b 100644 --- a/lsp/java/build.gradle.kts +++ b/lsp/java/build.gradle.kts @@ -58,11 +58,6 @@ dependencies { implementation(projects.subprojects.javacFs) implementation(projects.idetooltips) - // JavaServerSettings' formatter options are resident config, not part of javac's dex - // bloat (ADFA-4549 didn't flag google-java-format) -- kept resident like javac-fs, with - // lsp-java-compiler-impl seeing it via compileOnly so the type identity matches. - 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/models/JavaServerSettings.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/models/JavaServerSettings.java index c4e296204e..fcb0046fe1 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 @@ -17,11 +17,9 @@ 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. @@ -49,20 +47,14 @@ 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() { + /** + * {@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)) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 408e9ff536..ddd8cddb9c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -148,6 +148,7 @@ 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", diff --git a/subprojects/java-compiler-carrier/build.gradle.kts b/subprojects/java-compiler-carrier/build.gradle.kts new file mode 100644 index 0000000000..33c2e05cb0 --- /dev/null +++ b/subprojects/java-compiler-carrier/build.gradle.kts @@ -0,0 +1,42 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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.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) +} 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-services/build.gradle.kts b/subprojects/javac-services/build.gradle.kts index f80811b9e6..ab698259d4 100644 --- a/subprojects/javac-services/build.gradle.kts +++ b/subprojects/javac-services/build.gradle.kts @@ -20,9 +20,16 @@ dependencies { implementation(libs.google.guava) implementation(projects.common) implementation(projects.logger) - implementation(projects.subprojects.javacFs) - 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) From 861d411e07eeb02e7daa21290f46859fb6f957f6 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 19:17:41 -0700 Subject: [PATCH 09/21] ADFA-5053: Wire the javac carrier into app's assets New copyJavaCompilerCarrierToAssets task (app/build.gradle.kts), mirroring ADR 0011's copyKotlinCompilerCarrierToAssets pattern: builds :subprojects:java-compiler-carrier:assembleV8Release and copies the unsigned APK to app/src/main/assets/data/common/java-compiler-carrier.apk, wired into preBuild so it's always current. No PNG-optimization step needed here (unlike the Kotlin carrier) -- this module has no resources at all. Includes the same evaluationDependsOn(":subprojects:java-compiler-carrier") workaround the Kotlin carrier needed: without it, configureondemand=true only reaches that (com.android.application) project lazily via the task's cross-project dependsOn, which trips a "DefaultClassLoaderScope must be locked" Gradle failure specific to that project type. Verified: :app:copyJavaCompilerCarrierToAssets and :app:assembleV8Debug both succeed, and the built APK's assets/data/common/java-compiler-carrier.apk (~36MB) is a raw asset entry, not merged into app's own classes*.dex. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + app/build.gradle.kts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) 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/app/build.gradle.kts b/app/build.gradle.kts index 62202f490e..9ee24d2a57 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) @@ -408,6 +417,32 @@ 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") { + // See evaluationDependsOn(":subprojects:java-compiler-carrier") above for why this avoids + // project(":subprojects:java-compiler-carrier").layout... here. + dependsOn(":subprojects:java-compiler-carrier:assembleV8Release") + val sourceFile = + rootProject.layout.projectDirectory + .dir("subprojects/java-compiler-carrier/build/outputs/apk/v8/release") + .file("java-compiler-carrier-v8-release-unsigned.apk") + val destFile = layout.projectDirectory.file("src/main/assets/data/common/java-compiler-carrier.apk") + inputs.file(sourceFile) + outputs.file(destFile) + doLast { + sourceFile.asFile.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") From da6b1ad2d87007c8b4c457afb853fc5e91ae38b8 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 19:36:43 -0700 Subject: [PATCH 10/21] ADFA-5053: Remove app's now-pointless openjdk keep rule, add ADR 0012 -keep class openjdk.** { *; } is a no-op now that the javac fork lives only in the isolated carrier's dex, never app's own -- confirmed via a real :app:assembleV8Release build, dexdump-verified before and after: zero Lopenjdk/** class descriptors were reachable-but-for-the-rule, and the small resident set (CacheFSInfo/FSInfo/RelativePath, kept alive by genuine reachability from subprojects/projects, not this rule) survives R8 shrinking identically with the rule removed. jdkx.**'s keep rule stays -- unlike Kotlin's Analysis API, some jdkx/java-compiler classes remain genuinely resident (javac-fs, javapoet), so removing it isn't a pure no-op the way this one was. ADR 0012 documents the full decision: the ADR 0011 precedent this mirrors, the subprojects/projects coupling that made this harder than Kotlin's case and how the vendored-source relocation resolved it, the google-java-format -vs-javapoet resident/isolated judgment calls, and the three real duplicate-class-identity bugs (an `api` dependency in a vendored composite build's own build.gradle.kts propagating to every consumer regardless of how they declare their own dependency) found only by dexdumping the actual built carrier and app dex, not by reading the Gradle config. Verified: :app:assembleV8Release succeeds; the app launches without crashing on a physical device (Pixel 6 Pro) with the rule removed. Co-Authored-By: Claude Sonnet 5 --- app/proguard-rules.pro | 3 - ...0012-lazy-load-javac-via-dexclassloader.md | 56 +++++++++++++++++++ docs/adr/README.md | 1 + 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0012-lazy-load-javac-via-dexclassloader.md 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/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..f69a16e033 --- /dev/null +++ b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md @@ -0,0 +1,56 @@ +# 0012. Lazy-load the embedded javac fork via a carrier APK + DexClassLoader + +- **Status:** Proposed +- **Date:** 2026-08-06 +- **Deciders:** Code On The Go team + +## 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, but here it took three separate fixes to fully close: `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), and `javapoet`'s and `google-java-format`'s own composite-build files had the identical pattern. All three needed fixing at the source, not just at the `lsp/java-compiler-impl` consumer level. A fourth, unrelated 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`. + +## 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. +- 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 | From 7b3daf9b0b6e62378da2ca53f34f3ae747778120 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 22:15:57 -0700 Subject: [PATCH 11/21] ADFA-5053: Widen 3 cross-classloader protected/package-private members to public Found via the interactive on-device pass (not by the build or unit tests): opening a real .java file crashed with IllegalAccessError: Method 'java.util.Optional openjdk.tools.javac.file. CacheFSInfo.getAttributes(java.nio.file.Path)' is inaccessible to class 'openjdk.tools.javac.file.JavacFileManager' (declaration of 'JavacFileManager' appears in .../java-compiler-carrier.apk!classes4.dex) ART treats two classes with the identical package name as different runtime packages when they're loaded by different classloaders -- same-package and protected access are resolved by classloader identity, not just the package string. CacheFSInfo/RelativePath are resident (java-compiler, per ADR 0012); JavacFileManager is isolated in the carrier (jdk-compiler). Calling a protected or package-private member across that boundary throws IllegalAccessError at runtime, with no build-time or unit-test signal at all. Widened the three members JavacFileManager/JRTIndex actually call this way: CacheFSInfo.getAttributes, RelativePath.RelativeFile.forClass, and RelativePath.RelativeDirectory.forPackage. Audited the rest of jdk-compiler for other cross-boundary protected/package-private accesses on the six resident leaf classes (CacheFSInfo, FSInfo, Context, Assert, PlatformUtils, RelativePath) via static call-site search -- no others found. Verified live on a physical device (Pixel 6 Pro): opening a real .java file now extracts and DexClassLoader-loads the carrier, constructs SourceFileManager, and runs a full diagnostic pass to completion with zero IllegalAccessError and zero crashes, across two clean app restarts. Co-Authored-By: Claude Sonnet 5 --- .../java/openjdk/tools/javac/file/CacheFSInfo.java | 9 ++++++++- .../java/openjdk/tools/javac/file/RelativePath.java | 10 ++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/composite-builds/build-deps/java-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 index 607b300790..28c62d971b 100644 --- a/composite-builds/build-deps/java-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/java-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 index 280228518a..c89ab7ce7c 100644 --- a/composite-builds/build-deps/java-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); } From 7e1377b2123f840f8e1dd3b21d31158d0b46eb1d Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 22:16:59 -0700 Subject: [PATCH 12/21] ADFA-5053: Document the cross-classloader protected-access hazard in ADR 0012 Records the IllegalAccessError finding from the on-device verification pass as its own hazard class, distinct from the duplicate-class-identity one already documented: ART resolves same-package/protected access by classloader identity, not just the package name string, so a protected or package-private member on a resident class throws IllegalAccessError when called from isolated code, with zero build-time or unit-test signal. Co-Authored-By: Claude Sonnet 5 --- docs/adr/0012-lazy-load-javac-via-dexclassloader.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md index f69a16e033..2107fb6251 100644 --- a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md +++ b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md @@ -31,6 +31,8 @@ Investigation found this coupling narrower than it first looked: none of `CacheF **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, but here it took three separate fixes to fully close: `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), and `javapoet`'s and `google-java-format`'s own composite-build files had the identical pattern. All three needed fixing at the source, not just at the `lsp/java-compiler-impl` consumer level. A fourth, unrelated 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. + ## Consequences **Positive** @@ -40,7 +42,7 @@ Investigation found this coupling narrower than it first looked: none of `CacheF **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 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 From 147f04f6a835945d489e0dcf2f575eb613e8fabb Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 22:35:07 -0700 Subject: [PATCH 13/21] ADFA-5053: Apply spotless/ktlint formatting to relocated javac files The Spotless ratchet is file-level: since ~150 files were git-mv'd into the new lsp/java-compiler-impl module, every one of them differs from origin/stage and gets reformatted in full on first touch. Run spotlessApply and manually fix the handful of ktlint violations it couldn't auto-correct (wildcard import, duplicate license header, comment placement, mixed &&/||, a var that should've been val, and lines pushed over the 140-col limit by tab-width expansion). --- .../google-java-format/build.gradle.kts | 32 +- .../build-deps/javapoet/build.gradle.kts | 14 +- .../androidide/lsp/util/LSPEditorActions.java | 155 +- .../lsp/java/api/IJavaCompilerSession.kt | 5 +- .../java/CompilationCancellationException.kt | 8 +- .../lsp/java/JavaCompilerProvider.java | 121 +- .../lsp/java/actions/BaseJavaCodeAction.kt | 233 ++- .../actions/common/FindReferencesAction.kt | 109 +- .../actions/common/GoToDefinitionAction.kt | 111 +- .../actions/common/OrganizeImportsAction.kt | 177 +- .../common/RemoveUnusedImportsAction.kt | 127 +- .../actions/diagnostics/AddImportAction.kt | 366 ++-- .../actions/diagnostics/AddThrowsAction.kt | 178 +- .../diagnostics/AutoFixImportsAction.kt | 343 ++-- .../diagnostics/CreateMissingMethodAction.kt | 168 +- .../actions/diagnostics/FieldToBlockAction.kt | 178 +- .../ImplementAbstractMethodsAction.kt | 188 +- .../actions/diagnostics/RemoveClassAction.kt | 168 +- .../actions/diagnostics/RemoveMethodAction.kt | 178 +- .../diagnostics/RemoveUnusedThrowsAction.kt | 184 +- .../SuppressUncheckedWarningAction.kt | 176 +- .../diagnostics/VariableToStatementAction.kt | 182 +- .../GenerateMissingConstructorAction.kt | 174 +- .../java/compiler/CompilationTaskProcessor.kt | 18 +- .../lsp/java/compiler/CompileTask.java | 74 +- .../lsp/java/compiler/CompilerProvider.java | 40 +- .../DefaultCompilationTaskProcessor.kt | 22 +- .../lsp/java/compiler/JCReusableCompiler.kt | 10 +- .../lsp/java/compiler/JavaCompilerConfig.kt | 47 +- .../lsp/java/compiler/JavaCompilerImpl.kt | 99 +- .../java/compiler/JavaCompilerSessionImpl.kt | 5 +- .../lsp/java/compiler/SourceFileObject.java | 270 +-- .../lsp/java/compiler/SynchronizedTask.kt | 187 +- .../lsp/java/edits/AdvancedJavaEditHandler.kt | 16 +- .../lsp/java/edits/BaseJavaEditHandler.kt | 6 +- .../lsp/java/edits/ClassImportEditHandler.kt | 8 +- .../edits/MultipleClassImportEditHandler.kt | 45 +- .../lsp/java/models/CompilationRequest.kt | 16 +- .../lsp/java/models/DiagnosticCode.kt | 133 +- .../lsp/java/models/JavaCompletionItem.kt | 79 +- .../lsp/java/models/PartialReparseRequest.kt | 5 +- .../androidide/lsp/java/parser/IJavaParser.kt | 15 +- .../androidide/lsp/java/parser/ParseTask.java | 14 +- .../androidide/lsp/java/parser/Parser.java | 482 ++--- .../lsp/java/parser/ts/TSJavaParser.kt | 156 +- .../lsp/java/parser/ts/TSMethodPruner.kt | 71 +- .../lsp/java/parser/ts/TSParseCache.kt | 23 +- .../lsp/java/parser/ts/TSParseResult.kt | 15 +- .../java/providers/BaseJavaServiceProvider.kt | 3 +- .../providers/CancelableServiceProvider.kt | 5 +- .../java/providers/CodeFormatProvider.java | 234 ++- .../java/providers/DefinitionProvider.java | 168 +- .../lsp/java/providers/DiagnosticsProvider.kt | 664 +++---- .../java/providers/JavaDiagnosticProvider.kt | 47 +- .../java/providers/JavaSelectionProvider.java | 52 +- .../lsp/java/providers/ReferenceProvider.java | 232 +-- .../lsp/java/providers/SignatureProvider.java | 589 +++--- .../ClassNamesCompletionProvider.kt | 224 +-- .../completion/IJavaCompletionProvider.kt | 834 +++++---- .../IdentifierCompletionProvider.kt | 161 +- .../completion/ImportCompletionProvider.kt | 899 ++++----- .../completion/KeywordCompletionProvider.kt | 322 ++-- .../MemberReferenceCompletionProvider.kt | 353 ++-- .../MemberSelectCompletionProvider.kt | 467 ++--- .../completion/ScopeCompletionProvider.kt | 378 ++-- .../completion/SnippetCompletionProvider.kt | 117 +- .../StaticImportCompletionProvider.kt | 253 +-- .../SwitchConstantCompletionProvider.kt | 209 ++- .../definition/ErroneousDefinitionProvider.kt | 85 +- .../definition/IJavaDefinitionProvider.kt | 49 +- .../definition/LocalDefinitionProvider.kt | 42 +- .../definition/RemoteDefinitionProvider.kt | 28 +- .../lsp/java/rewrite/AddException.java | 113 +- .../lsp/java/rewrite/AddImport.java | 32 +- .../rewrite/AddSuppressWarningAnnotation.java | 83 +- .../lsp/java/rewrite/ConvertFieldToBlock.java | 86 +- .../rewrite/ConvertVariableToStatement.java | 114 +- .../lsp/java/rewrite/CreateMissingMethod.java | 362 ++-- .../rewrite/GenerateRecordConstructor.java | 246 +-- .../rewrite/ImplementAbstractMethods.java | 357 ++-- .../lsp/java/rewrite/RemoveClass.java | 28 +- .../lsp/java/rewrite/RemoveException.java | 309 ++-- .../lsp/java/rewrite/RemoveMethod.java | 61 +- .../androidide/lsp/java/rewrite/Rewrite.kt | 169 +- .../androidide/lsp/java/utils/ASTFixer.java | 210 ++- .../lsp/java/utils/CancelChecker.kt | 24 +- .../lsp/java/utils/CodeActionUtils.java | 407 ++-- .../androidide/lsp/java/utils/EditHelper.java | 410 ++-- .../androidide/lsp/java/utils/Extractors.java | 37 +- .../androidide/lsp/java/utils/FindHelper.java | 371 ++-- .../lsp/java/utils/JavaParserUtils.kt | 1645 +++++++++-------- .../lsp/java/utils/JavaPoetUtils.kt | 219 +-- .../lsp/java/utils/MarkdownHelper.java | 343 ++-- .../androidide/lsp/java/utils/MethodPtr.java | 210 +-- .../lsp/java/utils/NavigationHelper.java | 114 +- .../lsp/java/utils/ScopeHelper.java | 118 +- .../lsp/java/utils/ShortTypePrinter.java | 180 +- .../androidide/lsp/java/utils/TestUtils.kt | 73 +- .../androidide/lsp/java/utils/TreeUtils.kt | 61 +- .../androidide/lsp/java/utils/TypeUtils.java | 574 +++--- .../androidide/lsp/java/utils/insertUtils.kt | 48 +- .../lsp/java/visitors/DiagnosticVisitor.kt | 842 +++++---- .../FindAnonymousTypeDeclaration.java | 147 +- .../lsp/java/visitors/FindBiggerRange.java | 226 +-- .../lsp/java/visitors/FindCompletionsAt.java | 210 +-- .../lsp/java/visitors/FindInvocationAt.java | 86 +- .../lsp/java/visitors/FindMethodAt.kt | 61 +- .../lsp/java/visitors/FindMethodCallAt.java | 431 ++--- .../visitors/FindMethodDeclarationAt.java | 61 +- .../lsp/java/visitors/FindNameAt.java | 175 +- .../lsp/java/visitors/FindReferences.java | 76 +- .../java/visitors/FindTypeDeclarationAt.java | 63 +- .../visitors/FindTypeDeclarationNamed.java | 45 +- .../java/visitors/FindTypeDeclarations.java | 30 +- .../java/visitors/FindVariableAtCursor.java | 53 +- .../java/visitors/FindVariablesBetween.java | 152 +- .../lsp/java/visitors/MethodRangeScanner.kt | 104 +- .../java/visitors/PrettyPrintingVisitor.java | 295 +-- .../lsp/java/visitors/PrintingVisitor.kt | 28 +- .../lsp/java/visitors/PruneMethodBodies.java | 84 +- .../itsaky/androidide/lsp/java/JavaLSPTest.kt | 33 +- .../lsp/java/actions/AddImportTest.kt | 147 +- .../lsp/java/compiler/CompilerTest.kt | 134 +- .../java/partial/PartialReparserImplTest.kt | 237 ++- .../providers/JavaCompletionProviderTest.kt | 197 +- .../providers/JavaSelectionProviderTest.kt | 167 +- .../lsp/java/utils/FindHelperTest.kt | 45 +- .../lsp/java/debug/JavaDebugAdapter.kt | 51 +- .../lsp/java/debug/utils/ModelUtils.kt | 5 +- .../lsp/java/models/JavaServerSettings.java | 130 +- .../services/fs/AndroidFsProviderImpl.kt | 17 +- .../javac/services/fs/CacheFSInfoSingleton.kt | 65 +- .../javac/services/fs/CachedJarFileSystem.kt | 78 +- .../fs/CachingJarFileSystemProvider.kt | 108 +- .../services/fs/JarPackageProviderImpl.kt | 8 +- 135 files changed, 11854 insertions(+), 11587 deletions(-) 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 6ce5cfb17d..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,27 +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(libs.google.guava) + implementation(libs.google.auto.value.annotations) + implementation(libs.google.auto.service.annotations) - // 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) + // 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) -} \ No newline at end of file + annotationProcessor(libs.google.auto.value.ap) + annotationProcessor(libs.google.auto.service) +} diff --git a/composite-builds/build-deps/javapoet/build.gradle.kts b/composite-builds/build-deps/javapoet/build.gradle.kts index 79ac62ee32..f93a542f06 100644 --- a/composite-builds/build-deps/javapoet/build.gradle.kts +++ b/composite-builds/build-deps/javapoet/build.gradle.kts @@ -16,13 +16,13 @@ */ plugins { - kotlin("jvm") + kotlin("jvm") } dependencies { - // 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) -} \ 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/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 173d31e94a..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,80 +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()) { - // 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); - } - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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/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 index 4f115bc73b..80c783ddd9 100644 --- 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 @@ -95,5 +95,8 @@ interface IJavaCompilerSession : AutoCloseable { * path rather than a `SourceFileObject`, since that type lives only on this side of the * classloader boundary. */ - fun findSourceFilePath(module: ModuleProject, className: String): String? + fun findSourceFilePath( + module: ModuleProject, + className: String, + ): String? } diff --git a/lsp/java-compiler-impl/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 index 82e42d24c5..5dfcaef5c9 100644 --- a/lsp/java-compiler-impl/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 index 106d35bf83..2fd3987fef 100644 --- 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 @@ -19,16 +19,13 @@ 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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Provides {@link JavaCompilerService} instances for different {@link ModuleProject}s. @@ -36,71 +33,69 @@ * @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 static final Logger logger = LoggerFactory.getLogger(JavaCompilerProvider.class); + private static JavaCompilerProvider sInstance; + + @NonNull + public static JavaCompilerService get(ModuleProject module) { + return JavaCompilerProvider.getInstance().forModule(module); + } - private JavaCompilerProvider() {} + public static JavaCompilerProvider getInstance() { + if (sInstance == null) { + sInstance = new JavaCompilerProvider(); + } - @NonNull - public static JavaCompilerService get(ModuleProject module) { - return JavaCompilerProvider.getInstance().forModule(module); - } + return sInstance; + } - public static JavaCompilerProvider getInstance() { - if (sInstance == null) { - sInstance = new JavaCompilerProvider(); - } + private final Map mCompilers = new ConcurrentHashMap<>(); - return sInstance; - } + private JavaCompilerProvider() {} - /** - * 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; - } + // 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(); + } - @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; - } + /** + * 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; + } - final JavaCompilerService newInstance = new JavaCompilerService(module); - mCompilers.put(module, newInstance); + @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; + } - return newInstance; - } + final JavaCompilerService newInstance = new JavaCompilerService(module); + mCompilers.put(module, 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(); - } + 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 index fd9c7647b8..69aab41d03 100644 --- 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 @@ -1,117 +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 { - 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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 2f5fd243b0..785844152d 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 90c6d7a8bf..236b76141f 100644 --- a/lsp/java-compiler-impl/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 index 5419438978..39178856c2 100644 --- 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 @@ -1,87 +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() - } - } - } - } -} +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 index 00c6f92af4..03ab2f7f72 100644 --- 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 @@ -1,62 +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) - } - } -} +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 index de002a09bd..73c342d3cf 100644 --- 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 @@ -1,183 +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() - } - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index f52e48134b..d40f32a3f2 100644 --- 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 @@ -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.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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b00001c582..ceb79db253 100644 --- 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 @@ -42,165 +42,186 @@ import java.nio.file.Path * @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>) + 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-compiler-impl/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 index 75181aeac8..6350f8bebc 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 62bf0b0f33..086b9566da 100644 --- a/lsp/java-compiler-impl/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 index 76971f321f..3c98c55f95 100644 --- 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 @@ -1,94 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 221e5354e7..09da5a5ad7 100644 --- a/lsp/java-compiler-impl/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 index b0aa7ef5a1..1500fb5068 100644 --- 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 @@ -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.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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b1c3d35c33..acfbd5a64a 100644 --- 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 @@ -1,92 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b831e43def..5d03260274 100644 --- 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 @@ -1,88 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index f15b38cac6..c87a107b62 100644 --- 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 @@ -1,91 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-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 index e4fb0a058e..9c533c8b52 100644 --- 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 @@ -1,87 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 04f32e1ee8..f86965afc9 100644 --- a/lsp/java-compiler-impl/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-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 index dac24ba968..c7fe74b6ab 100644 --- 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 @@ -28,46 +28,46 @@ 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 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 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); - } + @Override + public void close() {} - 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() { + 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(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() {} + 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-compiler-impl/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 index db23b4ee77..58920adca9 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index bdd340baee..81a7561ccf 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index ca821b9cc0..f1fdc7c597 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 11d8ad1c45..b7fe68513b 100644 --- a/lsp/java-compiler-impl/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 index 960ccc74bc..c23a165ce4 100644 --- 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 @@ -31,60 +31,67 @@ import openjdk.tools.javac.tree.JCTree.JCCompilationUnit import openjdk.tools.javac.util.Context import kotlin.io.path.name -class JavaCompilerImpl(context: Context?) : ReusableJavaCompiler(context) { +class JavaCompilerImpl( + context: Context?, +) : ReusableJavaCompiler(context) { + override fun parse( + filename: JavaFileObject?, + content: CharSequence?, + ): JCCompilationUnit { + if (VMUtils.isJvm) { + return super.parse(filename, content) + } - override fun parse(filename: JavaFileObject?, content: CharSequence?): JCCompilationUnit { + val file = ClientCodeWrapper.instance(context).unwrap(filename) + val compilerConfig = JavaCompilerConfig.instance(context) - if (VMUtils.isJvm) { - return super.parse(filename, content) - } + // Preconditions + if ( + content == null || + compilerConfig.files == null || + filename?.kind != SOURCE || + compilerConfig.files?.contains(file) == false + ) { + return super.parse(filename, content) + } - val file = ClientCodeWrapper.instance(context).unwrap(filename) - val compilerConfig = JavaCompilerConfig.instance(context) + // 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) + } - // Preconditions - if ( - content == null || - compilerConfig.files == null || - filename?.kind != SOURCE || - compilerConfig.files?.contains(file) == false - ) { - return super.parse(filename, content) - } + val pruned = + withStopWatch("${if (file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch -> + val contentBuilder = StringBuilder(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) - } + return@withStopWatch TSJavaParser.parse(file).use { parseResult -> - val pruned = withStopWatch("${if(file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch -> - val contentBuilder = StringBuilder(content) + prune( + contentBuilder, + parseResult.tree, + compilerConfig.completionInfo?.cursor?.index ?: -1, + ) - return@withStopWatch TSJavaParser.parse(file).use { parseResult -> + watch.log() - prune( - contentBuilder, - parseResult.tree, - compilerConfig.completionInfo?.cursor?.index ?: -1 - ) + return@use contentBuilder + } + } - watch.log() + return super.parse(filename, pruned) + } - 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) }) - } - } + 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-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 index a6fd0d1431..e2f66f1a16 100644 --- 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 @@ -175,7 +175,10 @@ class JavaCompilerSessionImpl : IJavaCompilerSession { diagnosticProvider.clearTimestamp(file) } - override fun findSourceFilePath(module: ModuleProject, className: String): String? { + 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 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 index af91d0c00b..6bc2adef99 100644 --- 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 @@ -36,138 +36,140 @@ 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); - } + private static Kind kindFromExtension(String name) { + for (Kind candidate : Kind.values()) { + if (name.endsWith(candidate.extension)) { + return candidate; + } + } + return null; + } + + /** 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 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 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(path, 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 index 102a38471c..4e3d26bd95 100644 --- 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 @@ -1,19 +1,3 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU 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. * @@ -38,98 +22,99 @@ import org.slf4j.LoggerFactory import java.util.concurrent.Semaphore class SynchronizedTask { + @Volatile + @PublishedApi + internal var isCompiling = false - @Volatile - @PublishedApi - internal var isCompiling = false - - @PublishedApi - internal val semaphore = Semaphore(1) - - @PublishedApi - internal var task: CompileTask? = null - private set + @PublishedApi + internal val semaphore = Semaphore(1) - companion object { + @PublishedApi + internal var task: CompileTask? = null + private set - @PublishedApi - internal val log = LoggerFactory.getLogger(SynchronizedTask::class.java) - } + 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 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() - } - } + 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() } + 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 - } - } + 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 - } + fun setTask(task: CompileTask?) { + this.task = task + } - @get:Synchronized - val isBusy: Boolean - get() = isCompiling || semaphore.availablePermits() == 0 + @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 +/** +* **FOR INTERNAL USE ONLY!** +*/ + fun logStats() { + log.warn( + "[SynchronizedTask] isCompiling={} queuedLength={}", + isCompiling, + semaphore.queueLength, + ) + } +} diff --git a/lsp/java-compiler-impl/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 index 387d546cb4..d466a35601 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 4f12c45336..ef93f68954 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index d78a8e673d..47b88d5cd9 100644 --- a/lsp/java-compiler-impl/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 index 8b9886bb34..38ba99540d 100644 --- 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 @@ -31,29 +31,28 @@ import java.nio.file.Path * @author Akash Yadav */ class MultipleClassImportEditHandler( - private val classes: Set, - private val imported: Set, - file: Path + private val classes: Set, + private val imported: Set, + file: Path, ) : AdvancedJavaEditHandler(file) { + companion object { + private val log = LoggerFactory.getLogger(MultipleClassImportEditHandler::class.java) + } - 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) - } + 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-compiler-impl/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 index e50cf4ef32..7cb011d8e2 100644 --- a/lsp/java-compiler-impl/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 index d112687f7f..f642a7fc58 100644 --- 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 @@ -1,66 +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 { - return values().first { id == it.id } - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index af0ea85203..9aa9668a15 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 8c66b9ab8a..874ebbad9f 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 714f4914a5..5b806c5ad2 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 86830877c1..ac46694cc4 100644 --- a/lsp/java-compiler-impl/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 index 8a6ba9cc7d..9cf963496b 100644 --- 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 @@ -1,239 +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 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 ""; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 9f9f277c27..27bfc170c2 100644 --- 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 @@ -35,73 +35,91 @@ import org.slf4j.LoggerFactory * @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 - } + 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 index 921e638fe5..208dc866ac 100644 --- 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 @@ -29,41 +29,48 @@ import com.itsaky.androidide.treesitter.java.TSLanguageJava * @author Akash Yadav */ object TSMethodPruner { + private const val METHOD_BODIES_QUERY = "(method_declaration body: (block) @method.body)" - 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) - 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 - 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 + } - 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() - } - } - } - } + // +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, ' ') - } - } - } + 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-compiler-impl/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 index bc09de9837..f3897e2d27 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 870d494b0d..de9ff182a3 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 84950ef873..3b47c7b205 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index acdd03aa2a..ed39071d06 100644 --- a/lsp/java-compiler-impl/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 index a3f8c61690..0d7b746de6 100644 --- 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 @@ -1,119 +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)); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-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 index ae62e81758..c376059208 100644 --- 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 @@ -47,89 +47,87 @@ 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 ""; - } + 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 index abb6a33168..6ad6c2c3e1 100644 --- 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 @@ -1,318 +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 { - 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 - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 78671f4d67..b1241d73bf 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index aa80b2a453..e534477b94 100644 --- a/lsp/java-compiler-impl/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 index b799787b16..9b4826b08b 100644 --- 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 @@ -41,119 +41,119 @@ 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 + 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 index de4b9a3afc..7e0d29b2a4 100644 --- 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 @@ -65,327 +65,324 @@ public class SignatureProvider extends CancelableServiceProvider { - public static final SignatureHelp NOT_SUPPORTED = - new SignatureHelp(Collections.emptyList(), -1, -1); - private final CompilerProvider compiler; + 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; - } + 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) { - @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; - // 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; + }); + } - // 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 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()); - } + 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(); + } - @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 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; + } - @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)); + 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 + ")"); + } - if (type == null) { - return Collections.emptyList(); - } + 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); - 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; - } + if (!file.isPresent()) { + return; + } - 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; - } + final var parse = compiler.parse(file.get()); + final var source = FindHelper.findMethod(parse, className, methodName, erasedParameterTypes); + if (source == null) { + return; + } - @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; - } + final var path = Trees.instance(task.task).getPath(parse.root, source); + final var docTree = DocTrees.instance(task.task).getDocCommentTree(path); - @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; - } + if (docTree != null) { + info.setDocumentation(MarkdownHelper.asMarkupContent(docTree)); + } - @NonNull - private List parameters(@NonNull ExecutableElement method) { - abortIfCancelled(); - List list = new ArrayList<>(); - for (VariableElement p : method.getParameters()) { - list.add(parameter(p)); - } - return list; - } + info.setParameters(parametersFromSource(source)); + } - @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 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; + } - 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); + @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; + } - if (!file.isPresent()) { - return; - } + 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; + } - final var parse = compiler.parse(file.get()); - final var source = FindHelper.findMethod(parse, className, methodName, erasedParameterTypes); - if (source == null) { - return; - } + 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; + } - final var path = Trees.instance(task.task).getPath(parse.root, source); - final var docTree = DocTrees.instance(task.task).getDocCommentTree(path); + @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 (docTree != null) { - info.setDocumentation(MarkdownHelper.asMarkupContent(docTree)); - } + if (type == null) { + return Collections.emptyList(); + } - info.setParameters(parametersFromSource(source)); - } + 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 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 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 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 ParameterInformation parameter(@NonNull VariableElement p) { + abortIfCancelled(); + ParameterInformation info = new ParameterInformation(); + info.setLabel(ShortTypePrinter.NO_PACKAGE.print(p.asType())); + return info; + } - 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(); - } + @NonNull + private List parameters(@NonNull ExecutableElement method) { + abortIfCancelled(); + List list = new ArrayList<>(); + for (VariableElement p : method.getParameters()) { + list.add(parameter(p)); + } + return list; + } - 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; - } + @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 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; - } + @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 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; - } + 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 index 0b2f57916e..ce00b1d929 100644 --- 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 @@ -1,111 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index bf43a77cec..b8d896097c 100644 --- 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 @@ -1,403 +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 { - 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!! - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 3986cea4f4..768dee47c9 100644 --- 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 @@ -1,81 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b8434e9b6c..4f7a8a8bfb 100644 --- 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 @@ -1,446 +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 { - 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() -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 779bba385d..ce3fb9fed7 100644 --- 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 @@ -1,162 +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" - ) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 82ece497ba..069c137e67 100644 --- 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 @@ -1,174 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b6d1ed99cf..0281862b3d 100644 --- 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 @@ -1,227 +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 { - 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 - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index e6c48e71ac..940372ea53 100644 --- 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 @@ -1,188 +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 - 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 - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index fae50b8c27..65141620c8 100644 --- 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 @@ -40,72 +40,73 @@ import java.nio.file.Path * @author Akash Yadav */ class SnippetCompletionProvider( - cursor: Long, - completingFile: Path, - compiler: JavaCompilerService, - settings: IServerSettings + 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() - 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) } - // 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 + } - 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) } } - // add snippets for the current scope - snippetScope?.let { JavaSnippetRepository.snippets[it]?.let { list -> snippets.addAll(list) } } + val items = mutableListOf() - val items = mutableListOf() + for (snippet in snippets) { + val matchLevel = matchLevel(snippet.prefix, partial) + if (matchLevel == MatchLevel.NO_MATCH) { + continue + } - for (snippet in snippets) { - val matchLevel = matchLevel(snippet.prefix, partial) - if (matchLevel == MatchLevel.NO_MATCH) { - continue - } + items.add(snippetItem(snippet, matchLevel, partial, indent)) + } - items.add(snippetItem(snippet, matchLevel, partial, indent)) - } + return CompletionResult(items) + } - 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 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 - } + 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 index ba3a66dd96..c00f167571 100644 --- 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 @@ -1,126 +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 { - return (staticImport.contentEquals("*") || matchLevel(staticImport, partial) != NO_MATCH) - } - - private fun memberMatchesImport(staticImport: Name, member: Element): Boolean { - return staticImport.contentEquals("*") || staticImport.contentEquals(member.simpleName) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index b66912219c..372f2c7640 100644 --- 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 @@ -1,105 +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) - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 6f01deb224..abce3aefd8 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index e677208489..c3a2f27db0 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 5102b2eec9..65221adda7 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index da9a574de7..029be55873 100644 --- a/lsp/java-compiler-impl/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 index b2c7f5d6d7..21038fd1ad 100644 --- 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 @@ -32,70 +32,69 @@ public class AddException extends Rewrite { - final String className, methodName; - final String[] erasedParameterTypes; - final String exceptionType; + 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; - } + 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; - } + @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; - } + 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; - } + 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 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 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); - } + 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 + " "; - } + 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); - }); - } + 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-compiler-impl/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 index ae03d08be0..e69e09fa13 100644 --- a/lsp/java-compiler-impl/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 index 4fab2520ec..057f8e01de 100644 --- 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 @@ -32,48 +32,47 @@ public class AddSuppressWarningAnnotation extends Rewrite { - final String className, methodName; - final String[] erasedParameterTypes; + final String className, methodName; + final String[] erasedParameterTypes; - public AddSuppressWarningAnnotation( - String className, String methodName, String[] erasedParameterTypes) { - this.className = className; - this.methodName = methodName; - this.erasedParameterTypes = 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); - }); - } + @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 index 5369291828..4e8e4aaabc 100644 --- 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 @@ -37,49 +37,49 @@ import openjdk.source.util.Trees; public class ConvertFieldToBlock extends Rewrite { - final Path file; - final int position; + final Path file; + final int position; - public ConvertFieldToBlock(Path file, int position) { - this.file = file; - this.position = 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); - } + @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 index 6f23dc1fce..e054505d8c 100644 --- 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 @@ -35,64 +35,66 @@ import openjdk.source.util.Trees; public class ConvertVariableToStatement extends Rewrite { - final Path file; - final int position; + static VariableTree findVariable(ParseTask task, int position) { + return new FindVariableAtCursor(task.task).scan(task.root, position); + } - public ConvertVariableToStatement(Path file, int position) { - this.file = file; - this.position = 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; + } + } - @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); - } + final Path file; - static VariableTree findVariable(ParseTask task, int position) { - return new FindVariableAtCursor(task.task).scan(task.root, position); - } + final int 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; - } - } + 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 index bd3bffa87b..86e55c7ee7 100644 --- 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 @@ -57,209 +57,207 @@ 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; + 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; - } + 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; - } + @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 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 indent = EditorUtilKt.getIndentationString(); - final var isStatic = currentMethod.getModifiers().getFlags().contains(Modifier.STATIC) || - methodFinder.isStaticAccess(); + 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("}"); + 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(); + var insertText = insertTextBuilder.toString(); - final CompilationUnitTree compilationUnit; - final ClassTree enclosingClass; - final Position insertPoint; + 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)); - } + 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 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); - }); - } + 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; + 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); - } + // 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 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 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 guessParameterName(Tree argument, TypeMirror type) { + String fromTree = guessParameterNameFromTree(argument); + if (!fromTree.isEmpty()) { + return fromTree; + } - 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 + ")"; - } + String fromType = guessParameterNameFromType(type); + if (!fromType.isEmpty()) { + return fromType; + } - 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(); - } + argCount++; + return "param" + argCount; + } - private String guessParameterName(Tree argument, TypeMirror type) { - String fromTree = guessParameterNameFromTree(argument); - if (!fromTree.isEmpty()) { - return fromTree; - } + 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 ""; + } + } - String fromType = guessParameterNameFromType(type); - if (!fromType.isEmpty()) { - return fromType; - } + 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 ""; + } + } - argCount++; - return "param" + argCount; - } + 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 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 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 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 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 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 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"); + } } 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 index 2d4ba69ec3..4bd29c11f5 100644 --- 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 @@ -48,127 +48,127 @@ 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); - } + 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 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 index 5524ee3ef9..a6f40094cb 100644 --- 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 @@ -1,180 +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])); - }); - } - - 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)); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 0adbf192dd..fa4116c91f 100644 --- a/lsp/java-compiler-impl/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 index e98aeafcb3..3289bef9b3 100644 --- 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 @@ -45,159 +45,158 @@ 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); - } - } + 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 (contents.charAt(i + 1) == ' ') { + return i + 2; + } else { + return i + 1; + } + } + } + return -1; + } } diff --git a/lsp/java-compiler-impl/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 index 8488bd29e2..4fdb0e19eb 100644 --- a/lsp/java-compiler-impl/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 index c9ecc85d13..eb2e84fdd8 100644 --- 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 @@ -1,84 +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 var CANCELLED = emptyMap>() - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 3fbf10daaf..620e270158 100644 --- 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 @@ -36,110 +36,108 @@ * @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; - } - } + 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-compiler-impl/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 index 1646c2c1a2..0aa8721d31 100644 --- a/lsp/java-compiler-impl/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 index e7267416c9..3a87e762ee 100644 --- 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 @@ -1,208 +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 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); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index a49b46d6f9..2198e6ba36 100644 --- 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 @@ -1,208 +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 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(); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 2d277a0fd7..54b2016b29 100644 --- a/lsp/java-compiler-impl/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 index 3e7a2a066f..b5e47428c2 100644 --- 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 @@ -52,188 +52,191 @@ 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; - } + 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 index 7f0c5c83fb..d2f17b8999 100644 --- 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 @@ -1,789 +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 { - 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() -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 4a2ec418e9..29b591d457 100644 --- 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 @@ -1,109 +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 { - 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() - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index fa687be4cc..e83a1bce61 100644 --- 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 @@ -48,174 +48,177 @@ 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; - } + 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 index 803a3c1fb1..8e415d4636 100644 --- 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 @@ -1,105 +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); - } - } - - 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); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 309d5fe1fb..3e517ae421 100644 --- a/lsp/java-compiler-impl/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 index b7990091e5..73d9934af8 100644 --- 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 @@ -30,64 +30,64 @@ 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); + } - // 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 index 14a63d33b0..17e9f4a7b1 100644 --- 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 @@ -16,92 +16,96 @@ 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(); - } + 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-compiler-impl/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 index c3362e37b9..8c79ff397a 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index a9e67596a0..67cebe93ec 100644 --- a/lsp/java-compiler-impl/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 index 3c5385c9d8..e3d5a7c6ae 100644 --- 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 @@ -1,288 +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 { - - // 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()); - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 5375b05d0b..3ef4b148e5 100644 --- a/lsp/java-compiler-impl/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 index bf9d3383ce..5c1c9bc023 100644 --- 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 @@ -1,400 +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) - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 434f5bbec1..5c370986ba 100644 --- a/lsp/java-compiler-impl/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 index fc973ac116..10af46455a 100644 --- 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 @@ -38,117 +38,117 @@ */ 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; - } + 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 index 41deeb3b72..d4691d4d20 100644 --- 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 @@ -33,109 +33,109 @@ 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; - } + // 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 index e90303922e..2960d9587e 100644 --- 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 @@ -29,52 +29,52 @@ public class FindInvocationAt extends TreePathScanner { - private final JavacTask task; - private final ICancelChecker cancelChecker; - private CompilationUnitTree root; + private final JavacTask task; + private final ICancelChecker cancelChecker; + private CompilationUnitTree root; - public FindInvocationAt(JavacTask task, ICancelChecker cancelChecker) { - this.task = task; - this.cancelChecker = cancelChecker; - } + 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 reduce(TreePath a, TreePath b) { + cancelChecker.abortIfCancelled(); + if (a != null) { + return a; + } + return b; + } - @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 visitCompilationUnit(CompilationUnitTree t, Long find) { + cancelChecker.abortIfCancelled(); + root = t; + return reduce(super.visitCompilationUnit(t, find), getCurrentPath()); + } - @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 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 reduce(TreePath a, TreePath b) { - cancelChecker.abortIfCancelled(); - if (a != null) { - return a; - } - return b; - } + @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-compiler-impl/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 index 7314cddc2f..f6c61e7d33 100644 --- a/lsp/java-compiler-impl/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 index 734b119482..cb10572d8c 100644 --- 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 @@ -37,219 +37,220 @@ 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; - } + 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-compiler-impl/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 index 28243dc7f1..a7364729ed 100644 --- a/lsp/java-compiler-impl/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 index c5a76f3bfe..17dd1cb61a 100644 --- 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 @@ -37,101 +37,104 @@ public class FindNameAt extends TreePathScanner { - private final JavacTask task; - private CompilationUnitTree root; - private ClassTree surroundingClass; + private final JavacTask task; + private CompilationUnitTree root; + private ClassTree surroundingClass; - public FindNameAt(CompileTask task) { - this.task = task.task; - } + 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 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 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 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 visitIdentifier(IdentifierTree t, Long find) { + if (contains(t, t.getName(), find)) { + return getCurrentPath(); + } + return super.visitIdentifier(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 visitMemberReference(MemberReferenceTree t, Long find) { + if (contains(t, t.getName(), find)) { + return getCurrentPath(); + } + return super.visitMemberReference(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 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 visitMemberReference(MemberReferenceTree t, Long find) { - if (contains(t, t.getName(), find)) { - return getCurrentPath(); - } - return super.visitMemberReference(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 visitIdentifier(IdentifierTree t, Long find) { - if (contains(t, t.getName(), find)) { - return getCurrentPath(); - } - return super.visitIdentifier(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; - } + 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-compiler-impl/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 index 1c843344c6..28996a36b1 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 7f45f1b124..9dd251ec82 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 00df0274fc..d355aacb7a 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 0838d33ee4..071e844745 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 057c6be63f..20a7de1bb1 100644 --- a/lsp/java-compiler-impl/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-compiler-impl/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 index 5098e40359..16b7665aed 100644 --- a/lsp/java-compiler-impl/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 index 9313f2ccb4..ce5111c556 100644 --- 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 @@ -34,64 +34,66 @@ import org.slf4j.LoggerFactory * * @author Akash Yadav */ -class MethodRangeScanner(val task: JavacTaskImpl) : - TreePathScanner>>() { +class MethodRangeScanner( + val task: JavacTaskImpl, +) : TreePathScanner>>() { + var root: CompilationUnitTree? = null + var lines: LineMap? = null + val pos = Trees.instance(task).sourcePositions - var root: CompilationUnitTree? = null - var lines: LineMap? = null - val pos = Trees.instance(task).sourcePositions + companion object { + private val log = LoggerFactory.getLogger(MethodRangeScanner::class.java) + } - companion object { + override fun visitCompilationUnit( + node: CompilationUnitTree?, + p: MutableList>?, + ) { + this.root = node + this.lines = node?.lineMap + return super.visitCompilationUnit(node, p) + } - private val log = LoggerFactory.getLogger(MethodRangeScanner::class.java) - } + 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 + } - override fun visitCompilationUnit( - node: CompilationUnitTree?, - p: MutableList>? - ) { - this.root = node - this.lines = node?.lineMap - return super.visitCompilationUnit(node, p) - } + val start = getStartPosition(node) + val end = getEndPosition(node) - 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 - } + if (start == null || end == null) { + log.warn("Method '{}' skipped. Invalid position.", node.name) + return + } - val start = getStartPosition(node) - val end = getEndPosition(node) + list.add(Pair.create(Range(start, end), currentPath)) + } - if (start == null || end == null) { - log.warn("Method '{}' skipped. Invalid position.", node.name) - return - } + fun getStartPosition(node: MethodTree): Position? { + val position = this.pos.getStartPosition(this.root!!, node) + if (position.toInt() == -1) { + return null + } + return getPosition(position) + } - list.add(Pair.create(Range(start, end), currentPath)) - } + fun getEndPosition(node: MethodTree): Position? { + val position = this.pos.getEndPosition(this.root!!, node) + if (position.toInt() == -1) { + return null + } + return getPosition(position) + } - 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() } - } + 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 index ecb1ef5932..8893f14114 100644 --- 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 @@ -1,145 +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(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); - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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-compiler-impl/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 index 5094206c48..f6015ee5aa 100644 --- a/lsp/java-compiler-impl/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 index f4e72ef228..ab24ead76b 100644 --- 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 @@ -26,50 +26,50 @@ import openjdk.source.util.Trees; public class PruneMethodBodies extends TreeScanner { - private final JavacTask task; - private final StringBuilder buf = new StringBuilder(); - private CompilationUnitTree root; + private final JavacTask task; + private final StringBuilder buf = new StringBuilder(); + private CompilationUnitTree root; - public PruneMethodBodies(JavacTask task) { - this.task = task; - } + public PruneMethodBodies(JavacTask task) { + this.task = task; + } - @Override - public StringBuilder reduce(StringBuilder a, StringBuilder b) { - return buf; - } + @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 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; - } + @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-compiler-impl/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 index 3fc4ff7219..4fe9c86e0c 100644 --- a/lsp/java-compiler-impl/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 index c40a2e9cbf..26512c1cab 100644 --- 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 @@ -1,74 +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") - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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 index 0de93074bb..ebe8ef6fde 100644 --- 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 @@ -30,82 +30,82 @@ import java.time.Instant @RunWith(RobolectricTestRunner::class) @Config(manifest = Config.NONE) class CompilerTest { + @Before + fun setup() { + JavaLSPTest.setup() + } - @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() - @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() + } - 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() } + } + } - threads.forEach { it.join() } - } - } + @Test + fun testClosedFileChannel() { + JavaLSPTest.apply { + openFile("completion/MembersCompletionTest") - @Test - fun testClosedFileChannel() { - JavaLSPTest.apply { - openFile("completion/MembersCompletionTest") + Thread { getCompiler().compile(file!!).run { delay(500) } }.start() + Thread { getCompiler().compile(file!!).run { delay(200) } }.start() - Thread { getCompiler().compile(file!!).run { delay(500) } }.start() - Thread { getCompiler().compile(file!!).run { delay(200) } }.start() + getCompiler().compile(file!!).run { assertThat(it.diagnostics).isNotEmpty() } + } + } - getCompiler().compile(file!!).run { assertThat(it.diagnostics).isNotEmpty() } - } - } + private fun delay(millis: Long) { + Thread.sleep(millis) + } - private fun delay(millis: Long) { - Thread.sleep(millis) - } + @Test + fun testConcurrentAccess() { + JavaLSPTest.apply { + openFile("completion/MembersCompletionTest") - @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" }, + ) + } - 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() } - 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() } - } - } + 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 index 75ad2cb63e..fb25f999c4 100644 --- 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 @@ -47,123 +47,122 @@ import org.robolectric.annotation.Config @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) - } - } + @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 index 7a508f8e94..051305eff9 100644 --- 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 @@ -1,99 +1,98 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU 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") - } - } - - 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 } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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") + } + } + + 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 index 976afb669e..20f1780029 100644 --- 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 @@ -33,76 +33,99 @@ 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) - } + @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-compiler-impl/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 index 5eb17a0747..7f46a89a2b 100644 --- a/lsp/java-compiler-impl/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 @@ -37,28 +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 { - 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() - } - } -} \ 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/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 bbec566f97..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 @@ -153,7 +153,7 @@ internal class JavaDebugAdapter : _listenerState?.invalidate() listenerThread?.interrupt() - + _listenerState = ListenerState( client = client, @@ -161,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 @@ -361,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 @@ -370,8 +371,9 @@ internal class JavaDebugAdapter : qualifiedName = qualifiedName, suspendPolicy = breakpoint.suspendPolicy.asJdiInt(), ) + } - is MethodBreakpoint -> + is MethodBreakpoint -> { specList.createBreakpoint( source = breakpoint.source, methodId = breakpoint.methodId, @@ -379,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 = @@ -392,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) + } } }, ) @@ -638,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 1335aac259..2a3660b818 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 @@ -19,7 +19,10 @@ private val logger = LoggerFactory.getLogger("ModelUtilsKt") * 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, session: IJavaCompilerSession?): LspLocation { +fun Location.asLspLocation( + useDeclTypeName: Boolean = true, + session: IJavaCompilerSession?, +): LspLocation { val projectManager = ProjectManagerImpl.getInstance() val path = session?.let { s -> 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 fcb0046fe1..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,67 +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.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; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU 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/subprojects/javac-fs/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 index 360d933927..be49dea750 100644 --- a/subprojects/javac-fs/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-fs/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 index aa7f660b32..e109540ddd 100644 --- a/subprojects/javac-fs/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 index e11f4b3187..bf95504f3b 100644 --- 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 @@ -31,51 +31,49 @@ import java.nio.file.Path * @author Akash Yadav */ class CachedJarFileSystem( - provider: ZipFileSystemProvider?, - zfpath: Path?, - env: MutableMap? + provider: ZipFileSystemProvider?, + zfpath: Path?, + env: MutableMap?, ) : ZipFileSystem(provider, zfpath, env) { + companion object { + private val log = LoggerFactory.getLogger(CachedJarFileSystem::class.java) + } - companion object { - private val log = LoggerFactory.getLogger(CachedJarFileSystem::class.java) - } + internal val packages = mutableMapOf() - internal val packages = mutableMapOf() + override fun close() { + // Do nothing + // This is called manually by the Java LSP + } - 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) + } + } - @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 + } - fun storeJARPackageDir(dir: Path?): Boolean { - if (isValid(dir?.fileName)) { - packages[RelativeDirectory(rootDir.relativize(dir!!).toString())] = dir - return true - } + return false + } - 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) - } - } + 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 index 8edfaf87a0..6194028655 100644 --- 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 @@ -33,67 +33,71 @@ import kotlin.io.path.pathString * @author Akash Yadav */ object CachingJarFileSystemProvider : JarFileSystemProvider() { - private val cachedFs = ConcurrentHashMap() + private val cachedFs = ConcurrentHashMap() - private val log = LoggerFactory.getLogger(CachingJarFileSystemProvider::class.java) + 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) - } + 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 newFileSystem(path: Path): FileSystem? = newFileSystem(path, mutableMapOf()) - fun clearCache() { - cachedFs.values.forEach(this::closeFs) - cachedFs.clear() - } + fun clearCache() { + cachedFs.values.forEach(this::closeFs) + cachedFs.clear() + } - fun clearCaches(predicate: (Path) -> Boolean) { - return clearCachesForPaths { predicate(Paths.get(it)) } - } + 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 - } + 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) - } - } + if (toRemove.isNotEmpty()) { + toRemove.forEach(this::clearCache) + } + } - fun clearCache(path: Path) { - clearCache(path.normalize().pathString) - } + 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) - } - } + 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 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 - } + 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-fs/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 index 0606a5b523..3c9aea08da 100644 --- a/subprojects/javac-fs/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 + } } From 561b7937a50a79d0c78d790b537245a128276ff0 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Thu, 6 Aug 2026 22:45:26 -0700 Subject: [PATCH 14/21] ADFA-5053: Fix ADR 0012's javapoet duplicate-class-identity claim The ADR said all three of jdk-compiler/javapoet/google-java-format needed their api() dependency on java-compiler fixed to compileOnly, but the actual diff deliberately left javapoet's api() unchanged (correctly -- javapoet stays fully resident, so it has no isolated consumer to leak into). Only jdk-compiler and google-java-format needed the fix. Caught by an architecture-review pass before opening the PR. --- docs/adr/0012-lazy-load-javac-via-dexclassloader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md index 2107fb6251..3a2276b515 100644 --- a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md +++ b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md @@ -29,7 +29,7 @@ Investigation found this coupling narrower than it first looked: none of `CacheF - **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, but here it took three separate fixes to fully close: `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), and `javapoet`'s and `google-java-format`'s own composite-build files had the identical pattern. All three needed fixing at the source, not just at the `lsp/java-compiler-impl` consumer level. A fourth, unrelated 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`. +**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. From 513a2b2ab2e8ed62d284e8a3601f482dbd973316 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 12:34:40 -0700 Subject: [PATCH 15/21] ADFA-5053: Stop duplicating tree-sitter/sora-editor into the javac carrier Both were declared implementation instead of compileOnly, duplicating these resident libraries -- including native .so payloads -- into the isolated carrier dex alongside the identical resident copies, defeating part of the dex-size point of the isolation and risking UnsatisfiedLinkError if both classloaders load the same-named .so. The carrier's DexClassLoader resolves them from its parent (the resident classloader) instead. Also adds kotlinx-coroutines-core as compileOnly, needed by a follow-up commit that translates CancelAbort into CancellationException before it crosses back out of the isolated session. --- lsp/java-compiler-impl/build.gradle.kts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lsp/java-compiler-impl/build.gradle.kts b/lsp/java-compiler-impl/build.gradle.kts index b222370660..184ca52956 100644 --- a/lsp/java-compiler-impl/build.gradle.kts +++ b/lsp/java-compiler-impl/build.gradle.kts @@ -42,16 +42,27 @@ kapt { dependencies { kapt(projects.annotationProcessors) - implementation(libs.androidide.ts) - implementation(libs.androidide.ts.java) + // 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)) - implementation(libs.common.editor) + compileOnly(libs.common.editor) implementation(libs.common.javaparser) implementation(libs.androidx.annotation) implementation(libs.google.guava) implementation(libs.google.gson) implementation(libs.androidx.core.ktx) implementation(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 @@ -99,4 +110,7 @@ dependencies { // 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) } From 41abcc79365a2bc1d3a1a2eddeeb1f3db43fe07d Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 12:35:13 -0700 Subject: [PATCH 16/21] ADFA-5053: Close concurrency and lifecycle gaps in the carrier isolation Six independent issues found during code review of the javac carrier split: - JavaCompilerImpl.parse() closed the TSParseResult it got from TSJavaParser's own LRU cache via .use{} -- since the cache returns the same instance on a hit, this double-closed (and use-after-freed) the underlying native parse tree on the next completion request for an unchanged file. Stop closing it; the cache already owns its lifecycle. - JavaLanguageServer.complete()/formatCode() resolved the compiler session and released compilerLifecycleLock before using it, unlike onContentChange() -- a concurrent project reset could destroy() the session's compilers in the gap. Now hold the lock across both steps, same as onContentChange(). The five suspend methods (findReferences etc.) can't use the same fix: the Kotlin compiler rejects a suspension point inside a lock's critical section outright. Documented why, since the residual race there is narrower than it looks (JavaCompilerProvider's map access is already synchronized). - JavaLanguageServer.shutdown() gated its teardown on compilerLifecycle == INITIALIZED, but setupWithProject() sets state back to PENDING on every project switch even once the carrier's already loaded -- a switch queued without a .java-file interaction yet left a live session's DexClassLoader leaked. Gate on session existence instead. - JavaCompilerLoader.close() mutated the session field without synchronized(this), unlike getOrCreateSession(), risking a race between the two. - JavaCompilerSessionImpl.close() never destroyed NO_MODULE_COMPILER, unlike resetProject() -- now each session owns its own discardable DexClassLoader, so nothing could reach back to release it once replaced. - CodeActionsMenu.children was a plain, unsynchronized LinkedHashSet mutated from LSP-dispatch threads (session register/unregister) concurrently with the UI thread rendering the menu. Switched to CopyOnWriteArraySet. - CachedJarFileSystem.packages was a plain, unsynchronized map written by resident classpath indexing and read by the isolated compiler through the same shared provider -- now crossing the classloader boundary too, not just threads. Switched to ConcurrentHashMap. - javac's cancellation signal, CancelAbort, is thrown deep inside the isolated fork and is only classloader-identity-safe to recognize on the side that threw it -- IDEEditor's isCancelled() check (which looks for CancellationException) silently stopped recognizing it once javac moved into the carrier, logging routine cancellations as real failures. JavaCompilerSessionImpl now translates CancelAbort into CancellationException before it crosses back to resident code. --- .../actions/locations/CodeActionsMenu.kt | 112 ++++++++++-------- .../lsp/java/compiler/JavaCompilerImpl.kt | 21 ++-- .../java/compiler/JavaCompilerSessionImpl.kt | 62 +++++++--- .../androidide/lsp/java/JavaLanguageServer.kt | 34 +++++- .../lsp/java/loader/JavaCompilerLoader.kt | 10 +- .../javac/services/fs/CachedJarFileSystem.kt | 7 +- 6 files changed, 160 insertions(+), 86 deletions(-) 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/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 index c23a165ce4..4e086453ca 100644 --- 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 @@ -65,18 +65,21 @@ class JavaCompilerImpl( withStopWatch("${if (file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch -> val contentBuilder = StringBuilder(content) - return@withStopWatch TSJavaParser.parse(file).use { parseResult -> + // 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, - ) + prune( + contentBuilder, + parseResult.tree, + compilerConfig.completionInfo?.cursor?.index ?: -1, + ) - watch.log() + watch.log() - return@use contentBuilder - } + contentBuilder } return super.parse(filename, pruned) 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 index e2f66f1a16..3c48189621 100644 --- 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 @@ -17,6 +17,7 @@ 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 @@ -50,6 +51,7 @@ 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 @@ -96,6 +98,10 @@ class JavaCompilerSessionImpl : IJavaCompilerSession { } 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() } @@ -130,27 +136,49 @@ class JavaCompilerSessionImpl : IJavaCompilerSession { return completionProvider.complete(params) } - override suspend fun findReferences(params: ReferenceParams): ReferenceResult { - val compiler = getCompiler(params.file) - return ReferenceProvider(compiler, params.cancelChecker).findReferences(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 { - val compiler = getCompiler(params.file) - return DefinitionProvider(compiler, settings, params.cancelChecker).findDefinition(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 { - val compiler = getCompiler(params.file) - return JavaSelectionProvider(compiler).expandSelection(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 { - val compiler = getCompiler(params.file) - return SignatureProvider(compiler, params.cancelChecker).signatureHelp(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 = diagnosticProvider.analyze(file) + 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 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 915938fc0e..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 @@ -138,8 +138,14 @@ class JavaLanguageServer : ILanguageServer { 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. - if (compilerLifecycle == CompilerLifecycle.INITIALIZED) { + // 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. @@ -243,13 +249,30 @@ class JavaLanguageServer : ILanguageServer { session } + // 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 { if (params == null || !settings.completionsEnabled()) { return CompletionResult.EMPTY } - return ensureProjectReset()?.complete(params) ?: CompletionResult.EMPTY + return compilerLifecycleLock.withLock { + ensureProjectReset()?.complete(params) ?: CompletionResult.EMPTY + } } + // 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 { if (!settings.referencesEnabled()) { return ReferenceResult(emptyList()) @@ -296,7 +319,10 @@ class JavaLanguageServer : ILanguageServer { } } - override fun formatCode(params: FormatCodeParams?): CodeFormatResult = ensureProjectReset()?.formatCode(params) ?: CodeFormatResult.NONE + override fun formatCode(params: FormatCodeParams?): CodeFormatResult = + compilerLifecycleLock.withLock { + ensureProjectReset()?.formatCode(params) ?: CodeFormatResult.NONE + } override fun handleFailure(failure: LSPFailure?): Boolean = when (failure!!.type) { 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 index fef1fb9730..066bf67a1c 100644 --- 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 @@ -28,7 +28,9 @@ 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 - * `KotlinCompilerLoader`'s construction (ADR 0011), which mirrors `PluginLoader`'s. + * `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, @@ -95,8 +97,10 @@ class JavaCompilerLoader( fun currentSession(): IJavaCompilerSession? = session fun close() { - session?.close() - session = null + synchronized(this) { + session?.close() + session = null + } } companion object { 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 index bf95504f3b..c2cf7d3f8f 100644 --- 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 @@ -24,6 +24,7 @@ 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. @@ -39,7 +40,11 @@ class CachedJarFileSystem( private val log = LoggerFactory.getLogger(CachedJarFileSystem::class.java) } - internal val packages = mutableMapOf() + // 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 From c54d59ce209b98f587699313d19fb9d72ec66a13 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 12:35:43 -0700 Subject: [PATCH 17/21] ADFA-5053: Fix correctness bugs in edit/rewrite handlers and debug paths - SourceFileObject.equals() compared paths via the live Files.isSameFile() check while hashCode() hashed the raw Path, violating the equals/hashCode contract -- two objects Files.isSameFile() considered equal (e.g. a symlink, or a relative vs. canonicalized path) could land in different hash buckets, silently breaking the compile-cache map's lookups. Both now key off a single canonicalPath resolved once via toRealPath(). - MultipleClassImportEditHandler computed each new import's position independently against the same pre-edit AST, then applied them in sequence against the same live buffer -- an earlier insertion shifts every line after it, invalidating a later edit's pre-computed position. Apply bottom-to-top instead: inserting at a lower line never shifts anything above it. - Four unguarded edge cases in new rewrite handlers that threw instead of cancelling gracefully: GenerateRecordConstructor NPEs on an unresolvable type element/tree; CreateMissingMethod threw when a call sat in a field or static initializer (no enclosing method) and StringIndexOutOfBounds on an anonymous class's empty simple name; RemoveException read one past the end of the buffer when a trailing comma was its last character. - ModelUtils.asLspLocation()'s fallback treated JDI's sourcePath() (a package-relative string) as a real filesystem path when no compiler session was available yet -- e.g. the very first breakpoint hit in a session, before any .java file had loaded the carrier -- so File(path) silently failed to open. Resolve it against each module's compile source directories first, which works without a session; log clearly if that also fails instead of silently handing back an unusable path. --- .../lsp/java/compiler/SourceFileObject.java | 31 +++++++++++------ .../edits/MultipleClassImportEditHandler.kt | 9 ++++- .../lsp/java/rewrite/CreateMissingMethod.java | 18 ++++++++-- .../rewrite/GenerateRecordConstructor.java | 8 +++++ .../lsp/java/rewrite/RemoveException.java | 2 +- .../lsp/java/debug/utils/ModelUtils.kt | 34 ++++++++++++++++--- 6 files changed, 83 insertions(+), 19 deletions(-) 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 index 6bc2adef99..e0e68d1f6a 100644 --- 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 @@ -21,13 +21,13 @@ 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.Files; import java.nio.file.Path; import java.time.Instant; import java.util.Objects; @@ -45,8 +45,23 @@ private static Kind kindFromExtension(String name) { 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; @@ -61,6 +76,7 @@ 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; } @@ -79,14 +95,9 @@ public boolean equals(final Object o) { 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; - } + return Objects.equals(this.canonicalPath, that.canonicalPath) + && Objects.equals(contents, that.contents) + && Objects.equals(modified, that.modified); } @Override @@ -128,7 +139,7 @@ public NestingKind getNestingKind() { @Override public int hashCode() { - return Objects.hash(path, contents, modified); + return Objects.hash(canonicalPath, contents, modified); } @Override 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 index 38ba99540d..d5c7e9ac8f 100644 --- 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 @@ -52,7 +52,14 @@ class MultipleClassImportEditHandler( 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(edits, editor) + .performEdits(orderedEdits, editor) } } 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 index 86e55c7ee7..f96361fbef 100644 --- 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 @@ -83,12 +83,15 @@ public Map rewrite(@NonNull CompilerProvider compiler) { 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.getModifiers().getFlags().contains(Modifier.STATIC) || + final var isStatic = (currentMethod != null + && currentMethod.getModifiers().getFlags().contains(Modifier.STATIC)) || methodFinder.isStaticAccess(); insertTextBuilder.append( @@ -115,9 +118,12 @@ public Map rewrite(@NonNull CompilerProvider compiler) { 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, surroundingMethod(path)); + insertPoint = insertAfter(task.task, compilationUnit, currentMethod); } final int indentSpaces = indent(task.task, compilationUnit, enclosingClass) + EditorPreferences.INSTANCE.getTabSize(); @@ -209,7 +215,12 @@ private String guessParameterNameFromTree(Tree argument) { 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 ""; @@ -251,6 +262,7 @@ private ClassTree surroundingClass(TreePath call) { 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) { @@ -258,6 +270,6 @@ private MethodTree surroundingMethod(TreePath call) { } call = call.getParentPath(); } - throw new RuntimeException("No surrounding method"); + 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 index 4bd29c11f5..e822a5db56 100644 --- 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 @@ -71,7 +71,15 @@ public Map rewrite(@NonNull CompilerProvider compiler) { 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); 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 index 3289bef9b3..f71b1e2e24 100644 --- 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 @@ -190,7 +190,7 @@ 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) == ' ') { + if (i + 1 < contents.length() && contents.charAt(i + 1) == ' ') { return i + 2; } else { return i + 1; 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 2a3660b818..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 @@ -6,6 +6,7 @@ import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.api.ModuleProject import com.sun.jdi.Location import org.slf4j.LoggerFactory +import java.io.File import com.itsaky.androidide.lsp.debug.model.Location as LspLocation private val logger = LoggerFactory.getLogger("ModelUtilsKt") @@ -62,10 +63,23 @@ fun Location.asLspLocation( 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( @@ -76,3 +90,15 @@ fun Location.asLspLocation( 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 From 84946939334b5b285b0e7ca47c7ae8e8232901ce Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 12:36:11 -0700 Subject: [PATCH 18/21] ADFA-5053: Add missing test coverage found during code review - JavaCompletionProviderTest's members() was missing its @Test annotation, so JUnit silently skipped it -- any regression in that completion path would have passed CI unnoticed. - No test exercised JavaCompilerLoader at all. Added coverage for close()/currentSession()'s contract when no session was ever created. This does not cover the getOrCreateSession()-vs-close() race the synchronization fix in this branch addresses, or JavaLanguageServer's CompilerLifecycle state machine more broadly: 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) -- the same constraint that made JavaCompletionProviderTest bypass JavaLanguageServer entirely. Closing that gap needs either a DI seam for the classloader construction or an on-device instrumented test. --- .../providers/JavaCompletionProviderTest.kt | 1 + .../lsp/java/loader/JavaCompilerLoaderTest.kt | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lsp/java/src/test/java/com/itsaky/androidide/lsp/java/loader/JavaCompilerLoaderTest.kt 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 index 051305eff9..815ade5642 100644 --- 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 @@ -47,6 +47,7 @@ class JavaCompletionProviderTest { } } + @Test fun members() { JavaLSPTest.apply { // Complete members of String 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()) + } +} From 1d1313eb570d1999a62d60f4ff2e81e97a44ca2d Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 12:36:39 -0700 Subject: [PATCH 19/21] ADFA-5053: Fix ADR 0012's ADR 0011 precedent claim, document ReusableContext ADR 0012 and JavaCompilerLoader's doc comment cited ADR 0011 and KotlinCompilerLoader as already-established precedent ("mirrors ADR 0011 exactly"), but neither exists on this branch: ADFA-5010 (the sibling ticket that introduces them) is a separate, unmerged branch. Added a note clarifying the real relationship and pointing at the actual existing precedent both tickets extend, PluginLoader. Also documents why ReusableContext extends Context (isolated extending resident) accessing its protected ht/key() members doesn't need the same public-widening treatment ADR 0012 already applied to three sibling-access cases: protected access via inheritance is governed by a different JVMS 5.4.4 rule than same-package-sibling access, with no runtime-package/ classloader-identity condition attached. No code change -- confirmed safe, not a bug. --- ...0012-lazy-load-javac-via-dexclassloader.md | 10 +- .../services/compiler/ReusableContext.kt | 178 +++++++++--------- 2 files changed, 103 insertions(+), 85 deletions(-) diff --git a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md index 3a2276b515..d34bda2096 100644 --- a/docs/adr/0012-lazy-load-javac-via-dexclassloader.md +++ b/docs/adr/0012-lazy-load-javac-via-dexclassloader.md @@ -4,6 +4,14 @@ - **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). @@ -31,7 +39,7 @@ Investigation found this coupling narrower than it first looked: none of `CacheF **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. +**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 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 + } + } } From d28f1ad7e7b0006d8a7e40b3ef57003f96e90da2 Mon Sep 17 00:00:00 2001 From: David Schachter Date: Fri, 7 Aug 2026 18:42:31 -0700 Subject: [PATCH 20/21] ADFA-5053: Fix flaky copyJavaCompilerCarrierToAssets CI failures CI's "Build Universal APK" check has failed 3/3 times on this branch (before and after this batch of fixes) with: Property '$1' specifies file '.../java-compiler-carrier-v8-release-unsigned.apk' which doesn't exist. Reproduced locally: packageV8Release completes and reports success, but the plain APK file is intermittently missing from disk immediately afterward. The task only had dependsOn(":...:assembleV8Release") -- a task-ordering hint, not a real value-based dependency -- wired to a hardcoded path guessing the AGP-produced filename ("-unsigned" suffix included). That's exactly the shape of bug that races the file's own write-to-disk on some environments even though dependsOn ordering is satisfied. Fixed by exposing the release variant's real APK output directory via AGP's variant artifacts API (variant.artifacts.get(SingleArtifact.APK)) instead of a hardcoded path -- this ties Gradle's dependency tracking to the actual producing task's Provider, and also stops assuming the "-unsigned" filename, which was never guaranteed to stay accurate. Verified with 5 consecutive clean (--no-build-cache --rerun-tasks) local rebuilds, plus a full :app:assembleV8Debug sanity build. --- app/build.gradle.kts | 28 +++++++++++++------ .../java-compiler-carrier/build.gradle.kts | 12 ++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ee24d2a57..6640c9d7e5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -424,18 +424,30 @@ tasks.register("downloadDocDb") { // 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") { - // See evaluationDependsOn(":subprojects:java-compiler-carrier") above for why this avoids - // project(":subprojects:java-compiler-carrier").layout... here. + // 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") - val sourceFile = - rootProject.layout.projectDirectory - .dir("subprojects/java-compiler-carrier/build/outputs/apk/v8/release") - .file("java-compiler-carrier-v8-release-unsigned.apk") + @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.file(sourceFile) + inputs.dir(sourceDir) outputs.file(destFile) doLast { - sourceFile.asFile.copyTo(destFile.asFile, overwrite = true) + val apkFile = + sourceDir + .get() + .asFileTree + .matching { include("*.apk") } + .singleFile + apkFile.copyTo(destFile.asFile, overwrite = true) } } diff --git a/subprojects/java-compiler-carrier/build.gradle.kts b/subprojects/java-compiler-carrier/build.gradle.kts index 33c2e05cb0..365069ec6a 100644 --- a/subprojects/java-compiler-carrier/build.gradle.kts +++ b/subprojects/java-compiler-carrier/build.gradle.kts @@ -15,6 +15,7 @@ * along with AndroidIDE. If not, see . */ +import com.android.build.api.artifact.SingleArtifact import com.itsaky.androidide.build.config.BuildConfig plugins { @@ -40,3 +41,14 @@ android { 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) + } +} From 17e14656c2eea11e2c3d9cf74fae9b36360b6c2a Mon Sep 17 00:00:00 2001 From: David Schachter Date: Sat, 8 Aug 2026 13:48:44 -0700 Subject: [PATCH 21/21] ADFA-5053: Stop declaring 5 resident libraries as implementation in java-compiler-impl androidx.annotation, guava, gson, androidx.core.ktx, and kotlin-stdlib were all `implementation` despite every one already being resident (loaded by the parent classloader by the time the carrier's DexClassLoader runs) -- the same pattern already used correctly for androidide.ts/common.editor two lines above. Changed all five to compileOnly. Verified via a clean rebuild that this alone doesn't shrink java-compiler-carrier.apk: androidx.core's resources and the other four libraries also arrive through implementation(javacServices), which pulls in :common (and guava, kotlin-stdlib) on its own account, independent of what this module declares directly. That's a separate, deeper fix blocked by an AGP consistent-classpath conflict -- tracked as its own follow-up. This change is still correct and harmless on its own terms; it just isn't where the carrier's bytes are. --- lsp/java-compiler-impl/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lsp/java-compiler-impl/build.gradle.kts b/lsp/java-compiler-impl/build.gradle.kts index 184ca52956..77f7c52f06 100644 --- a/lsp/java-compiler-impl/build.gradle.kts +++ b/lsp/java-compiler-impl/build.gradle.kts @@ -53,11 +53,11 @@ dependencies { implementation(platform(libs.sora.bom)) compileOnly(libs.common.editor) implementation(libs.common.javaparser) - implementation(libs.androidx.annotation) - implementation(libs.google.guava) - implementation(libs.google.gson) - implementation(libs.androidx.core.ktx) - implementation(libs.common.kotlin) + 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