mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
build: reorganize and reconfigure modules to resolve dependency resolution errors
Gradle, for some reason, does not the path of modules to identify them uniquely. Instead, the 'build id' is used to do so, which consists of the maven group and artifact ID. So, even if two modules are located in separate directories, but have the name group and artifact ID, including both of them in other modules may result in dependency resolution errors.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import com.itsaky.androidide.build.config.BuildConfig
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("realm-android")
|
||||
}
|
||||
|
||||
groupConfig {
|
||||
groupIdSuffix.set("event")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "${BuildConfig.packageName}.lsp.java"
|
||||
|
||||
sourceSets {
|
||||
getByName("androidTest") {
|
||||
assets.srcDirs(rootProject.file("subprojects/framework-stubs/libs"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("eventBusIndex", "${BuildConfig.packageName}.events.LspJavaEventsIndex")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
kapt(projects.annotation.processors)
|
||||
|
||||
implementation(libs.androidide.ts)
|
||||
implementation(libs.androidide.ts.java)
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.common.editor)
|
||||
implementation(libs.common.javaparser)
|
||||
implementation(libs.common.utilcode)
|
||||
implementation(libs.androidx.annotation)
|
||||
implementation(libs.google.guava)
|
||||
implementation(libs.google.gson)
|
||||
implementation(libs.google.material)
|
||||
|
||||
implementation(projects.core.actions)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.indexing)
|
||||
implementation(projects.core.lspApi)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.editor.api)
|
||||
implementation(projects.java.javacServices)
|
||||
|
||||
implementation(libs.composite.javac)
|
||||
implementation(libs.composite.javapoet)
|
||||
implementation(libs.composite.jaxp)
|
||||
implementation(libs.composite.jdkJdeps)
|
||||
implementation(libs.composite.googleJavaFormat)
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.common.kotlin)
|
||||
|
||||
testImplementation(projects.testing.commonTest)
|
||||
testImplementation(projects.testing.lspTest)
|
||||
|
||||
androidTestImplementation(projects.testing.androidTest)
|
||||
androidTestImplementation(projects.utilities.shared)
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
|
||||
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import openjdk.tools.classfile.AccessFlags
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class JavaClassCRUDTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
|
||||
|
||||
@Test
|
||||
fun testSimpleJavaClassCRUD() {
|
||||
dbTestRule.withDb("java-class-CRUD") {
|
||||
val klass = ModelBuilder.createJavaClass()
|
||||
|
||||
// CREATE
|
||||
executeTransaction {
|
||||
it.insert(klass)
|
||||
}
|
||||
|
||||
var dbKlass: JavaClass? = null
|
||||
executeTransaction {
|
||||
// READ
|
||||
dbKlass = where(JavaClass::class.java)
|
||||
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
|
||||
.findFirst()
|
||||
|
||||
assertThat(dbKlass).isNotNull()
|
||||
assertThat(dbKlass?.fqn).isEqualTo("com/itsaky/androidide/indexing/TestClass")
|
||||
assertThat(dbKlass?.name).isEqualTo("TestClass")
|
||||
assertThat(dbKlass?.packageName).isEqualTo("com/itsaky/androidide/indexing")
|
||||
assertThat(dbKlass?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL)
|
||||
assertThat(dbKlass?.superClassFqn).isEqualTo("java/lang/Object")
|
||||
assertThat(dbKlass?.superInterfacesFqn).isEqualTo(RealmList<String>())
|
||||
assertThat(dbKlass?.fields).isNotEmpty()
|
||||
assertThat(dbKlass?.fields).hasSize(1)
|
||||
|
||||
dbKlass?.fields?.get(0).apply {
|
||||
assertThat(this).isNotNull()
|
||||
assertThat(this?.name).isEqualTo("someString")
|
||||
assertThat(this?.type).isEqualTo(JavaType.STRING)
|
||||
assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PRIVATE)
|
||||
}
|
||||
|
||||
assertThat(dbKlass?.methods).isNotEmpty()
|
||||
assertThat(dbKlass?.methods).hasSize(1)
|
||||
|
||||
dbKlass?.methods?.get(0).apply {
|
||||
assertThat(this).isNotNull()
|
||||
assertThat(this?.name).isEqualTo("getSomeString")
|
||||
assertThat(this?.paramsTypes).isEqualTo(RealmList<String>())
|
||||
assertThat(this?.returnType).isEqualTo(JavaType.STRING)
|
||||
assertThat(this?.accessFlags).isEqualTo(AccessFlags.ACC_PUBLIC)
|
||||
}
|
||||
}
|
||||
|
||||
assertThat(dbKlass).isNotNull()
|
||||
|
||||
// UPDATE
|
||||
executeTransaction {
|
||||
dbKlass?.accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT
|
||||
}
|
||||
|
||||
// verify UPDATE
|
||||
executeTransaction {
|
||||
assertThat(
|
||||
where(JavaClass::class.java)
|
||||
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
|
||||
.findFirst()?.accessFlags
|
||||
).isEqualTo(AccessFlags.ACC_PUBLIC or AccessFlags.ACC_ABSTRACT)
|
||||
}
|
||||
|
||||
// DELETE
|
||||
executeTransaction {
|
||||
RealmObject.deleteFromRealm(dbKlass!!)
|
||||
}
|
||||
|
||||
// verify DELTE
|
||||
executeTransaction {
|
||||
assertThat(
|
||||
where(JavaClass::class.java)
|
||||
.equalTo("fqn", "com/itsaky/androidide/indexing/TestClass")
|
||||
.findFirst()
|
||||
).isNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.blankj.utilcode.util.ConvertUtils
|
||||
import com.google.common.truth.Truth
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
|
||||
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
|
||||
import io.realm.RealmModel
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class JavaIndexerTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
|
||||
|
||||
val androidJar: File by lazy {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val file = File.createTempFile("ajar", null, context.cacheDir)
|
||||
context.assets.open("android.jar").use { asset ->
|
||||
Truth.assertThat(asset).isNotNull()
|
||||
file.outputStream().buffered().use { out ->
|
||||
asset.copyTo(out)
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleAndroidJarIndexingDurationCheck() {
|
||||
val worker = JavaJarModelBuilder(androidJar)
|
||||
val batches = mutableMapOf<Class<*>, MutableList<RealmModel>>()
|
||||
|
||||
var dbWriteDuration = 0L
|
||||
|
||||
dbTestRule.withDb("android-jar-classes") {
|
||||
val totalStart = System.currentTimeMillis()
|
||||
worker.consumeTypes { type ->
|
||||
val batched = batches.computeIfAbsent(type.javaClass) { mutableListOf() }
|
||||
batched.add(type)
|
||||
|
||||
if (batched.size >= 100) {
|
||||
val start = System.currentTimeMillis()
|
||||
executeTransaction {
|
||||
insertOrUpdate(batched)
|
||||
}
|
||||
dbWriteDuration += System.currentTimeMillis() - start
|
||||
|
||||
batched.clear()
|
||||
}
|
||||
}
|
||||
|
||||
for ((_, batched) in batches) {
|
||||
if (batched.isNotEmpty()) {
|
||||
val start = System.currentTimeMillis()
|
||||
executeTransaction {
|
||||
insertOrUpdate(batched)
|
||||
}
|
||||
dbWriteDuration += System.currentTimeMillis() - start
|
||||
}
|
||||
}
|
||||
|
||||
println(
|
||||
"Took ${dbWriteDuration}ms to write android.jar (${
|
||||
ConvertUtils.byte2FitMemorySize(
|
||||
androidJar.length()
|
||||
)
|
||||
}) classes to Realm"
|
||||
)
|
||||
println("Total time: ${System.currentTimeMillis() - totalStart}ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaField
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaMethod
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
|
||||
import io.realm.RealmList
|
||||
import openjdk.tools.classfile.AccessFlags
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object ModelBuilder {
|
||||
|
||||
fun createJavaClass() = JavaClass.newInstance(
|
||||
fqn = "com/itsaky/androidide/indexing/TestClass",
|
||||
name = "TestClass",
|
||||
packageName = "com/itsaky/androidide/indexing",
|
||||
accessFlags = AccessFlags.ACC_PUBLIC or AccessFlags.ACC_FINAL,
|
||||
superClassFqn = "java/lang/Object",
|
||||
superInterfacesFqn = RealmList(),
|
||||
fields = RealmList<JavaField>().apply {
|
||||
add(JavaField.newField("someString", JavaType.STRING, AccessFlags.ACC_PRIVATE))
|
||||
},
|
||||
methods = RealmList<JavaMethod>().apply {
|
||||
add(
|
||||
JavaMethod.newInstance(
|
||||
name = "getSomeString",
|
||||
paramsTypes = RealmList(),
|
||||
returnType = JavaType.STRING,
|
||||
accessFlags = AccessFlags.ACC_PUBLIC
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.ArrayAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.ClassAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.EnumAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.IAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaConstant
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaIndexingRealmModule
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.PrimitiveAnnotationElementValue
|
||||
import com.itsaky.androidide.testing.android.rules.RealmDBTestRule
|
||||
import io.realm.RealmAny
|
||||
import io.realm.RealmList
|
||||
import io.realm.exceptions.RealmPrimaryKeyConstraintException
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SharedModelCRUDTest {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val dbTestRule = RealmDBTestRule(JavaIndexingRealmModule())
|
||||
|
||||
@Test
|
||||
fun testApiInfoCreate() {
|
||||
dbTestRule.withDb("api-info") {
|
||||
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
|
||||
assertInsertUnique(apiInfo)
|
||||
|
||||
val apiInfo2 = apiInfo.clone().update(removedIn = 26)
|
||||
assertInsertion(allowUpdate = true, apiInfo, apiInfo2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(apiInfo, apiInfo2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testApiInfoDuplicationError() {
|
||||
dbTestRule.withDb("api-info-duplication-error") {
|
||||
val apiInfo = ApiInfo.newInstance(since = 1, deprecatedIn = 21, removedIn = 23)
|
||||
assertInsertSingle(apiInfo)
|
||||
assertInsertSingle(apiInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConstantCreate() {
|
||||
dbTestRule.withDb("java-constant-create") {
|
||||
val constant =
|
||||
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt()))
|
||||
val constant2 =
|
||||
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(2.toInt()))
|
||||
|
||||
assertInsertUnique(constant)
|
||||
|
||||
assertInsertion(allowUpdate = true, constant, constant2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(constant, constant2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testConstantDuplicationError() {
|
||||
dbTestRule.withDb("java-constant-duplication-error") {
|
||||
val constant =
|
||||
JavaConstant.newInstance(kind = JavaType.KIND_INT, value = RealmAny.valueOf(1.toInt()))
|
||||
assertInsertSingle(constant)
|
||||
assertInsertSingle(constant)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJavaTypeCreate() {
|
||||
dbTestRule.withDb("java-type-create") {
|
||||
assertInsertUnique(JavaType.INT)
|
||||
|
||||
assertInsertion(JavaType.STRING, JavaType.OBJECT) { models ->
|
||||
assertThat(models).hasSize(3)
|
||||
assertThat(models).containsExactly(JavaType.INT, JavaType.STRING, JavaType.OBJECT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testJavaTypeDuplicationError() {
|
||||
dbTestRule.withDb("java-type-duplication-error") {
|
||||
assertInsertSingle(JavaType.OBJECT)
|
||||
assertInsertSingle(JavaType.OBJECT)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPrimitiveAnnotationValueCreate() {
|
||||
dbTestRule.withDb("annotation-value-primitive") {
|
||||
val prim1 = PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_INT,
|
||||
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt()))
|
||||
)
|
||||
val prim2 = PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_STRING,
|
||||
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
|
||||
)
|
||||
|
||||
assertInsertUnique(prim1)
|
||||
|
||||
assertInsertion(allowUpdate = true, prim1, prim2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(prim1, prim2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testPrimitiveAnnotationValueDuplicationError() {
|
||||
dbTestRule.withDb("annotation-value-primitive-duplication") {
|
||||
val prim1 = PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_INT,
|
||||
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(1.toInt()))
|
||||
)
|
||||
assertInsertSingle(prim1)
|
||||
assertInsertSingle(prim1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testArrayAnnotationValueCreate() {
|
||||
dbTestRule.withDb("annotation-value-array") {
|
||||
val obj1 = ArrayAnnotationElementValue.newInstance(
|
||||
RealmList()
|
||||
)
|
||||
val obj2 = ArrayAnnotationElementValue.newInstance(
|
||||
RealmList<RealmAny>().apply {
|
||||
add(
|
||||
RealmAny.valueOf(
|
||||
PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_STRING,
|
||||
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RealmAny.valueOf(
|
||||
PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_INT,
|
||||
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt()))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
assertInsertUnique(obj1)
|
||||
|
||||
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(obj1, obj2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testArrayAnnotationValueDuplicationError() {
|
||||
dbTestRule.withDb("annotation-value-array-duplication") {
|
||||
val obj1 = ArrayAnnotationElementValue.newInstance(
|
||||
RealmList<RealmAny>().apply {
|
||||
add(
|
||||
RealmAny.valueOf(
|
||||
PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_STRING,
|
||||
JavaConstant.newInstance(JavaType.KIND_REF, RealmAny.valueOf("Something something"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RealmAny.valueOf(
|
||||
PrimitiveAnnotationElementValue.newInstance(
|
||||
IAnnotationElementValue.KIND_INT,
|
||||
JavaConstant.newInstance(JavaType.KIND_INT, RealmAny.valueOf(123.toInt()))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
assertInsertSingle(obj1)
|
||||
assertInsertSingle(obj1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClassAnnotationValueCreate() {
|
||||
dbTestRule.withDb("annotation-value-class") {
|
||||
val obj1 = ClassAnnotationElementValue.newInstance(
|
||||
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF)
|
||||
)
|
||||
val obj2 = ClassAnnotationElementValue.newInstance(
|
||||
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF, arrayDims = 5)
|
||||
)
|
||||
|
||||
assertInsertUnique(obj1)
|
||||
|
||||
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(obj1, obj2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testClassAnnotationValueDuplicationError() {
|
||||
dbTestRule.withDb("annotation-value-class-duplication") {
|
||||
val obj1 = ClassAnnotationElementValue.newInstance(
|
||||
JavaType.newInstance("com/example/SomeType", JavaType.KIND_REF)
|
||||
)
|
||||
assertInsertSingle(obj1)
|
||||
assertInsertSingle(obj1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEnumAnnotationValueCreate() {
|
||||
dbTestRule.withDb("annotation-value-enum") {
|
||||
val obj1 = EnumAnnotationElementValue.newInstance(
|
||||
"SomeEnum_ENTRY",
|
||||
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
|
||||
)
|
||||
val obj2 = EnumAnnotationElementValue.newInstance(
|
||||
"SomeEnum_ENTRY_2",
|
||||
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
|
||||
)
|
||||
|
||||
assertInsertUnique(obj1)
|
||||
|
||||
assertInsertion(allowUpdate = true, obj1, obj2) { models ->
|
||||
assertThat(models).hasSize(2)
|
||||
assertThat(models).containsExactly(obj1, obj2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RealmPrimaryKeyConstraintException::class)
|
||||
fun testEnumAnnotationValueDuplicationError() {
|
||||
dbTestRule.withDb("annotation-value-enum-duplication") {
|
||||
val obj1 = EnumAnnotationElementValue.newInstance(
|
||||
"SomeEnum_ENTRY",
|
||||
JavaType.newInstance("com/example/SomeEnum", JavaType.KIND_REF),
|
||||
)
|
||||
assertInsertSingle(obj1)
|
||||
assertInsertSingle(obj1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmModel
|
||||
import io.realm.RealmResults
|
||||
|
||||
inline fun <reified T : RealmModel> Realm.assertInsertion(
|
||||
vararg models: T,
|
||||
crossinline verify: Realm.(models: RealmResults<T>) -> Unit
|
||||
) {
|
||||
assertInsertion(false, *models, verify = verify)
|
||||
}
|
||||
|
||||
inline fun <reified T : RealmModel> Realm.assertInsertion(
|
||||
allowUpdate: Boolean = false,
|
||||
vararg models: T,
|
||||
crossinline verify: Realm.(models: RealmResults<T>) -> Unit
|
||||
) {
|
||||
val modelList = models.toList()
|
||||
assertThat(modelList).isNotEmpty()
|
||||
|
||||
executeTransaction {
|
||||
if (allowUpdate) {
|
||||
it.insertOrUpdate(modelList)
|
||||
} else {
|
||||
it.insert(modelList)
|
||||
}
|
||||
}
|
||||
|
||||
executeTransaction {
|
||||
verify(it.where(modelList[0].javaClass).findAll())
|
||||
}
|
||||
}
|
||||
|
||||
fun Realm.assertInsertSingle(model: RealmModel) {
|
||||
assertInsertion(model) { models ->
|
||||
assertThat(models).isNotNull()
|
||||
assertThat(models).isNotEmpty()
|
||||
assertThat(models).hasSize(1)
|
||||
assertThat(models[0]).isEqualTo(model)
|
||||
}
|
||||
}
|
||||
|
||||
fun Realm.assertInsertUnique(model: RealmModel) {
|
||||
assertInsertSingle(model)
|
||||
assertInsertion(true, model) { models ->
|
||||
assertThat(models).isNotNull()
|
||||
assertThat(models).isNotEmpty()
|
||||
assertThat(models).hasSize(1)
|
||||
assertThat(models[0]).isEqualTo(model)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ This file is part of AndroidIDE.
|
||||
~
|
||||
~ AndroidIDE is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ AndroidIDE is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<manifest/>
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"lcomm": {
|
||||
"desc": "Create a line comment",
|
||||
"body": [
|
||||
"// $0"
|
||||
]
|
||||
},
|
||||
"bcomm": {
|
||||
"desc": "Create a block comment",
|
||||
"body": [
|
||||
"/*",
|
||||
" * $0",
|
||||
" */"
|
||||
]
|
||||
},
|
||||
"todo": {
|
||||
"desc": "Create a TODO comment",
|
||||
"body": [
|
||||
"// TODO: $0"
|
||||
]
|
||||
},
|
||||
"fixme": {
|
||||
"desc": "Create a FIXME comment",
|
||||
"body": [
|
||||
"// FIXME: $0"
|
||||
]
|
||||
},
|
||||
"jdoc": {
|
||||
"desc": "Create a JavaDoc comment",
|
||||
"body": [
|
||||
"/**",
|
||||
" * $0",
|
||||
" */"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"for": {
|
||||
"desc": "Indexed for loop",
|
||||
"body": [
|
||||
"for(int ${1:i} = 0; $1 < ${2:count}; ++$1) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"foreach": {
|
||||
"desc": "For-each loop",
|
||||
"body": [
|
||||
"for(${1:element} : ${2:iterable}) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"forr": {
|
||||
"desc": "Reverse-indexed for loop",
|
||||
"body": [
|
||||
"for(int ${1:i} = ${2:count} - 1; $1 >= 0; --$1) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"while": {
|
||||
"desc": "While loop",
|
||||
"body": [
|
||||
"while(${1:condition}) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"dowhile": {
|
||||
"desc": "Do-While loop",
|
||||
"body": [
|
||||
"do {",
|
||||
"\t$0",
|
||||
"} while(${1:condition});"
|
||||
]
|
||||
},
|
||||
"if": {
|
||||
"desc": "If statement",
|
||||
"body": [
|
||||
"if(${1:condition}) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"ifelse": {
|
||||
"desc": "If-else statement",
|
||||
"body": [
|
||||
"if(${1:condition}) {",
|
||||
"\t$0",
|
||||
"} else {",
|
||||
"\t",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"ifnull": {
|
||||
"desc": "If-null",
|
||||
"body": [
|
||||
"if(${1:object} == null) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"ifnotnull": {
|
||||
"desc": "If-not-null",
|
||||
"body": [
|
||||
"if(${1:object} != null) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"new_object": {
|
||||
"desc": "Create a new object",
|
||||
"body": [
|
||||
"${1:Object} ${2:foo} = new $1($3);",
|
||||
"$0"
|
||||
]
|
||||
},
|
||||
"trycatch": {
|
||||
"desc": "A try-catch statement",
|
||||
"body": [
|
||||
"try {",
|
||||
"\t$0",
|
||||
"} catch(${1:Exception} ${2:err}) {",
|
||||
"\t",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"tryresources": {
|
||||
"desc": "A try-with-resources statement",
|
||||
"body": [
|
||||
"try ($1) {",
|
||||
"\t$0",
|
||||
"} catch(${2:Exception} ${3:err}) {",
|
||||
"\t",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"const": {
|
||||
"desc": "Create a constant field",
|
||||
"body": [
|
||||
"public static final ${1:type} ${2/(.*)/${1:/upcase}/} = ${3:value};"
|
||||
]
|
||||
},
|
||||
"ctor": {
|
||||
"desc": "Public constructor",
|
||||
"body": [
|
||||
"public ${1:$TM_FILENAME_BASE}($2) {",
|
||||
"\t${3:super();}$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"public_method": {
|
||||
"desc": "Create a new public method",
|
||||
"body": [
|
||||
"public ${1:void} ${2:method}($3) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"protected_method": {
|
||||
"desc": "Create a new protected method",
|
||||
"body": [
|
||||
"protected ${1:void} ${2:method}($3) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"private_method": {
|
||||
"desc": "Create a new private method",
|
||||
"body": [
|
||||
"private ${1:void} ${2:method}($3) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"public_static_method": {
|
||||
"desc": "Create a new public static method",
|
||||
"body": [
|
||||
"public static ${1:void} ${2:method}($3) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"private_static_method": {
|
||||
"desc": "Create a new private stataic method",
|
||||
"body": [
|
||||
"private static ${1:void} ${2:method}($3) {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"class": {
|
||||
"desc": "Create a new class",
|
||||
"body" : [
|
||||
"public class ${1:$TM_FILENAME_BASE} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"interface": {
|
||||
"desc": "Create a new interface",
|
||||
"body" : [
|
||||
"public interface ${1:$TM_FILENAME_BASE} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"enum": {
|
||||
"desc": "Create a new enum class",
|
||||
"body" : [
|
||||
"public enum ${1:$TM_FILENAME_BASE} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
|
||||
import com.itsaky.androidide.projects.api.ModuleProject;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Provides {@link JavaCompilerService} instances for different {@link ModuleProject}s.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class JavaCompilerProvider {
|
||||
|
||||
private static JavaCompilerProvider sInstance;
|
||||
private final Map<ModuleProject, JavaCompilerService> mCompilers = new ConcurrentHashMap<>();
|
||||
|
||||
private JavaCompilerProvider() {}
|
||||
|
||||
@NonNull
|
||||
public static JavaCompilerService get(ModuleProject module) {
|
||||
return JavaCompilerProvider.getInstance().forModule(module);
|
||||
}
|
||||
|
||||
public static JavaCompilerProvider getInstance() {
|
||||
if (sInstance == null) {
|
||||
sInstance = new JavaCompilerProvider();
|
||||
}
|
||||
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
@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 mdoule instance.
|
||||
final JavaCompilerService cached = mCompilers.get(module);
|
||||
if (cached != null && cached.getModule() != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final JavaCompilerService newInstance = new JavaCompilerService(module);
|
||||
mCompilers.put(module, newInstance);
|
||||
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
// TODO This currently destroys all the compiler instances
|
||||
// We must have a method to destroy only the required instance in
|
||||
// JavaLanguageServer.handleFailure(LSPFailure)
|
||||
public synchronized void destroy() {
|
||||
for (final JavaCompilerService compiler : mCompilers.values()) {
|
||||
compiler.destroy();
|
||||
}
|
||||
mCompilers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
|
||||
import com.itsaky.androidide.eventbus.events.editor.DocumentCloseEvent
|
||||
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.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.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.init
|
||||
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
|
||||
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.FailureType
|
||||
import com.itsaky.androidide.lsp.models.FormatCodeParams
|
||||
import com.itsaky.androidide.lsp.models.LSPFailure
|
||||
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.api.ModuleProject
|
||||
import com.itsaky.androidide.projects.api.Project
|
||||
import com.itsaky.androidide.utils.DocumentUtils
|
||||
import com.itsaky.androidide.utils.VMUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.util.Objects
|
||||
|
||||
class JavaLanguageServer : ILanguageServer {
|
||||
|
||||
private val completionProvider: CompletionProvider = CompletionProvider()
|
||||
private val diagnosticProvider: JavaDiagnosticProvider?
|
||||
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
|
||||
|
||||
val settings: IServerSettings
|
||||
get() {
|
||||
return _settings ?: JavaServerSettings.getInstance()
|
||||
.also { _settings = it }
|
||||
}
|
||||
|
||||
override val serverId: String = SERVER_ID
|
||||
|
||||
companion object {
|
||||
|
||||
const val SERVER_ID = "ide.lsp.java"
|
||||
private val log = LoggerFactory.getLogger(JavaLanguageServer::class.java)
|
||||
}
|
||||
|
||||
init {
|
||||
diagnosticProvider = JavaDiagnosticProvider()
|
||||
cachedCompletion = CachedCompletion.EMPTY
|
||||
|
||||
applySettings(JavaServerSettings.getInstance())
|
||||
|
||||
if (!EventBus.getDefault().isRegistered(this)) {
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
init()
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
JavaCompilerProvider.getInstance().destroy()
|
||||
SourceFileManager.clearCache()
|
||||
CacheFSInfoSingleton.clearCache()
|
||||
clearCache()
|
||||
EventBus.getDefault().unregister(this)
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
override fun connectClient(client: ILanguageClient?) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
override fun applySettings(settings: IServerSettings?) {
|
||||
this._settings = settings
|
||||
}
|
||||
|
||||
override fun setupWithProject(project: Project) {
|
||||
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu)
|
||||
|
||||
// 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 project.subProjects) {
|
||||
if (subModule !is ModuleProject || subModule.path == project.rootProject.path) {
|
||||
continue
|
||||
}
|
||||
SourceFileManager.forModule(subModule)
|
||||
}
|
||||
startOrRestartAnalyzeTimer()
|
||||
}
|
||||
|
||||
override fun complete(params: CompletionParams?): CompletionResult {
|
||||
val compiler = getCompiler(params!!.file)
|
||||
if (!settings.completionsEnabled() || !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) { cachedCompletion: CachedCompletion ->
|
||||
updateCachedCompletion(cachedCompletion)
|
||||
}
|
||||
|
||||
return completionProvider.complete(params)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun expandSelection(params: ExpandSelectionParams): Range {
|
||||
val compiler = getCompiler(params.file)
|
||||
return if (!settings.smartSelectionsEnabled()) {
|
||||
params.selection
|
||||
} else JavaSelectionProvider(compiler).expandSelection(params)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun analyze(file: Path): DiagnosticResult {
|
||||
if (!settings.diagnosticsEnabled() || !DocumentUtils.isJavaFile(file)) {
|
||||
return DiagnosticResult.NO_UPDATE
|
||||
}
|
||||
|
||||
return if (!settings.codeAnalysisEnabled()) {
|
||||
DiagnosticResult.NO_UPDATE
|
||||
} else diagnosticProvider!!.analyze(file)
|
||||
}
|
||||
|
||||
override fun formatCode(params: FormatCodeParams?): CodeFormatResult {
|
||||
return CodeFormatProvider(settings).format(params)
|
||||
}
|
||||
|
||||
override fun handleFailure(failure: LSPFailure?): Boolean {
|
||||
return when (failure!!.type) {
|
||||
FailureType.COMPLETION -> {
|
||||
if (isCancelled(failure.error)) {
|
||||
return true
|
||||
}
|
||||
JavaCompilerProvider.getInstance().destroy()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
|
||||
fun getCompiler(file: Path?): JavaCompilerService {
|
||||
if (!DocumentUtils.isJavaFile(file)) {
|
||||
return JavaCompilerService.NO_MODULE_COMPILER
|
||||
}
|
||||
val root = getInstance().rootProject
|
||||
?: return JavaCompilerService.NO_MODULE_COMPILER
|
||||
val module = root.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER
|
||||
return JavaCompilerProvider.get(module)
|
||||
}
|
||||
|
||||
private fun updateCachedCompletion(cachedCompletion: CachedCompletion) {
|
||||
Objects.requireNonNull(cachedCompletion)
|
||||
this.cachedCompletion = cachedCompletion
|
||||
}
|
||||
|
||||
private fun startOrRestartAnalyzeTimer() {
|
||||
if (VMUtils.isJvm()) {
|
||||
return
|
||||
}
|
||||
if (!timer.isStarted) {
|
||||
timer.start()
|
||||
} else {
|
||||
timer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.ASYNC)
|
||||
@Suppress("unused")
|
||||
fun onContentChange(event: DocumentChangeEvent) {
|
||||
if (!DocumentUtils.isJavaFile(event.changedFile)) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
|
||||
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event)
|
||||
val module = getInstance()
|
||||
.findModuleForFile(event.changedFile)
|
||||
if (module != null) {
|
||||
val compiler = JavaCompilerProvider.get(module)
|
||||
compiler.onDocumentChange(event)
|
||||
}
|
||||
startOrRestartAnalyzeTimer()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.ASYNC)
|
||||
@Suppress("unused")
|
||||
fun onFileSelected(event: DocumentSelectedEvent) {
|
||||
selectedFile = event.selectedFile
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.ASYNC)
|
||||
@Suppress("unused")
|
||||
fun onFileOpened(event: DocumentOpenEvent) {
|
||||
selectedFile = event.openedFile
|
||||
startOrRestartAnalyzeTimer()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.ASYNC)
|
||||
@Suppress("unused")
|
||||
fun onFileClosed(event: DocumentCloseEvent) {
|
||||
diagnosticProvider?.clearTimestamp(event.closedFile)
|
||||
|
||||
if (getActiveDocumentCount() == 0) {
|
||||
selectedFile = null
|
||||
timer.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private fun analyzeSelected() {
|
||||
if (selectedFile == null || client == null) {
|
||||
return
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val result = analyze(selectedFile!!)
|
||||
withContext(Dispatchers.Main) {
|
||||
client?.publishDiagnostics(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
visible = DocumentUtils.isJavaFile(file.toPath())
|
||||
enabled = visible
|
||||
}
|
||||
|
||||
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.getDefault().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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.actions
|
||||
|
||||
import android.content.Context
|
||||
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.requirePath
|
||||
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.utils.flashInfo
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import jdkx.lang.model.element.Modifier.STATIC
|
||||
import openjdk.source.tree.ClassTree
|
||||
import openjdk.source.tree.Tree.Kind.VARIABLE
|
||||
import openjdk.source.tree.VariableTree
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.CompletionException
|
||||
|
||||
/**
|
||||
* Any action that has to work with fields in the current class can inherit this action.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class FieldBasedAction : BaseJavaCodeAction() {
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(FieldBasedAction::class.java)
|
||||
}
|
||||
|
||||
override fun prepare(data: ActionData) {
|
||||
super.prepare(data)
|
||||
|
||||
if (
|
||||
!visible ||
|
||||
!data.hasRequiredData(
|
||||
com.itsaky.androidide.models.Range::class.java,
|
||||
CodeEditor::class.java
|
||||
) ||
|
||||
IProjectManager.getInstance().rootProject == null
|
||||
) {
|
||||
markInvisible()
|
||||
return
|
||||
}
|
||||
|
||||
visible = true
|
||||
enabled = true
|
||||
}
|
||||
|
||||
override suspend fun execAction(data: ActionData): Any {
|
||||
val range = data[com.itsaky.androidide.models.Range::class.java]!!
|
||||
val file = data.requirePath()
|
||||
val module = IProjectManager.getInstance().findModuleForFile(file, false) ?: return Any()
|
||||
|
||||
return JavaCompilerProvider.get(module).compile(file).get { task ->
|
||||
val triple = findFields(task, file, range)
|
||||
val type = triple.second
|
||||
val fields = triple.third
|
||||
val fieldNames = fields.map { "${it.name}: ${it.type}" } // Get the names
|
||||
|
||||
log.debug("Found {} fields in class {}", fieldNames.size, type.simpleName)
|
||||
|
||||
return@get fieldNames
|
||||
}
|
||||
}
|
||||
|
||||
protected fun findFields(
|
||||
task: CompileTask,
|
||||
file: Path,
|
||||
range: com.itsaky.androidide.models.Range
|
||||
): Triple<FindTypeDeclarationAt, ClassTree, MutableList<VariableTree>> {
|
||||
// 1-based line and column index
|
||||
val startLine = range.start.line + 1
|
||||
val startColumn = range.start.column + 1
|
||||
val endLine = range.end.line + 1
|
||||
val endColumn = range.end.column + 1
|
||||
val lines = task.root().lineMap
|
||||
val start = lines.getPosition(startLine.toLong(), startColumn.toLong())
|
||||
val end = lines.getPosition(endLine.toLong(), endColumn.toLong())
|
||||
|
||||
if (start == (-1).toLong() || end == (-1).toLong()) {
|
||||
throw CompletionException(
|
||||
RuntimeException("Unable to find position for the given selection range")
|
||||
)
|
||||
}
|
||||
|
||||
val typeFinder = FindTypeDeclarationAt(task.task)
|
||||
var type = typeFinder.scan(task.root(file), start)
|
||||
if (type == null) {
|
||||
type = typeFinder.scan(task.root(file), end)
|
||||
}
|
||||
|
||||
if (type == null) {
|
||||
throw CompletionException(
|
||||
RuntimeException("Unable to find class declaration within cursor range")
|
||||
)
|
||||
}
|
||||
|
||||
val fields =
|
||||
type.members
|
||||
.filter { it.kind == VARIABLE }
|
||||
.map { it as VariableTree }
|
||||
.filter { !it.modifiers.flags.contains(STATIC) }
|
||||
.toMutableList()
|
||||
return Triple(typeFinder, type, fields)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
if (result !is List<*>) {
|
||||
log.error("Unable to find fields in the current class")
|
||||
return
|
||||
}
|
||||
|
||||
if (result.isEmpty()) {
|
||||
flashInfo(data[Context::class.java]!!.getString(R.string.msg_no_fields_found))
|
||||
return
|
||||
}
|
||||
|
||||
onGetFields(result as List<String>, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the fields of the current class are found. As this method is called inside
|
||||
* [postExec], the current thread is the UI thread.
|
||||
*/
|
||||
abstract fun onGetFields(fields: List<String>, data: ActionData)
|
||||
|
||||
/**
|
||||
* Shows the field selector dialog. Returns a [CompletableFuture] which is completed when the user
|
||||
* confirms the selected fields.
|
||||
*/
|
||||
protected fun showFieldSelector(
|
||||
fields: List<String>,
|
||||
data: ActionData,
|
||||
listener: OnFieldsSelectedListener?
|
||||
) {
|
||||
val names = fields.toTypedArray()
|
||||
val checkedNames = mutableSetOf<String>()
|
||||
val builder = newDialogBuilder(data)
|
||||
builder.setTitle(data[Context::class.java]!!.getString(R.string.msg_select_fields))
|
||||
builder.setMultiChoiceItems(names, BooleanArray(fields.size)) { _, which, checked ->
|
||||
checkedNames.apply {
|
||||
val item = names[which]
|
||||
if (checked) {
|
||||
add(item)
|
||||
} else {
|
||||
remove(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
||||
if (checkedNames.isEmpty()) {
|
||||
flashInfo(data[Context::class.java]!!.getString(R.string.msg_no_fields_selected))
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
listener?.onFieldsSelected(checkedNames)
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
/** Listener to get callback when fields are selected by the user. */
|
||||
fun interface OnFieldsSelectedListener {
|
||||
|
||||
/**
|
||||
* Called when the user is done selecting fields.
|
||||
*
|
||||
* @param fields The selected field names.
|
||||
*/
|
||||
fun onFieldsSelected(fields: MutableSet<String>)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.actions
|
||||
|
||||
import com.itsaky.androidide.actions.ActionItem
|
||||
import com.itsaky.androidide.lsp.actions.IActionsMenuProvider
|
||||
import com.itsaky.androidide.lsp.java.actions.common.CommentAction
|
||||
import com.itsaky.androidide.lsp.java.actions.common.FindReferencesAction
|
||||
import com.itsaky.androidide.lsp.java.actions.common.GoToDefinitionAction
|
||||
import com.itsaky.androidide.lsp.java.actions.common.OrganizeImportsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.common.RemoveUnusedImportsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.common.UncommentAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.AddThrowsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.AutoFixImportsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.CreateMissingMethodAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.FieldToBlockAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.ImplementAbstractMethodsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveClassAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveMethodAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveUnusedThrowsAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.SuppressUncheckedWarningAction
|
||||
import com.itsaky.androidide.lsp.java.actions.diagnostics.VariableToStatementAction
|
||||
import com.itsaky.androidide.lsp.java.actions.generators.GenerateConstructorAction
|
||||
import com.itsaky.androidide.lsp.java.actions.generators.GenerateMissingConstructorAction
|
||||
import com.itsaky.androidide.lsp.java.actions.generators.GenerateSettersAndGettersAction
|
||||
import com.itsaky.androidide.lsp.java.actions.generators.GenerateToStringMethodAction
|
||||
import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMethodsAction
|
||||
|
||||
/**
|
||||
* Java code actions.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object JavaCodeActionsMenu : IActionsMenuProvider {
|
||||
|
||||
override val actions: List<ActionItem> =
|
||||
listOf(
|
||||
CommentAction(),
|
||||
UncommentAction(),
|
||||
GoToDefinitionAction(),
|
||||
FindReferencesAction(),
|
||||
AddImportAction(),
|
||||
AutoFixImportsAction(),
|
||||
ImplementAbstractMethodsAction(),
|
||||
VariableToStatementAction(),
|
||||
FieldToBlockAction(),
|
||||
RemoveClassAction(),
|
||||
RemoveMethodAction(),
|
||||
RemoveUnusedThrowsAction(),
|
||||
CreateMissingMethodAction(),
|
||||
SuppressUncheckedWarningAction(),
|
||||
AddThrowsAction(),
|
||||
GenerateSettersAndGettersAction(),
|
||||
OverrideSuperclassMethodsAction(),
|
||||
GenerateMissingConstructorAction(),
|
||||
GenerateConstructorAction(),
|
||||
GenerateToStringMethodAction(),
|
||||
RemoveUnusedImportsAction(),
|
||||
OrganizeImportsAction()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.actions.common
|
||||
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.requireEditor
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.resources.R
|
||||
|
||||
/** @author Akash Yadav */
|
||||
class CommentAction : BaseJavaCodeAction() {
|
||||
override val id: String = "ide.editor.lsp.java.commentLine"
|
||||
override var label: String = ""
|
||||
|
||||
override val titleTextRes: Int = R.string.action_comment_line
|
||||
|
||||
override var requiresUIThread: Boolean = true
|
||||
|
||||
override suspend fun execAction(data: ActionData): Boolean {
|
||||
val editor = data.requireEditor()
|
||||
val text = editor.text
|
||||
val cursor = editor.cursor
|
||||
|
||||
text.beginBatchEdit()
|
||||
for (line in cursor.leftLine..cursor.rightLine) {
|
||||
text.insert(line, 0, "//")
|
||||
}
|
||||
text.endBatchEdit()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun dismissOnAction() = false
|
||||
}
|
||||
+53
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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 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
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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 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
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.itsaky.androidide.lsp.java.actions.common
|
||||
|
||||
import com.google.googlejavaformat.java.FormatterException
|
||||
import com.google.googlejavaformat.java.ImportOrderer
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.hasRequiredData
|
||||
import com.itsaky.androidide.actions.markInvisible
|
||||
import com.itsaky.androidide.actions.requireEditor
|
||||
import com.itsaky.androidide.editor.api.IEditor
|
||||
import com.itsaky.androidide.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
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(OrganizeImportsAction::class.java)
|
||||
}
|
||||
|
||||
override fun prepare(data: ActionData) {
|
||||
super.prepare(data)
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.hasRequiredData(CodeEditor::class.java)) {
|
||||
markInvisible()
|
||||
return
|
||||
}
|
||||
|
||||
visible = true
|
||||
enabled = true
|
||||
}
|
||||
|
||||
override suspend fun execAction(data: ActionData): Any {
|
||||
val watch = com.itsaky.androidide.utils.StopWatch("Organize imports")
|
||||
return try {
|
||||
val editor = data.requireEditor()
|
||||
val content = editor.text
|
||||
val server = data[JavaLanguageServer::class.java]
|
||||
val settings = server!!.settings as JavaServerSettings
|
||||
val output = ImportOrderer.reorderImports(content.toString(), settings.style)
|
||||
watch.log()
|
||||
output
|
||||
} catch (e: FormatterException) {
|
||||
log.error("Failed to reorder imports", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
super.postExec(data, result)
|
||||
if (result is String) {
|
||||
if (result.isNotEmpty()) {
|
||||
val editor = data.requireEditor()
|
||||
val cursor = editor.cursor.left()
|
||||
|
||||
editor.text.apply {
|
||||
val endLine = getLine(lineCount - 1)
|
||||
replace(0, 0, lineCount - 1, endLine.length + endLine.lineSeparator.length, result)
|
||||
}
|
||||
|
||||
(editor as? IEditor?)?.also {
|
||||
it.setSelectionAround(cursor)
|
||||
editor.ensureSelectionVisible()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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.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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.actions.common
|
||||
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.requireEditor
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.resources.R
|
||||
|
||||
/** @author Akash Yadav */
|
||||
class UncommentAction : BaseJavaCodeAction() {
|
||||
override val id: String = "ide.editor.lsp.java.uncommentLine"
|
||||
override var label: String = ""
|
||||
|
||||
override val titleTextRes: Int = R.string.action_uncomment_line
|
||||
|
||||
override var requiresUIThread: Boolean = true
|
||||
|
||||
override suspend fun execAction(data: ActionData): Boolean {
|
||||
val editor = data.requireEditor()
|
||||
val text = editor.text
|
||||
val cursor = editor.cursor
|
||||
|
||||
text.beginBatchEdit()
|
||||
for (line in cursor.leftLine..cursor.rightLine) {
|
||||
val l = text.getLineString(line)
|
||||
if (l.trim().startsWith("//")) {
|
||||
val i = l.indexOf("//")
|
||||
text.delete(line, i, line, i + 2)
|
||||
}
|
||||
}
|
||||
text.endBatchEdit()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun dismissOnAction() = false
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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
|
||||
|
||||
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<out JavaFileObject>)
|
||||
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<out JavaFileObject>
|
||||
)!!
|
||||
val file = data.requireFile()
|
||||
val module =
|
||||
IProjectManager.getInstance().findModuleForFile(file, false)
|
||||
?: run {
|
||||
markInvisible()
|
||||
return Any()
|
||||
}
|
||||
|
||||
val compiler = JavaCompilerProvider.get(module)
|
||||
|
||||
val titles = mutableListOf<String>()
|
||||
val rewrites = mutableListOf<AddImport>()
|
||||
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<CodeActionItem>()
|
||||
val titles = result.first as List<String>
|
||||
val rewrites = result.second as List<Rewrite>
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.actions.diagnostics
|
||||
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
import com.itsaky.androidide.actions.requireContext
|
||||
import com.itsaky.androidide.actions.requirePath
|
||||
import com.itsaky.androidide.lsp.java.R
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.models.DiagnosticCode
|
||||
import com.itsaky.androidide.lsp.java.utils.positionForImports
|
||||
import com.itsaky.androidide.lsp.models.CodeActionItem
|
||||
import com.itsaky.androidide.lsp.models.CodeActionKind
|
||||
import com.itsaky.androidide.lsp.models.DocumentChange
|
||||
import com.itsaky.androidide.lsp.models.TextEdit
|
||||
import com.itsaky.androidide.models.Range
|
||||
import com.itsaky.androidide.utils.DialogUtils
|
||||
import com.itsaky.androidide.utils.flashInfo
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Analyzes the source file for unresolved names and tries to import all of them at once.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class AutoFixImportsAction : BaseJavaCodeAction() {
|
||||
|
||||
override val titleTextRes: Int = R.string.title_fix_imports
|
||||
override val id: String = "ide.editor.lsp.java.diagnostics.autoFixImports"
|
||||
override var label: String = ""
|
||||
|
||||
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<String, List<String>>()
|
||||
|
||||
// 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<String, List<String>>? = 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<String> {
|
||||
val names = mutableListOf<String>()
|
||||
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<String> {
|
||||
return task.root(file).imports.map { it.qualifiedIdentifier }.map { it.toString() }.toSet()
|
||||
}
|
||||
|
||||
inner class Result(val fileImports: Set<String>, val classes: MutableMap<String, List<String>>)
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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 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)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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
|
||||
|
||||
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<out JavaFileObject>)
|
||||
?: 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<out JavaFileObject>
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.actions.generators
|
||||
|
||||
import android.content.Context
|
||||
import com.blankj.utilcode.util.ThreadUtils
|
||||
import com.github.javaparser.StaticJavaParser
|
||||
import com.github.javaparser.ast.Modifier
|
||||
import com.github.javaparser.ast.body.ConstructorDeclaration
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
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.FieldBasedAction
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import com.itsaky.androidide.lsp.java.utils.ShortTypePrinter.NO_PACKAGE
|
||||
import com.itsaky.androidide.preferences.utils.indentationString
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.resources.R.string
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import openjdk.source.tree.ClassTree
|
||||
import openjdk.source.tree.VariableTree
|
||||
import openjdk.source.util.TreePath
|
||||
import openjdk.tools.javac.api.JavacTrees
|
||||
import openjdk.tools.javac.code.Symbol.ClassSymbol
|
||||
import openjdk.tools.javac.code.Symbol.VarSymbol
|
||||
import openjdk.tools.javac.code.Type
|
||||
import openjdk.tools.javac.tree.JCTree
|
||||
import openjdk.tools.javac.tree.TreeInfo
|
||||
import openjdk.tools.javac.util.ListBuffer
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Allows the user to select fields and generate a constructor which has parameters same as the
|
||||
* selected fields.
|
||||
*
|
||||
* The generated constructor has statements which assigns the fields to the parameter types.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class GenerateConstructorAction : FieldBasedAction() {
|
||||
|
||||
override val titleTextRes: Int = string.action_generate_constructor
|
||||
override val id: String = "ide.editor.lsp.java.generator.constructor"
|
||||
override var label: String = ""
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(GenerateConstructorAction::class.java)
|
||||
}
|
||||
|
||||
override fun onGetFields(fields: List<String>, data: ActionData) {
|
||||
showFieldSelector(fields, data) { selected ->
|
||||
CompletableFuture.runAsync { generateConstructor(data, selected) }
|
||||
.whenComplete { _, error ->
|
||||
if (error != null) {
|
||||
log.error("Unable to generate constructor for the selected fields", error)
|
||||
flashError(string.msg_cannot_generate_constructor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateConstructor(data: ActionData, selected: MutableSet<String>) {
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
|
||||
val range = data[com.itsaky.androidide.models.Range::class.java]!!
|
||||
val file = data.requirePath()
|
||||
|
||||
compiler.compile(file).run { task ->
|
||||
val triple = findFields(task, file, range)
|
||||
val typeFinder = triple.first
|
||||
val type = triple.second
|
||||
val fields = triple.third
|
||||
|
||||
fields.removeIf { !selected.contains("${it.name}: ${it.type}") }
|
||||
|
||||
log.debug("Creating toString() method with fields: {}", fields.map { it.name })
|
||||
|
||||
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateForFields(
|
||||
data: ActionData,
|
||||
task: CompileTask,
|
||||
type: ClassTree,
|
||||
paths: List<TreePath>
|
||||
) {
|
||||
val editor = data[CodeEditor::class.java]!!
|
||||
val trees = JavacTrees.instance(task.task)
|
||||
val sym = TreeInfo.symbolFor(type as JCTree) as ClassSymbol
|
||||
val varTypes = mapTypes(paths)
|
||||
val varNames = paths.map { it.leaf as VariableTree }.map { it.name.toString() }
|
||||
|
||||
if (paths.isEmpty() || trees.findConstructor(sym, varTypes) != null) {
|
||||
log.warn(
|
||||
"A constructor with same parameter types is already available in class {}", type.simpleName
|
||||
)
|
||||
flashError(data[Context::class.java]!!.getString(string.msg_constructor_available))
|
||||
return
|
||||
}
|
||||
|
||||
val stopWatch = com.itsaky.androidide.utils.StopWatch("generateConstructorForFields()")
|
||||
val constructor =
|
||||
newConstructor(type.simpleName.toString(), varTypes.toTypedArray(), varNames.toTypedArray())
|
||||
val body = constructor.createBody()
|
||||
for (varName in varNames) {
|
||||
body.addStatement(StaticJavaParser.parseStatement("this.$varName = $varName;"))
|
||||
}
|
||||
|
||||
stopWatch.lap("Constructor generated")
|
||||
log.info("Inserting constructor into editor...")
|
||||
|
||||
val insertAt = EditHelper.insertAfter(task.task, task.root(), paths.last().leaf)
|
||||
val indent = EditHelper.indent(task.task, task.root(), paths.last().leaf)
|
||||
var text = constructor.toString()
|
||||
text = text.replace("\n", "\n${indentationString(indent)}")
|
||||
text += "\n"
|
||||
|
||||
ThreadUtils.runOnUiThread {
|
||||
editor.text.insert(insertAt.line, insertAt.column, text)
|
||||
editor.formatCodeAsync()
|
||||
stopWatch.log()
|
||||
}
|
||||
}
|
||||
|
||||
private fun newConstructor(
|
||||
name: String,
|
||||
paramTypes: Array<Type>,
|
||||
paramNames: Array<String>
|
||||
): ConstructorDeclaration {
|
||||
val constructor = ConstructorDeclaration()
|
||||
constructor.setName(name)
|
||||
constructor.addModifier(Modifier.Keyword.PUBLIC)
|
||||
|
||||
for (i in paramTypes.indices) {
|
||||
val paramType = paramTypes[i]
|
||||
val paramName = paramNames[i]
|
||||
|
||||
constructor.addParameter(NO_PACKAGE.print(paramType), paramName)
|
||||
}
|
||||
|
||||
return constructor
|
||||
}
|
||||
|
||||
private fun mapTypes(paths: List<TreePath>): openjdk.tools.javac.util.List<Type> {
|
||||
val buffer = ListBuffer<Type>()
|
||||
for (path in paths) {
|
||||
val leaf = path.leaf
|
||||
val sym = TreeInfo.symbolFor(leaf as JCTree) as VarSymbol
|
||||
buffer.add(sym.type)
|
||||
}
|
||||
|
||||
return buffer.toList()
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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.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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.actions.generators
|
||||
|
||||
import android.content.Context
|
||||
import com.blankj.utilcode.util.ThreadUtils
|
||||
import com.github.javaparser.StaticJavaParser
|
||||
import com.github.javaparser.ast.Modifier
|
||||
import com.github.javaparser.ast.body.MethodDeclaration
|
||||
import com.github.javaparser.ast.expr.SimpleName
|
||||
import com.github.javaparser.ast.stmt.BlockStmt
|
||||
import com.github.javaparser.ast.type.Type
|
||||
import com.github.javaparser.ast.type.VoidType
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
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.FieldBasedAction
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
|
||||
import com.itsaky.androidide.lsp.java.utils.TypeUtils.toType
|
||||
import com.itsaky.androidide.preferences.internal.EditorPreferences
|
||||
import com.itsaky.androidide.preferences.utils.indentationString
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import jdkx.lang.model.element.Modifier.FINAL
|
||||
import jdkx.lang.model.element.VariableElement
|
||||
import openjdk.source.tree.ClassTree
|
||||
import openjdk.source.util.TreePath
|
||||
import openjdk.source.util.Trees
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Allows the user to select fields from the current class, then generates setters and getters for
|
||||
* the selected fields.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class GenerateSettersAndGettersAction : FieldBasedAction() {
|
||||
|
||||
override val id: String = "ide.editor.lsp.java.generator.settersAndGetters"
|
||||
override var label: String = ""
|
||||
|
||||
override val titleTextRes: Int = R.string.action_generate_setters_getters
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(GenerateSettersAndGettersAction::class.java)
|
||||
}
|
||||
|
||||
override fun onGetFields(fields: List<String>, data: ActionData) {
|
||||
|
||||
showFieldSelector(fields, data) { checkedNames ->
|
||||
CompletableFuture.runAsync { generateForFields(data, checkedNames) }
|
||||
.whenComplete {
|
||||
_, error,
|
||||
->
|
||||
if (error != null) {
|
||||
log.error("Unable to generate setters and getters", error)
|
||||
ThreadUtils.runOnUiThread {
|
||||
flashError(
|
||||
data[Context::class.java]!!.getString(R.string.msg_cannot_generate_setters_getters)
|
||||
)
|
||||
}
|
||||
return@whenComplete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateForFields(data: ActionData, names: MutableSet<String>) {
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
|
||||
val range = data[com.itsaky.androidide.models.Range::class.java]!!
|
||||
val file = data.requirePath()
|
||||
|
||||
compiler.compile(file).run { task ->
|
||||
val triple = findFields(task, file, range)
|
||||
val typeFinder = triple.first
|
||||
val type = triple.second
|
||||
val fields = triple.third
|
||||
|
||||
fields.removeIf { !names.contains("${it.name}: ${it.type}") }
|
||||
|
||||
log.debug("Creating setters/getters for fields: {}", fields.map { it.name })
|
||||
|
||||
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateForFields(
|
||||
data: ActionData,
|
||||
task: CompileTask,
|
||||
type: ClassTree,
|
||||
paths: List<TreePath>,
|
||||
) {
|
||||
val file = data.requirePath()
|
||||
val editor = data[CodeEditor::class.java]!!
|
||||
val trees = Trees.instance(task.task)
|
||||
val insert = EditHelper.insertAtEndOfClass(task.task, task.root(file), type)
|
||||
val sb = StringBuilder()
|
||||
|
||||
for (path in paths) {
|
||||
val element = trees.getElement(path) ?: continue
|
||||
if (element !is VariableElement) {
|
||||
continue
|
||||
}
|
||||
|
||||
val leaf = path.leaf
|
||||
val indent = EditHelper.indent(task.task, task.root(file), leaf) + EditorPreferences.tabSize
|
||||
sb.append(createGetter(element, indent))
|
||||
if (!element.modifiers.contains(FINAL)) {
|
||||
sb.append(createSetter(element, indent))
|
||||
}
|
||||
}
|
||||
|
||||
ThreadUtils.runOnUiThread {
|
||||
editor.text.insert(insert.line, insert.column, sb)
|
||||
editor.formatCodeAsync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createGetter(variable: VariableElement, indent: Int): String {
|
||||
val name = variable.simpleName.toString()
|
||||
val method =
|
||||
createMethod(variable, "get", toType(variable.asType())) { _, body ->
|
||||
body.addStatement(createReturnStmt(name))
|
||||
}
|
||||
var text = "\n" + JavaParserUtils.prettyPrint(method) { false }
|
||||
text = text.replace("\n", "\n${indentationString(indent)}")
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun createReturnStmt(name: String) = StaticJavaParser.parseStatement("return this.$name;")
|
||||
|
||||
private fun createSetter(variable: VariableElement, indent: Int): String {
|
||||
val name: String = variable.simpleName.toString()
|
||||
val method =
|
||||
createMethod(variable, "set", VoidType()) { method, body ->
|
||||
method.addParameter(toType(variable.asType()), name)
|
||||
body.addStatement(createAssignmentStmt(name))
|
||||
}
|
||||
|
||||
var text = "\n" + JavaParserUtils.prettyPrint(method) { false }
|
||||
text = text.replace("\n", "\n${indentationString(indent)}")
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
private fun createMethod(
|
||||
variable: VariableElement,
|
||||
prefix: String,
|
||||
returnType: Type,
|
||||
vararg modifiers: Modifier.Keyword = arrayOf(Modifier.Keyword.PUBLIC),
|
||||
block: (MethodDeclaration, BlockStmt) -> Unit
|
||||
): MethodDeclaration {
|
||||
val name = variable.simpleName.toString()
|
||||
val method = MethodDeclaration()
|
||||
val body = method.createBody()
|
||||
method.name = SimpleName(createName(name, prefix))
|
||||
method.type = returnType
|
||||
method.addModifier(*modifiers)
|
||||
block(method, body)
|
||||
return method
|
||||
}
|
||||
|
||||
private fun createAssignmentStmt(name: String) =
|
||||
StaticJavaParser.parseStatement("this.$name = $name;")
|
||||
|
||||
private fun createName(name: String, prefix: String): String {
|
||||
val sb = StringBuilder(name)
|
||||
sb.setCharAt(0, Character.toUpperCase(sb[0]))
|
||||
sb.insert(0, prefix)
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
+189
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.actions.generators
|
||||
|
||||
import android.content.Context
|
||||
import com.blankj.utilcode.util.ThreadUtils
|
||||
import com.github.javaparser.StaticJavaParser
|
||||
import com.github.javaparser.ast.Modifier
|
||||
import com.github.javaparser.ast.body.MethodDeclaration
|
||||
import com.github.javaparser.ast.stmt.ReturnStmt
|
||||
import com.itsaky.androidide.actions.ActionData
|
||||
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.FieldBasedAction
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import com.itsaky.androidide.preferences.internal.EditorPreferences
|
||||
import com.itsaky.androidide.preferences.utils.indentationString
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.resources.R.string
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import jdkx.lang.model.element.VariableElement
|
||||
import openjdk.source.tree.ClassTree
|
||||
import openjdk.source.tree.VariableTree
|
||||
import openjdk.source.util.TreePath
|
||||
import openjdk.tools.javac.api.JavacTrees
|
||||
import openjdk.tools.javac.code.Symbol.ClassSymbol
|
||||
import openjdk.tools.javac.code.Symbol.MethodSymbol
|
||||
import openjdk.tools.javac.tree.JCTree
|
||||
import openjdk.tools.javac.tree.TreeInfo
|
||||
import openjdk.tools.javac.util.Names
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Generates the `toString()` method for the current class.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class GenerateToStringMethodAction : FieldBasedAction() {
|
||||
|
||||
override val titleTextRes: Int = R.string.action_generate_toString
|
||||
override val id: String = "ide.editor.lsp.java.generator.toString"
|
||||
override var label: String = ""
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(GenerateToStringMethodAction::class.java)
|
||||
}
|
||||
|
||||
override fun onGetFields(fields: List<String>, data: ActionData) {
|
||||
showFieldSelector(fields, data) { selected ->
|
||||
CompletableFuture.runAsync { generateToString(data, selected) }
|
||||
.whenComplete { _, error ->
|
||||
if (error != null) {
|
||||
log.error("Unable to generate toString() implementation", error)
|
||||
ThreadUtils.runOnUiThread {
|
||||
flashError(
|
||||
data[Context::class.java]!!.getString(R.string.msg_cannot_generate_toString)
|
||||
)
|
||||
}
|
||||
return@whenComplete
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateToString(data: ActionData, selected: MutableSet<String>) {
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
|
||||
val range = data[com.itsaky.androidide.models.Range::class.java]!!
|
||||
val file = data.requirePath()
|
||||
|
||||
compiler.compile(file).run { task ->
|
||||
val triple = findFields(task, file, range)
|
||||
val typeFinder = triple.first
|
||||
val type = triple.second
|
||||
val fields = triple.third
|
||||
|
||||
fields.removeIf { !selected.contains("${it.name}: ${it.type}") }
|
||||
|
||||
log.debug("Creating toString() method with fields: {}", fields.map { it.name })
|
||||
|
||||
generateForFields(data, task, type, fields.map { TreePath(typeFinder.path, it) })
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateForFields(
|
||||
data: ActionData,
|
||||
task: CompileTask,
|
||||
type: ClassTree,
|
||||
paths: List<TreePath>
|
||||
) {
|
||||
if (isToStringOverridden(task, type)) {
|
||||
ThreadUtils.runOnUiThread {
|
||||
flashError(data[Context::class.java]!!.getString(string.msg_toString_overridden))
|
||||
}
|
||||
log.warn("toString() method has already been overridden in class {}", type.simpleName)
|
||||
return
|
||||
}
|
||||
|
||||
val file = data.requirePath()
|
||||
val editor = data[CodeEditor::class.java]!!
|
||||
val trees = JavacTrees.instance(task.task)
|
||||
val indent = EditHelper.indent(task.task, task.root(), type) + EditorPreferences.tabSize
|
||||
val insert = EditHelper.insertAtEndOfClass(task.task, task.root(file), type)
|
||||
val string = StringBuilder()
|
||||
var isFirst = true
|
||||
|
||||
string.append("\"")
|
||||
string.append(type.simpleName)
|
||||
string.append('[')
|
||||
for (path in paths) {
|
||||
val element = trees.getElement(path) ?: continue
|
||||
if (element !is VariableElement) {
|
||||
continue
|
||||
}
|
||||
|
||||
val leaf = path.leaf as VariableTree
|
||||
if (!isFirst) {
|
||||
string.append(", ")
|
||||
}
|
||||
string.append(leaf.name)
|
||||
string.append("=")
|
||||
string.append("\" + ")
|
||||
string.append(leaf.name)
|
||||
string.append(" + \"")
|
||||
|
||||
// "ClassName[field1=' + field1 + ', field2=' + field2 + ']"
|
||||
|
||||
isFirst = false
|
||||
}
|
||||
string.append("]\"")
|
||||
|
||||
val method = overrideToString()
|
||||
val body = method.createBody()
|
||||
body.addStatement(createReturnStatement(string.toString()))
|
||||
|
||||
var text = "\n" + method.toString()
|
||||
text = text.replace("\n", "\n${indentationString(indent)}")
|
||||
text += "\n"
|
||||
|
||||
ThreadUtils.runOnUiThread {
|
||||
editor.text.insert(insert.line, insert.column, text)
|
||||
editor.formatCodeAsync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isToStringOverridden(task: CompileTask, type: ClassTree): Boolean {
|
||||
val names = Names.instance(task.task.context)
|
||||
val sym = TreeInfo.symbolFor(type as JCTree) as ClassSymbol
|
||||
val toStrings =
|
||||
sym.members().getSymbolsByName(names.toString).filterIsInstance<MethodSymbol>().filter {
|
||||
it.params.isEmpty()
|
||||
}
|
||||
|
||||
return toStrings.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun createReturnStatement(string: String): ReturnStmt {
|
||||
return StaticJavaParser.parseStatement("return $string;") as ReturnStmt
|
||||
}
|
||||
|
||||
private fun overrideToString(): MethodDeclaration {
|
||||
val method = MethodDeclaration()
|
||||
method.addMarkerAnnotation("Override")
|
||||
method.addModifier(Modifier.Keyword.PUBLIC)
|
||||
method.setType("String")
|
||||
method.setName("toString")
|
||||
return method
|
||||
}
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.actions.generators
|
||||
|
||||
import android.content.Context
|
||||
import com.blankj.utilcode.util.ThreadUtils
|
||||
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.lsp.java.JavaCompilerProvider
|
||||
import com.itsaky.androidide.lsp.java.actions.BaseJavaCodeAction
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider
|
||||
import com.itsaky.androidide.lsp.java.parser.ParseTask
|
||||
import com.itsaky.androidide.lsp.java.rewrite.AddImport
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import com.itsaky.androidide.lsp.java.utils.FindHelper
|
||||
import com.itsaky.androidide.lsp.java.utils.JavaParserUtils
|
||||
import com.itsaky.androidide.lsp.java.utils.MethodPtr
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarationAt
|
||||
import com.itsaky.androidide.models.Position
|
||||
import com.itsaky.androidide.preferences.internal.EditorPreferences
|
||||
import com.itsaky.androidide.preferences.utils.indentationString
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import com.itsaky.androidide.resources.R
|
||||
import com.itsaky.androidide.utils.flashError
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
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.type.DeclaredType
|
||||
import jdkx.lang.model.type.ExecutableType
|
||||
import jdkx.tools.JavaFileObject
|
||||
import openjdk.source.tree.MethodTree
|
||||
import openjdk.source.util.Trees
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.Arrays
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
/**
|
||||
* Allows the user to override multiple methods from superclass at once.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
|
||||
|
||||
override val titleTextRes: Int = R.string.action_override_superclass_methods
|
||||
override val id: String = "ide.editor.lsp.java.generator.overrideSuperclassMethods"
|
||||
override var label: String = ""
|
||||
private var position: Long = -1
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(OverrideSuperclassMethodsAction::class.java)
|
||||
}
|
||||
|
||||
override fun prepare(data: ActionData) {
|
||||
super.prepare(data)
|
||||
|
||||
if (
|
||||
!visible ||
|
||||
!data.hasRequiredData(
|
||||
com.itsaky.androidide.models.Range::class.java,
|
||||
CodeEditor::class.java
|
||||
)
|
||||
) {
|
||||
markInvisible()
|
||||
return
|
||||
}
|
||||
|
||||
visible = true
|
||||
enabled = true
|
||||
}
|
||||
|
||||
override suspend fun execAction(data: ActionData): Any {
|
||||
val range = data[com.itsaky.androidide.models.Range::class.java]!!
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
|
||||
val file = data.requirePath()
|
||||
|
||||
return compiler.compile(file).get { task ->
|
||||
// 1-based line and column index
|
||||
val startLine = range.start.line + 1
|
||||
val startColumn = range.start.column + 1
|
||||
val endLine = range.end.line + 1
|
||||
val endColumn = range.end.column + 1
|
||||
val lines = task.root().lineMap
|
||||
val start = lines.getPosition(startLine.toLong(), startColumn.toLong())
|
||||
val end = lines.getPosition(endLine.toLong(), endColumn.toLong())
|
||||
|
||||
if (start == (-1).toLong() || end == (-1).toLong()) {
|
||||
return@get false
|
||||
}
|
||||
|
||||
this.position = start
|
||||
val typeFinder = FindTypeDeclarationAt(task.task)
|
||||
var type = typeFinder.scan(task.root(file), start)
|
||||
if (type == null) {
|
||||
type = typeFinder.scan(task.root(file), end)
|
||||
position = end
|
||||
}
|
||||
|
||||
if (type == null) {
|
||||
return@get false
|
||||
}
|
||||
|
||||
val overridable = mutableListOf<MethodPtr>()
|
||||
val trees = Trees.instance(task.task)
|
||||
val elements = task.task.elements
|
||||
val classPath = typeFinder.path
|
||||
val element = trees.getElement(classPath) as TypeElement
|
||||
|
||||
for (member in elements.getAllMembers(element)) {
|
||||
if (member.modifiers.contains(Modifier.FINAL) || member.kind != ElementKind.METHOD) {
|
||||
continue
|
||||
}
|
||||
|
||||
val method = member as ExecutableElement
|
||||
val methodSource = member.getEnclosingElement() as TypeElement
|
||||
if (
|
||||
methodSource.qualifiedName.contentEquals("java.lang.Object") || methodSource == element
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
val pointer = MethodPtr(task.task, method)
|
||||
overridable.add(pointer)
|
||||
}
|
||||
|
||||
return@get overridable
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun postExec(data: ActionData, result: Any) {
|
||||
if (result !is List<*> || result.isEmpty() || position < 0) {
|
||||
log.warn("Unable to find any overridable method")
|
||||
flashError(data[Context::class.java]!!.getString(R.string.msg_no_overridable_methods))
|
||||
return
|
||||
}
|
||||
|
||||
val methods = (result as List<MethodPtr>).sortedBy { it.methodName }
|
||||
val checkedMethods = mutableListOf<MethodPtr>()
|
||||
val names = mapMethodPointers(methods)
|
||||
val builder = newDialogBuilder(data)
|
||||
builder.setTitle(data[Context::class.java]!!.getString(R.string.msg_select_methods))
|
||||
builder.setMultiChoiceItems(names, BooleanArray(names.size)) { _, which, isChecked ->
|
||||
checkedMethods.apply {
|
||||
if (isChecked) {
|
||||
add(methods[which])
|
||||
} else {
|
||||
remove(methods[which])
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
|
||||
if (checkedMethods.isEmpty()) {
|
||||
flashError(data[Context::class.java]!!.getString(R.string.msg_no_methods_selected))
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
CompletableFuture.runAsync { overrideMethods(data, checkedMethods) }
|
||||
.whenComplete {
|
||||
_, error,
|
||||
->
|
||||
if (error != null) {
|
||||
log.error("An error occurred overriding methods")
|
||||
|
||||
ThreadUtils.runOnUiThread {
|
||||
flashError(
|
||||
data[Context::class.java]!!.getString(R.string.msg_cannot_override_methods)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(android.R.string.cancel, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun overrideMethods(data: ActionData, checkedMethods: MutableList<MethodPtr>) {
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
|
||||
val file = data.requirePath()
|
||||
|
||||
compiler.compile(file).run { task ->
|
||||
val types = task.task.types
|
||||
val trees = Trees.instance(task.task)
|
||||
val sb = StringBuilder()
|
||||
val imports = mutableSetOf<String>()
|
||||
|
||||
val typeFinder = FindTypeDeclarationAt(task.task)
|
||||
val classTree = typeFinder.scan(task.root(), position)
|
||||
val thisClass = trees.getElement(typeFinder.path) as TypeElement
|
||||
val indent = EditHelper.indent(task.task, task.root(), classTree) + EditorPreferences.tabSize
|
||||
val fileImports = task.root(file).imports.map { it.qualifiedIdentifier.toString() }.toSet()
|
||||
val filePackage = task.root(file).`package`.packageName.toString()
|
||||
|
||||
for (pointer in checkedMethods) {
|
||||
val superMethod =
|
||||
FindHelper.findMethod(
|
||||
task,
|
||||
pointer.className,
|
||||
pointer.methodName,
|
||||
pointer.erasedParameterTypes
|
||||
) ?: continue
|
||||
|
||||
val thisDeclaredType = thisClass.asType() as DeclaredType
|
||||
val executableType = types.asMemberOf(thisDeclaredType, superMethod) as ExecutableType
|
||||
val source = findSource(compiler, task, superMethod)
|
||||
val method =
|
||||
if (source != null) {
|
||||
JavaParserUtils.printMethod(superMethod, executableType, source)
|
||||
} else {
|
||||
JavaParserUtils.printMethod(superMethod, executableType, superMethod)
|
||||
}
|
||||
|
||||
val newImports = JavaParserUtils.collectImports(executableType)
|
||||
sb.append("\n")
|
||||
sb.append(method.toString())
|
||||
sb.replace(Regex(Regex.escape("\n")), "\n${indentationString(indent)}")
|
||||
sb.append("\n")
|
||||
|
||||
newImports.removeIf {
|
||||
it.startsWith("java.lang.") || it.startsWith(filePackage) || fileImports.contains(it)
|
||||
}
|
||||
|
||||
imports.addAll(newImports)
|
||||
}
|
||||
|
||||
ThreadUtils.runOnUiThread {
|
||||
performEdits(
|
||||
data,
|
||||
sb,
|
||||
imports,
|
||||
EditHelper.insertAtEndOfClass(task.task, task.root(file), classTree)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performEdits(
|
||||
data: ActionData,
|
||||
sb: StringBuilder,
|
||||
imports: MutableSet<String>,
|
||||
position: Position,
|
||||
) {
|
||||
val compiler =
|
||||
JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
|
||||
val editor = data[CodeEditor::class.java]!!
|
||||
val file = data.requirePath()
|
||||
val text = editor.text
|
||||
|
||||
text.beginBatchEdit()
|
||||
|
||||
text.insert(position.line, position.column, sb)
|
||||
|
||||
for (name in imports) {
|
||||
val rewrite = AddImport(file, name)
|
||||
val edits = rewrite.rewrite(compiler)[file]
|
||||
if (edits.isNullOrEmpty()) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (edit in edits) {
|
||||
if (edit.range.start == edit.range.end) {
|
||||
text.insert(edit.range.start.line, edit.range.start.column, edit.newText)
|
||||
} else {
|
||||
text.replace(
|
||||
edit.range.start.line,
|
||||
edit.range.start.column,
|
||||
edit.range.end.line,
|
||||
edit.range.end.column,
|
||||
edit.newText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.endBatchEdit()
|
||||
editor.formatCodeAsync()
|
||||
}
|
||||
|
||||
private fun mapMethodPointers(methods: List<MethodPtr>): Array<CharSequence> {
|
||||
return methods
|
||||
.map { Arrays.toString(it.simplifiedErasedParameterTypes) }
|
||||
.map {
|
||||
val arr = it.toCharArray()
|
||||
arr[0] = '('
|
||||
arr[arr.size - 1] = ')'
|
||||
|
||||
String(arr)
|
||||
}
|
||||
.mapIndexed { index, params -> "${methods[index].methodName}$params" }
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
private fun findSource(
|
||||
compiler: CompilerProvider,
|
||||
task: CompileTask,
|
||||
method: ExecutableElement,
|
||||
): MethodTree? {
|
||||
val superClass = method.enclosingElement as TypeElement
|
||||
val superClassName = superClass.qualifiedName.toString()
|
||||
val methodName = method.simpleName.toString()
|
||||
val erasedParameterTypes = FindHelper.erasedParameterTypes(task, method)
|
||||
val sourceFile: Optional<JavaFileObject> = compiler.findAnywhere(superClassName)
|
||||
if (!sourceFile.isPresent) return null
|
||||
val parse: ParseTask = compiler.parse(sourceFile.get())
|
||||
return FindHelper.findMethod(parse, superClassName, methodName, erasedParameterTypes)
|
||||
}
|
||||
}
|
||||
+38
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import openjdk.source.tree.CompilationUnitTree
|
||||
import openjdk.tools.javac.api.JavacTaskImpl
|
||||
import java.util.function.*
|
||||
|
||||
/**
|
||||
* A compilation task processor process the [JavacTaskImpl]. Usually, a processor decides what files
|
||||
* should be parsed and analyzed.
|
||||
*
|
||||
* @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<CompilationUnitTree>)
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import static com.itsaky.androidide.javac.config.JavacConfigProvider.PROP_ANDROIDIDE_JAVA_HOME;
|
||||
import static com.itsaky.androidide.javac.config.JavacConfigProvider.disableModules;
|
||||
import static com.itsaky.androidide.javac.config.JavacConfigProvider.enableModules;
|
||||
import static com.itsaky.androidide.javac.config.JavacConfigProvider.setLatestSourceVersion;
|
||||
import static com.itsaky.androidide.javac.config.JavacConfigProvider.setLatestSupportedSourceVersion;
|
||||
import static com.itsaky.androidide.utils.Environment.JAVA_HOME;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Pair;
|
||||
import com.itsaky.androidide.builder.model.IJavaCompilerSettings;
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableBorrow;
|
||||
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
|
||||
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
|
||||
import com.itsaky.androidide.lsp.java.visitors.MethodRangeScanner;
|
||||
import com.itsaky.androidide.models.Range;
|
||||
import com.itsaky.androidide.projects.api.ModuleProject;
|
||||
import com.itsaky.androidide.projects.util.StringSearch;
|
||||
import com.itsaky.androidide.tooling.api.ProjectType;
|
||||
import com.itsaky.androidide.utils.ClassTrie;
|
||||
import com.itsaky.androidide.utils.SourceClassTrie;
|
||||
import com.itsaky.androidide.utils.StopWatch;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import jdkx.lang.model.SourceVersion;
|
||||
import jdkx.tools.Diagnostic;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import openjdk.source.util.TreePath;
|
||||
import openjdk.tools.javac.api.ClientCodeWrapper;
|
||||
import openjdk.tools.javac.api.JavacTaskImpl;
|
||||
import openjdk.tools.javac.code.Kinds;
|
||||
import openjdk.tools.javac.util.JCDiagnostic;
|
||||
|
||||
public class CompileBatch implements AutoCloseable {
|
||||
|
||||
public static final String DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION = "11";
|
||||
protected final JavaCompilerService parent;
|
||||
protected final ReusableBorrow borrow;
|
||||
protected final JavacTaskImpl task;
|
||||
protected final List<CompilationUnitTree> roots;
|
||||
protected final Map<String, List<Pair<Range, TreePath>>> methodPositions = new HashMap<>();
|
||||
protected DiagnosticListenerImpl diagnosticListener;
|
||||
/** Indicates the task that requested the compilation is finished with it. */
|
||||
boolean closed;
|
||||
|
||||
CompileBatch(
|
||||
JavaCompilerService parent,
|
||||
Collection<? extends JavaFileObject> files,
|
||||
CompilationRequest compilationRequest) {
|
||||
this.parent = parent;
|
||||
this.borrow = batchTask(parent, files);
|
||||
this.task = borrow.task;
|
||||
this.roots = new ArrayList<>();
|
||||
|
||||
final var context = task.getContext();
|
||||
final var config = JavaCompilerConfig.instance(context);
|
||||
config.setFiles(files);
|
||||
|
||||
if (compilationRequest.configureContext != null) {
|
||||
compilationRequest.configureContext.accept(context);
|
||||
}
|
||||
|
||||
Objects.requireNonNull(compilationRequest, "A task processor is required");
|
||||
|
||||
try {
|
||||
compilationRequest.compilationTaskProcessor.process(borrow.task, this::processCompilationUnit);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
config.setFiles(null);
|
||||
}
|
||||
|
||||
private void processCompilationUnit(final CompilationUnitTree root) {
|
||||
roots.add(root);
|
||||
// updatePositions(root, false);
|
||||
}
|
||||
|
||||
void updatePositions(CompilationUnitTree tree, boolean allowDuplicate) {
|
||||
final StopWatch watch = new StopWatch("Scan method positions");
|
||||
final List<Pair<Range, TreePath>> positions = new ArrayList<>();
|
||||
new MethodRangeScanner(this.task).scan(tree, positions);
|
||||
final String path = new File(tree.getSourceFile().toUri()).getAbsolutePath();
|
||||
final List<Pair<Range, TreePath>> old = this.methodPositions.put(path, positions);
|
||||
if (old != null && !allowDuplicate) {
|
||||
throw new IllegalStateException(
|
||||
"Duplicate CompilationUnitTree for file:" + tree.getSourceFile().toUri());
|
||||
}
|
||||
|
||||
watch.log();
|
||||
}
|
||||
|
||||
private ReusableBorrow batchTask(
|
||||
@NonNull JavaCompilerService parent, @NonNull Collection<? extends JavaFileObject> sources) {
|
||||
|
||||
parent.diagnostics.clear();
|
||||
final Iterable<String> options = options();
|
||||
|
||||
diagnosticListener =
|
||||
new DiagnosticListenerWrapper(parent.diagnostics::add, sources.iterator().next());
|
||||
|
||||
final ReusableBorrow borrow =
|
||||
parent.compiler.getTask(
|
||||
parent.fileManager, diagnosticListener, options, Collections.emptyList(), sources);
|
||||
|
||||
if (parent.fileManager != null) {
|
||||
parent.fileManager.setContext(borrow.task.getContext());
|
||||
}
|
||||
|
||||
return borrow;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private List<String> options() {
|
||||
List<String> options = new ArrayList<>();
|
||||
|
||||
// This won't be used if the current module is Android module project
|
||||
System.setProperty(PROP_ANDROIDIDE_JAVA_HOME, JAVA_HOME.getAbsolutePath());
|
||||
if (this.parent.module != null && this.parent.module.getType() == ProjectType.Android) {
|
||||
setLatestSourceVersion(SourceVersion.RELEASE_8);
|
||||
setLatestSupportedSourceVersion(SourceVersion.RELEASE_11);
|
||||
disableModules();
|
||||
} else {
|
||||
setLatestSourceVersion(SourceVersion.RELEASE_11);
|
||||
setLatestSupportedSourceVersion(SourceVersion.RELEASE_11);
|
||||
enableModules();
|
||||
}
|
||||
|
||||
setupCompileOptions(parent.module, options);
|
||||
Collections.addAll(options, "-proc:none", "-g");
|
||||
|
||||
Collections.addAll(
|
||||
options,
|
||||
"-XDcompilePolicy=byfile",
|
||||
"-XD-Xprefer=source",
|
||||
"-XDide",
|
||||
"-XDkeepCommentsOverride=ignore",
|
||||
"-XDsuppressAbortOnBadClassFile",
|
||||
"-XDshould-stop.at=GENERATE",
|
||||
"-XDdiags.formatterOptions=-source",
|
||||
"-XDdiags.layout=%L%m|%L%m|%L%m",
|
||||
"-XDbreakDocCommentParsingOnError=false",
|
||||
"-Xlint:cast",
|
||||
"-Xlint:deprecation",
|
||||
"-Xlint:empty",
|
||||
"-Xlint:fallthrough",
|
||||
"-Xlint:finally",
|
||||
"-Xlint:path",
|
||||
"-Xlint:unchecked",
|
||||
"-Xlint:varargs",
|
||||
"-Xlint:static");
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
protected void setupCompileOptions(final ModuleProject module, final List<String> options) {
|
||||
if (module == null) {
|
||||
Collections.addAll(
|
||||
options,
|
||||
"-source",
|
||||
DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION,
|
||||
"-target",
|
||||
DEFAULT_COMPILER_SOURCE_AND_TARGET_VERSION);
|
||||
return;
|
||||
}
|
||||
|
||||
final IJavaCompilerSettings compilerSettings = module.getCompilerSettings();
|
||||
options.add("-source");
|
||||
options.add(compilerSettings.getJavaSourceVersion());
|
||||
options.add("-target");
|
||||
options.add(compilerSettings.getJavaBytecodeVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the compilation failed because javac didn't find some package-private files in source files
|
||||
* with different names, list those source files.
|
||||
*/
|
||||
Set<Path> needsAdditionalSources() {
|
||||
|
||||
if (parent.getModule() == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
// Check for "class not found errors" that refer to package private classes
|
||||
Set<Path> addFiles = new HashSet<>();
|
||||
for (Diagnostic<? extends JavaFileObject> err : parent.diagnostics) {
|
||||
if (!err.getCode().equals("compiler.err.cant.resolve.location")) {
|
||||
continue;
|
||||
}
|
||||
if (!isValidFileRange(err)) {
|
||||
continue;
|
||||
}
|
||||
String packageName = packageName(err);
|
||||
ClassTrie.Node node = parent.getModule().compileJavaSourceClasses.findNode(packageName);
|
||||
if (node != null && node.isClass() && node instanceof SourceClassTrie.SourceNode) {
|
||||
addFiles.add(((SourceClassTrie.SourceNode) node).getFile());
|
||||
}
|
||||
}
|
||||
return addFiles;
|
||||
}
|
||||
|
||||
private String packageName(Diagnostic<? extends JavaFileObject> err) {
|
||||
if (err instanceof ClientCodeWrapper.DiagnosticSourceUnwrapper) {
|
||||
JCDiagnostic diagnostic = ((ClientCodeWrapper.DiagnosticSourceUnwrapper) err).d;
|
||||
JCDiagnostic.DiagnosticPosition pos = diagnostic.getDiagnosticPosition();
|
||||
Object[] args = diagnostic.getArgs();
|
||||
Kinds.KindName kind = (Kinds.KindName) args[0];
|
||||
if (kind == Kinds.KindName.CLASS) {
|
||||
if (pos.toString().contains(".")) {
|
||||
return pos.toString().substring(0, pos.toString().lastIndexOf('.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
Path file = Paths.get(err.getSource().toUri());
|
||||
return StringSearch.packageName(file);
|
||||
}
|
||||
|
||||
private boolean isValidFileRange(Diagnostic<? extends JavaFileObject> d) {
|
||||
return d.getSource().toUri().getScheme().equals("file")
|
||||
&& d.getStartPosition() >= 0
|
||||
&& d.getEndPosition() >= 0;
|
||||
}
|
||||
|
||||
public static class DiagnosticListenerWrapper extends DiagnosticListenerImpl {
|
||||
|
||||
private final Consumer<Diagnostic<? extends JavaFileObject>> consumer;
|
||||
|
||||
public DiagnosticListenerWrapper(
|
||||
final Consumer<Diagnostic<? extends JavaFileObject>> consumer, JavaFileObject jfo) {
|
||||
super(jfo);
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void report(final Diagnostic<? extends JavaFileObject> diagnostic) {
|
||||
consumer.accept(diagnostic);
|
||||
super.report(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.itsaky.androidide.javac.services.partial.DiagnosticListenerImpl;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import jdkx.tools.Diagnostic;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import openjdk.tools.javac.api.JavacTaskImpl;
|
||||
|
||||
public class CompileTask implements AutoCloseable {
|
||||
|
||||
public final JavacTaskImpl task;
|
||||
public final List<CompilationUnitTree> roots;
|
||||
public final List<Diagnostic<? extends JavaFileObject>> diagnostics;
|
||||
public final CompileBatch compileBatch;
|
||||
public final DiagnosticListenerImpl diagnosticListener;
|
||||
|
||||
public CompileTask(
|
||||
@NonNull CompileBatch compileBatch, List<Diagnostic<? extends JavaFileObject>> diagnostics) {
|
||||
this.compileBatch = compileBatch;
|
||||
this.task = compileBatch.task;
|
||||
this.roots = compileBatch.roots;
|
||||
this.diagnostics = diagnostics;
|
||||
this.diagnosticListener = compileBatch.diagnosticListener;
|
||||
}
|
||||
|
||||
public CompilationUnitTree root() {
|
||||
if (roots.size() != 1) {
|
||||
throw new RuntimeException("No compilation units found. Roots: " + roots.size());
|
||||
}
|
||||
return roots.get(0);
|
||||
}
|
||||
|
||||
public CompilationUnitTree root(Path file) {
|
||||
for (CompilationUnitTree root : roots) {
|
||||
if (root.getSourceFile().toUri().equals(file.toUri())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Compilation unit not found");
|
||||
}
|
||||
|
||||
public CompilationUnitTree root(JavaFileObject file) {
|
||||
for (CompilationUnitTree root : roots) {
|
||||
if (root.getSourceFile().toUri().equals(file.toUri())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Compilation unit not found");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
|
||||
import com.itsaky.androidide.lsp.java.parser.ParseTask;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
|
||||
public interface CompilerProvider {
|
||||
Path NOT_FOUND = Paths.get("");
|
||||
|
||||
TreeSet<String> publicTopLevelTypes();
|
||||
|
||||
TreeSet<String> packagePrivateTopLevelTypes(String packageName);
|
||||
|
||||
Optional<JavaFileObject> findAnywhere(String className);
|
||||
|
||||
Path findTypeDeclaration(String className);
|
||||
|
||||
Path[] findTypeReferences(String className);
|
||||
|
||||
Path[] findMemberReferences(String className, String memberName);
|
||||
|
||||
default List<String> findQualifiedNames(String simpleName) {
|
||||
return findQualifiedNames(simpleName, false);
|
||||
}
|
||||
|
||||
List<String> findQualifiedNames(String simpleName, boolean onlyOne);
|
||||
|
||||
ParseTask parse(Path file);
|
||||
|
||||
ParseTask parse(JavaFileObject file);
|
||||
|
||||
default SynchronizedTask compile(Path... files) {
|
||||
return compile(Arrays.stream(files).map(SourceFileObject::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
default SynchronizedTask compile(Collection<? extends JavaFileObject> sources) {
|
||||
return compile(new CompilationRequest(sources));
|
||||
}
|
||||
|
||||
SynchronizedTask compile(CompilationRequest request);
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import com.itsaky.androidide.utils.StopWatch
|
||||
import openjdk.source.tree.CompilationUnitTree
|
||||
import openjdk.tools.javac.api.JavacTaskImpl
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Default implementation of [CompilationTaskProcessor].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class DefaultCompilationTaskProcessor : CompilationTaskProcessor {
|
||||
|
||||
override fun process(task: JavacTaskImpl, processCompilationUnit: Consumer<CompilationUnitTree>) {
|
||||
val watch = StopWatch("Process compilation task")
|
||||
val trees = task.parse()
|
||||
watch.lapFromLast("Parsed treees")
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableContext
|
||||
|
||||
/**
|
||||
* Implementation of the [ReusableCompiler] which replaces necessary components from the
|
||||
* [ReusableContext].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JCReusableCompiler : ReusableCompiler() {
|
||||
|
||||
override fun onCreateContext(): ReusableContext {
|
||||
return super.onCreateContext().also {
|
||||
JavaCompilerImpl.preRegister(context = it, replace = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import com.itsaky.androidide.models.Position
|
||||
import jdkx.tools.JavaFileObject
|
||||
import openjdk.tools.javac.util.Context
|
||||
|
||||
/**
|
||||
* Configuration for the [JavaCompilerImpl].
|
||||
*
|
||||
* @property files The main files that are being compiled/parsed.
|
||||
* @property completionInfo Information about the completion
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JavaCompilerConfig(context: Context) {
|
||||
init {
|
||||
context.put(compilerConfigKey, this)
|
||||
}
|
||||
|
||||
var files: Collection<JavaFileObject>? = null
|
||||
var completionInfo: CompletionInfo? = null
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmField val compilerConfigKey = Context.Key<JavaCompilerConfig>()
|
||||
|
||||
@JvmStatic
|
||||
fun instance(context: Context): JavaCompilerConfig {
|
||||
var instance = context.get(compilerConfigKey)
|
||||
if (instance == null) {
|
||||
instance = JavaCompilerConfig(context)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the completion request initiated by the Java completion provider.
|
||||
*
|
||||
* @property cursor The cursor position for the completion.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
data class CompletionInfo(val cursor: Position)
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableContext
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableJavaCompiler
|
||||
import com.itsaky.androidide.lsp.java.parser.ts.TSJavaParser
|
||||
import com.itsaky.androidide.lsp.java.parser.ts.TSMethodPruner.prune
|
||||
import com.itsaky.androidide.projects.FileManager
|
||||
import com.itsaky.androidide.utils.VMUtils
|
||||
import com.itsaky.androidide.utils.withStopWatch
|
||||
import jdkx.tools.JavaFileObject
|
||||
import jdkx.tools.JavaFileObject.Kind.SOURCE
|
||||
import openjdk.tools.javac.api.ClientCodeWrapper
|
||||
import openjdk.tools.javac.tree.JCTree.JCCompilationUnit
|
||||
import openjdk.tools.javac.util.Context
|
||||
import kotlin.io.path.name
|
||||
|
||||
class JavaCompilerImpl(context: Context?) : ReusableJavaCompiler(context) {
|
||||
|
||||
override fun parse(filename: JavaFileObject?, content: CharSequence?): JCCompilationUnit {
|
||||
|
||||
if (VMUtils.isJvm()) {
|
||||
return super.parse(filename, content)
|
||||
}
|
||||
|
||||
val file = ClientCodeWrapper.instance(context).unwrap(filename)
|
||||
val compilerConfig = JavaCompilerConfig.instance(context)
|
||||
|
||||
// Preconditions
|
||||
if (
|
||||
content == null ||
|
||||
compilerConfig.files == null ||
|
||||
filename?.kind != SOURCE ||
|
||||
compilerConfig.files?.contains(file) == false
|
||||
) {
|
||||
return super.parse(filename, content)
|
||||
}
|
||||
|
||||
// If the file is NOT being parsed for a completion request,
|
||||
// we should not prune method bodies of active documents
|
||||
if (compilerConfig.completionInfo == null && FileManager.isActive(filename.toUri())) {
|
||||
return super.parse(filename, content)
|
||||
}
|
||||
|
||||
val pruned = withStopWatch("${if(file is SourceFileObject) "[${file.path.name}] " else ""}Prune method bodies") { watch ->
|
||||
val contentBuilder = StringBuilder(content)
|
||||
|
||||
return@withStopWatch TSJavaParser.parse(file).use { parseResult ->
|
||||
|
||||
prune(
|
||||
contentBuilder,
|
||||
parseResult.tree,
|
||||
compilerConfig.completionInfo?.cursor?.index ?: -1
|
||||
)
|
||||
|
||||
watch.log()
|
||||
|
||||
return@use contentBuilder
|
||||
}
|
||||
}
|
||||
|
||||
return super.parse(filename, pruned)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun preRegister(context: ReusableContext, replace: Boolean = false) {
|
||||
if (replace) {
|
||||
context.drop(compilerKey)
|
||||
}
|
||||
context.put(compilerKey, Context.Factory { JavaCompilerImpl(it) })
|
||||
}
|
||||
}
|
||||
}
|
||||
+576
@@ -0,0 +1,576 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent;
|
||||
import com.itsaky.androidide.javac.services.compiler.ReusableCompiler;
|
||||
import com.itsaky.androidide.javac.services.partial.CompilationInfo;
|
||||
import com.itsaky.androidide.javac.services.partial.PartialReparser;
|
||||
import com.itsaky.androidide.javac.services.partial.PartialReparserImpl;
|
||||
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
|
||||
import com.itsaky.androidide.lsp.java.models.PartialReparseRequest;
|
||||
import com.itsaky.androidide.lsp.java.parser.ParseTask;
|
||||
import com.itsaky.androidide.lsp.java.parser.Parser;
|
||||
import com.itsaky.androidide.lsp.java.utils.Extractors;
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindTypeDeclarations;
|
||||
import com.itsaky.androidide.models.Position;
|
||||
import com.itsaky.androidide.models.Range;
|
||||
import com.itsaky.androidide.projects.FileManager;
|
||||
import com.itsaky.androidide.projects.api.AndroidModule;
|
||||
import com.itsaky.androidide.projects.api.ModuleProject;
|
||||
import com.itsaky.androidide.projects.util.BootClasspathProvider;
|
||||
import com.itsaky.androidide.projects.util.StringSearch;
|
||||
import com.itsaky.androidide.utils.Cache;
|
||||
import com.itsaky.androidide.utils.Environment;
|
||||
import com.itsaky.androidide.utils.SourceClassTrie;
|
||||
import com.itsaky.androidide.utils.StopWatch;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import jdkx.tools.Diagnostic;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
import jdkx.tools.StandardLocation;
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import openjdk.source.tree.MethodTree;
|
||||
import openjdk.source.util.SourcePositions;
|
||||
import openjdk.source.util.TreePath;
|
||||
import openjdk.source.util.Trees;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class JavaCompilerService implements CompilerProvider {
|
||||
|
||||
public static final JavaCompilerService NO_MODULE_COMPILER = new JavaCompilerService(null);
|
||||
private static final Cache<String, Boolean> cacheContainsWord = new Cache<>();
|
||||
private static final Cache<Void, List<String>> cacheContainsType = new Cache<>();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaCompilerService.class);
|
||||
protected final Set<String> classPathClasses;
|
||||
protected final List<Diagnostic<? extends JavaFileObject>> diagnostics = new ArrayList<>();
|
||||
protected final Map<JavaFileObject, Long> cachedModified = new HashMap<>();
|
||||
protected final Cache<Void, List<String>> cacheFileImports = new Cache<>();
|
||||
protected final SynchronizedTask synchronizedTask = new SynchronizedTask();
|
||||
protected final SourceFileManager fileManager;
|
||||
protected final ModuleProject module;
|
||||
public ReusableCompiler compiler = new JCReusableCompiler();
|
||||
protected Set<String> bootClasspathClasses =
|
||||
BootClasspathProvider.getTopLevelClasses(
|
||||
Collections.singleton(Environment.ANDROID_JAR.getAbsolutePath()));
|
||||
private CompileBatch cachedCompile;
|
||||
private int changeDelta = 0;
|
||||
|
||||
private Position lastReparsePosition = Position.NONE;
|
||||
private Position newCursorPosition = Position.NONE;
|
||||
|
||||
// The module project must not be null
|
||||
// It is marked as nullable just for some special cases like tests
|
||||
public JavaCompilerService(@Nullable ModuleProject module) {
|
||||
this.module = module;
|
||||
if (module == null) {
|
||||
this.fileManager = SourceFileManager.NO_MODULE;
|
||||
this.classPathClasses = Collections.emptySet();
|
||||
} else {
|
||||
this.fileManager = SourceFileManager.forModule(module);
|
||||
this.classPathClasses =
|
||||
Collections.unmodifiableSet(module.compileClasspathClasses.allClassNames());
|
||||
this.bootClasspathClasses = Collections.unmodifiableSet(getBootclasspathClasses());
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getBootclasspathClasses() {
|
||||
if (module != null && module instanceof AndroidModule) {
|
||||
final List<String> classpaths =
|
||||
((AndroidModule) module)
|
||||
.getBootClassPaths().stream().map(File::getPath).collect(Collectors.toList());
|
||||
BootClasspathProvider.update(classpaths);
|
||||
this.bootClasspathClasses =
|
||||
Collections.unmodifiableSet(BootClasspathProvider.getTopLevelClasses(classpaths));
|
||||
}
|
||||
return bootClasspathClasses;
|
||||
}
|
||||
|
||||
private JavaCompilerService(
|
||||
@Nullable ModuleProject module,
|
||||
SourceFileManager fileManager,
|
||||
Set<String> bootClasspathClasses,
|
||||
Set<String> classPathClasses) {
|
||||
this.module = module;
|
||||
this.fileManager = fileManager;
|
||||
this.bootClasspathClasses = bootClasspathClasses;
|
||||
this.classPathClasses = classPathClasses;
|
||||
}
|
||||
|
||||
public ModuleProject getModule() {
|
||||
return module;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeSet<String> publicTopLevelTypes() {
|
||||
TreeSet<String> all = new TreeSet<>();
|
||||
List<SourceClassTrie.SourceNode> sourceClasses =
|
||||
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
|
||||
for (SourceClassTrie.SourceNode node : sourceClasses) {
|
||||
all.add(node.getQualifiedName());
|
||||
}
|
||||
all.addAll(classPathClasses);
|
||||
all.addAll(bootClasspathClasses);
|
||||
return all;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TreeSet<String> packagePrivateTopLevelTypes(String packageName) {
|
||||
return new TreeSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<JavaFileObject> findAnywhere(String className) {
|
||||
Path fromSource = findTypeDeclaration(className);
|
||||
if (fromSource != NOT_FOUND) {
|
||||
return Optional.of(new SourceFileObject(fromSource));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path findTypeDeclaration(String className) {
|
||||
Path fastFind = findPublicTypeDeclaration(className);
|
||||
if (fastFind != NOT_FOUND) {
|
||||
return fastFind;
|
||||
}
|
||||
// In principle, the slow path can be skipped in many cases.
|
||||
// If we're spending a lot of time in findTypeDeclaration, this would be a good
|
||||
// optimization.
|
||||
String packageName = Extractors.packageName(className);
|
||||
String simpleName = Extractors.simpleName(className);
|
||||
List<SourceClassTrie.SourceNode> classes =
|
||||
module != null ? module.listClassesFromSourceDirs(packageName) : Collections.emptyList();
|
||||
for (SourceClassTrie.SourceNode node : classes) {
|
||||
final Path path = node.getFile();
|
||||
if (containsWord(path, simpleName) && containsType(path, className)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return NOT_FOUND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path[] findTypeReferences(String className) {
|
||||
String packageName = Extractors.packageName(className);
|
||||
String simpleName = Extractors.simpleName(className);
|
||||
List<Path> candidates = new ArrayList<>();
|
||||
List<SourceClassTrie.SourceNode> sourceNodes =
|
||||
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
|
||||
for (SourceClassTrie.SourceNode node : sourceNodes) {
|
||||
final Path path = node.getFile();
|
||||
if (containsWord(path, packageName)
|
||||
&& containsImport(path, className)
|
||||
&& containsWord(path, simpleName)) {
|
||||
candidates.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.toArray(new Path[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Path[] findMemberReferences(String className, String memberName) {
|
||||
List<Path> candidates = new ArrayList<>();
|
||||
List<SourceClassTrie.SourceNode> sourceNodes =
|
||||
module != null ? module.compileJavaSourceClasses.allSources() : Collections.emptyList();
|
||||
for (SourceClassTrie.SourceNode node : sourceNodes) {
|
||||
final Path path = node.getFile();
|
||||
if (containsWord(path, memberName)) {
|
||||
candidates.add(path);
|
||||
}
|
||||
}
|
||||
return candidates.toArray(new Path[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findQualifiedNames(String simpleName, boolean onlyOne) {
|
||||
final var names = new ArrayList<String>();
|
||||
for (var name : publicTopLevelTypes()) {
|
||||
// This will be true in a test environment
|
||||
if (name.contains("/")) {
|
||||
name = name.replace('/', '.');
|
||||
}
|
||||
|
||||
if (name.endsWith("." + simpleName)) {
|
||||
names.add(name);
|
||||
if (onlyOne) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseTask parse(Path file) {
|
||||
Parser parser = Parser.parseFile(file);
|
||||
return new ParseTask(parser.task, parser.root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParseTask parse(JavaFileObject file) {
|
||||
Parser parser = Parser.parseJavaFileObject(file);
|
||||
return new ParseTask(parser.task, parser.root);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SynchronizedTask compile(final CompilationRequest request) {
|
||||
return compileBatch(request);
|
||||
}
|
||||
|
||||
private SynchronizedTask compileBatch(CompilationRequest request) {
|
||||
synchronizedTask.post(
|
||||
() -> {
|
||||
if (needsCompilation(request.sources)) {
|
||||
reparseOrRecompile(request);
|
||||
} else {
|
||||
LOG.info("...using cached compile");
|
||||
}
|
||||
synchronizedTask.setTask(new CompileTask(cachedCompile, diagnostics));
|
||||
});
|
||||
|
||||
return synchronizedTask;
|
||||
}
|
||||
|
||||
private boolean needsCompilation(Collection<? extends JavaFileObject> sources) {
|
||||
if (cachedModified.size() != sources.size()) {
|
||||
return true;
|
||||
}
|
||||
for (JavaFileObject f : sources) {
|
||||
if (!cachedModified.containsKey(f)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Long modified = cachedModified.get(f);
|
||||
if (modified != null && f.getLastModified() != modified) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private synchronized void reparseOrRecompile(CompilationRequest request) {
|
||||
// if (needsRecompilation(request)) {
|
||||
// LOG.warn("Cannot reparse. Recompilation is required");
|
||||
recompile(request);
|
||||
// } else {
|
||||
// LOG.debug("Trying to perform a reparse...");
|
||||
// tryReparse(request);
|
||||
// }
|
||||
}
|
||||
|
||||
private boolean needsRecompilation(final CompilationRequest request) {
|
||||
return this.cachedCompile == null
|
||||
|| this.cachedCompile.closed;
|
||||
// || request.partialRequest == null
|
||||
// || request.partialRequest.cursor < 0
|
||||
// || !isChangeValidForReparse()
|
||||
// || request.sources.size() != 1; // Cannot perform a reparse if there are multiple files
|
||||
}
|
||||
|
||||
private boolean isChangeValidForReparse() {
|
||||
return this.lastReparsePosition == Position.NONE
|
||||
|| (this.newCursorPosition != Position.NONE
|
||||
&& this.lastReparsePosition.getLine() == this.newCursorPosition.getLine());
|
||||
}
|
||||
|
||||
private void tryReparse(@NonNull final CompilationRequest request) {
|
||||
|
||||
// Satisfy lint
|
||||
final PartialReparseRequest partialRequest = request.partialRequest;
|
||||
Objects.requireNonNull(partialRequest);
|
||||
|
||||
final StopWatch watch = new StopWatch("Method reparse");
|
||||
final File file = new File(request.sources.iterator().next().toUri());
|
||||
final String path = file.getAbsolutePath();
|
||||
final List<Pair<Range, TreePath>> positions = this.cachedCompile.methodPositions.get(path);
|
||||
if (positions == null) {
|
||||
LOG.warn("Cannot perform reparse. No method positions found.");
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
final Pair<Range, TreePath> currentMethod =
|
||||
binarySearchCurrentMethod(positions, partialRequest.cursor);
|
||||
if (currentMethod == null) {
|
||||
LOG.warn("Cannot perform reparse. Unable to find current method");
|
||||
recompile(request);
|
||||
return;
|
||||
} else {
|
||||
watch.lapFromLast("Found method at cursor position");
|
||||
}
|
||||
|
||||
final MethodTree methodTree = (MethodTree) currentMethod.second.getLeaf();
|
||||
LOG.debug("Trying to reparse method: {}", methodTree.getName());
|
||||
|
||||
final CompilationInfo info =
|
||||
new CompilationInfo(
|
||||
cachedCompile.task, cachedCompile.diagnosticListener, cachedCompile.roots.get(0));
|
||||
watch.setLastLap(System.currentTimeMillis());
|
||||
final SourcePositions sourcePositions = Trees.instance(cachedCompile.task).getSourcePositions();
|
||||
final int start = (int) sourcePositions.getStartPosition(info.cu, methodTree.getBody());
|
||||
final int end =
|
||||
(int) sourcePositions.getEndPosition(info.cu, methodTree.getBody()) + this.changeDelta;
|
||||
|
||||
if (start < 0 || end < 0 || start > end || end >= partialRequest.contents.length()) {
|
||||
LOG.warn(
|
||||
"Cannot reparse. Invalid change delta. end: {} changeDelta: {} content.length: {}",
|
||||
end,
|
||||
this.changeDelta,
|
||||
partialRequest.contents.length()
|
||||
);
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
watch.lapFromLast("Found start and end positions of current method");
|
||||
final PartialReparser reparser = new PartialReparserImpl();
|
||||
|
||||
final String newBody = partialRequest.contents.substring(start, end);
|
||||
final boolean reparsed =
|
||||
reparser.reparseMethod(info, currentMethod.second, newBody, partialRequest.contents);
|
||||
if (!reparsed) {
|
||||
LOG.error("Failed to reparse");
|
||||
recompile(request);
|
||||
return;
|
||||
}
|
||||
|
||||
watch.log();
|
||||
LOG.info("Successfully reparsed method: {}", methodTree.getName());
|
||||
updateModificationCache(request);
|
||||
cachedCompile.updatePositions(info.cu, true);
|
||||
this.changeDelta = 0;
|
||||
this.lastReparsePosition = this.newCursorPosition;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Range, TreePath> binarySearchCurrentMethod(
|
||||
@NonNull final List<Pair<Range, TreePath>> positions, final long cursor) {
|
||||
int left = 0;
|
||||
int right = positions.size() - 1;
|
||||
while (left <= right) {
|
||||
int mid = (left + right) / 2;
|
||||
final Pair<Range, TreePath> method = positions.get(mid);
|
||||
final Range range = method.first;
|
||||
final int startIndex = range.getStart().requireIndex();
|
||||
final int endIndex = range.getEnd().requireIndex();
|
||||
|
||||
if (cursor < startIndex) {
|
||||
right = mid - 1;
|
||||
} else if (cursor > endIndex) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized void recompile(CompilationRequest request) {
|
||||
close();
|
||||
this.cachedCompile = performCompilation(request);
|
||||
this.changeDelta = 0;
|
||||
updateModificationCache(request);
|
||||
}
|
||||
|
||||
public synchronized void close() {
|
||||
if (cachedCompile != null) {
|
||||
cachedCompile.close();
|
||||
cachedCompile.borrow.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateModificationCache(final CompilationRequest request) {
|
||||
cachedModified.clear();
|
||||
for (JavaFileObject f : request.sources) {
|
||||
cachedModified.put(f, f.getLastModified());
|
||||
}
|
||||
}
|
||||
|
||||
private CompileBatch performCompilation(CompilationRequest request) {
|
||||
final Collection<? extends JavaFileObject> sources = request.sources;
|
||||
if (sources.isEmpty()) {
|
||||
throw new RuntimeException("empty sources");
|
||||
}
|
||||
|
||||
CompileBatch firstAttempt = new CompileBatch(this, sources, request);
|
||||
Set<Path> addFiles = firstAttempt.needsAdditionalSources();
|
||||
|
||||
if (addFiles.isEmpty()) {
|
||||
return firstAttempt;
|
||||
}
|
||||
|
||||
// If the compiler needs additional source files that contain package-private files
|
||||
LOG.info("...need to recompile with {}", addFiles);
|
||||
firstAttempt.close();
|
||||
firstAttempt.borrow.close();
|
||||
|
||||
List<JavaFileObject> moreSources = new ArrayList<>(sources);
|
||||
for (Path add : addFiles) {
|
||||
moreSources.add(new SourceFileObject(add));
|
||||
}
|
||||
|
||||
return new CompileBatch(this, moreSources, request);
|
||||
}
|
||||
|
||||
private boolean containsWord(Path file, String word) {
|
||||
if (cacheContainsWord.needs(file, word)) {
|
||||
cacheContainsWord.load(file, word, StringSearch.containsWord(file, word));
|
||||
}
|
||||
return cacheContainsWord.get(file, word);
|
||||
}
|
||||
|
||||
private boolean containsImport(Path file, String className) {
|
||||
String packageName = Extractors.packageName(className);
|
||||
if (packageNameOrEmpty(file).equals(packageName)) {
|
||||
return true;
|
||||
}
|
||||
String star = packageName + ".*";
|
||||
for (String i : readImports(file)) {
|
||||
if (i.equals(className) || i.equals(star)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<String> readImports(Path file) {
|
||||
if (cacheFileImports.needs(file, null)) {
|
||||
loadImports(file);
|
||||
}
|
||||
return cacheFileImports.get(file, null);
|
||||
}
|
||||
|
||||
private void loadImports(Path file) {
|
||||
List<String> list = new ArrayList<>();
|
||||
Pattern importClass = Pattern.compile("^import +([\\w.]+\\.\\w+);");
|
||||
Pattern importStar = Pattern.compile("^import +([\\w.]+\\.\\*);");
|
||||
try (BufferedReader lines = FileManager.INSTANCE.getReader(file)) {
|
||||
for (String line = lines.readLine(); line != null; line = lines.readLine()) {
|
||||
// If we reach a class declaration, stop looking for imports
|
||||
// TODO This could be a little more specific
|
||||
if (line.contains("class")) {
|
||||
break;
|
||||
}
|
||||
// import foo.bar.Doh;
|
||||
Matcher matchesClass = importClass.matcher(line);
|
||||
if (matchesClass.matches()) {
|
||||
list.add(matchesClass.group(1));
|
||||
}
|
||||
// import foo.bar.*
|
||||
Matcher matchesStar = importStar.matcher(line);
|
||||
if (matchesStar.matches()) {
|
||||
list.add(matchesStar.group(1));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
cacheFileImports.load(file, null, list);
|
||||
}
|
||||
|
||||
private String packageNameOrEmpty(Path file) {
|
||||
return module != null ? module.packageNameOrEmpty(file) : "";
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
synchronizedTask.post(
|
||||
() -> {
|
||||
close();
|
||||
cachedCompile = null;
|
||||
cachedModified.clear();
|
||||
compiler = new ReusableCompiler();
|
||||
});
|
||||
}
|
||||
|
||||
public SynchronizedTask getSynchronizedTask() {
|
||||
return synchronizedTask;
|
||||
}
|
||||
|
||||
public void onDocumentChange(@NonNull DocumentChangeEvent event) {
|
||||
this.changeDelta += event.getChangeDelta();
|
||||
this.newCursorPosition = event.getChangeRange().getEnd();
|
||||
}
|
||||
|
||||
public JavaCompilerService copy() {
|
||||
final JavaCompilerService compiler =
|
||||
new JavaCompilerService(
|
||||
this.module, this.fileManager, this.bootClasspathClasses, this.classPathClasses);
|
||||
compiler.cachedCompile = null;
|
||||
compiler.newCursorPosition = Position.NONE;
|
||||
compiler.lastReparsePosition = Position.NONE;
|
||||
compiler.changeDelta = 0;
|
||||
compiler.compiler = new ReusableCompiler();
|
||||
compiler.diagnostics.clear();
|
||||
compiler.cachedModified.clear();
|
||||
return compiler;
|
||||
}
|
||||
|
||||
private boolean containsType(Path file, String className) {
|
||||
if (cacheContainsType.needs(file, null)) {
|
||||
CompilationUnitTree root = parse(file).root;
|
||||
List<String> types = new ArrayList<>();
|
||||
new FindTypeDeclarations().scan(root, types);
|
||||
cacheContainsType.load(file, null, types);
|
||||
}
|
||||
return cacheContainsType.get(file, null).contains(className);
|
||||
}
|
||||
|
||||
private Path findPublicTypeDeclaration(String className) {
|
||||
JavaFileObject source;
|
||||
try {
|
||||
source =
|
||||
fileManager.getJavaFileForInput(
|
||||
StandardLocation.SOURCE_PATH, className, JavaFileObject.Kind.SOURCE);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
if (source == null) {
|
||||
return NOT_FOUND;
|
||||
}
|
||||
if (!source.toUri().getScheme().equals("file")) {
|
||||
return NOT_FOUND;
|
||||
}
|
||||
Path file = Paths.get(source.toUri());
|
||||
if (!containsType(file, className)) {
|
||||
return NOT_FOUND;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.emptySet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.blankj.utilcode.util.CloseUtils;
|
||||
import com.itsaky.androidide.javac.config.JavacConfigProvider;
|
||||
import com.itsaky.androidide.javac.services.fs.AndroidFsProviderImpl;
|
||||
import com.itsaky.androidide.projects.api.AndroidModule;
|
||||
import com.itsaky.androidide.projects.api.ModuleProject;
|
||||
import com.itsaky.androidide.projects.util.StringSearch;
|
||||
import com.itsaky.androidide.utils.ClassTrie;
|
||||
import com.itsaky.androidide.utils.Environment;
|
||||
import com.itsaky.androidide.utils.SourceClassTrie;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Stream;
|
||||
import jdkx.tools.FileObject;
|
||||
import jdkx.tools.ForwardingJavaFileManager;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
import jdkx.tools.StandardLocation;
|
||||
import openjdk.tools.javac.api.JavacTool;
|
||||
import openjdk.tools.javac.file.JavacFileManager;
|
||||
import openjdk.tools.javac.util.Context;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SourceFileManager extends ForwardingJavaFileManager<JavacFileManager> {
|
||||
|
||||
public static final SourceFileManager NO_MODULE;
|
||||
private static final Logger LOG;
|
||||
private static final Map<ModuleProject, SourceFileManager> cachedFileManagers =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
// Initialize LOG first to prevent NPE
|
||||
LOG = LoggerFactory.getLogger(SourceFileManager.class);
|
||||
NO_MODULE = new SourceFileManager(null);
|
||||
}
|
||||
|
||||
private final ModuleProject module;
|
||||
|
||||
private SourceFileManager(final ModuleProject module) {
|
||||
super(createDelegateFileManager());
|
||||
this.module = module;
|
||||
|
||||
AndroidFsProviderImpl.INSTANCE.init();
|
||||
|
||||
if (module == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be set before setting classpaths
|
||||
System.setProperty(JavacConfigProvider.PROP_ANDROIDIDE_JAVA_HOME,
|
||||
Environment.JAVA_HOME.getAbsolutePath());
|
||||
|
||||
setLocationLogError(StandardLocation.SOURCE_PATH, module.getCompileSourceDirectories());
|
||||
setLocationLogError(StandardLocation.CLASS_PATH, configureClasspaths(module));
|
||||
listLocations(EnumSet.of(StandardLocation.CLASS_PATH, StandardLocation.PLATFORM_CLASS_PATH));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Set<File> configureClasspaths(final ModuleProject module) {
|
||||
if (module == null) {
|
||||
return emptySet();
|
||||
}
|
||||
|
||||
if (module instanceof AndroidModule) {
|
||||
final AndroidModule androidModule = (AndroidModule) module;
|
||||
setLocationLogError(StandardLocation.PLATFORM_CLASS_PATH, androidModule.getBootClassPaths());
|
||||
}
|
||||
|
||||
return module.getCompileClasspaths();
|
||||
}
|
||||
|
||||
private static JavacFileManager createDelegateFileManager() {
|
||||
return JavacTool.create()
|
||||
.getStandardFileManager(msg -> LOG.debug(String.valueOf(msg)), Locale.getDefault(),
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public void setLocationLogError(Location location, Iterable<File> searchPath) {
|
||||
try {
|
||||
fileManager.setLocation(location, searchPath);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to set location", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void listLocations(final EnumSet<StandardLocation> locations) {
|
||||
for (StandardLocation location : locations) {
|
||||
try {
|
||||
fileManager.cacheLocation(location);
|
||||
} catch (Exception e) {
|
||||
// Ignored
|
||||
LOG.debug("Failed to list location: {}", location, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<JavaFileObject> list(
|
||||
Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
|
||||
throws IOException {
|
||||
if (location == StandardLocation.SOURCE_PATH) {
|
||||
if (this.module == null) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
final ClassTrie.Node node = module.compileJavaSourceClasses.findNode(packageName);
|
||||
if (node == null || node.isClass()) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
final Stream<ClassTrie.Node> nodes;
|
||||
if (recurse) {
|
||||
nodes = node.allClassNodes().stream();
|
||||
} else {
|
||||
nodes = node.getChildren().values().stream();
|
||||
}
|
||||
|
||||
final Stream<JavaFileObject> stream =
|
||||
nodes
|
||||
.filter(it -> it instanceof SourceClassTrie.SourceNode)
|
||||
.map(it -> asJavaFileObject((SourceClassTrie.SourceNode) it));
|
||||
|
||||
return stream::iterator;
|
||||
}
|
||||
|
||||
return super.list(location, packageName, kinds, recurse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String inferBinaryName(Location location, JavaFileObject file) {
|
||||
if (location == StandardLocation.SOURCE_PATH) {
|
||||
SourceFileObject source = (SourceFileObject) file;
|
||||
String packageName = packageNameOrEmpty(source.path);
|
||||
String className = removeExtension(source.path.getFileName().toString());
|
||||
if (!packageName.isEmpty()) {
|
||||
className = packageName + "." + className;
|
||||
}
|
||||
return className;
|
||||
}
|
||||
|
||||
return super.inferBinaryName(location, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLocation(Location location) {
|
||||
return location == StandardLocation.SOURCE_PATH || super.hasLocation(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JavaFileObject getJavaFileForInput(
|
||||
Location location, String className, JavaFileObject.Kind kind) throws IOException {
|
||||
if (location == StandardLocation.SOURCE_PATH) {
|
||||
String packageName = StringSearch.mostName(className);
|
||||
String simpleClassName = StringSearch.lastName(className);
|
||||
List<SourceClassTrie.SourceNode> classes =
|
||||
module != null ? module.listClassesFromSourceDirs(packageName) : emptyList();
|
||||
for (SourceClassTrie.SourceNode node : classes) {
|
||||
final Path path = node.getFile();
|
||||
if (path.getFileName().toString().equals(simpleClassName + kind.extension)) {
|
||||
return new SourceFileObject(path);
|
||||
}
|
||||
}
|
||||
// Fall through to disk in case we have .jar or .zip files on the source path
|
||||
}
|
||||
return super.getJavaFileForInput(location, className, kind);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileObject getFileForInput(Location location, String packageName, String relativeName)
|
||||
throws IOException {
|
||||
if (location == StandardLocation.SOURCE_PATH) {
|
||||
return null;
|
||||
}
|
||||
return super.getFileForInput(location, packageName, relativeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Location location, FileObject file) throws IOException {
|
||||
if (location == StandardLocation.SOURCE_PATH) {
|
||||
SourceFileObject source = (SourceFileObject) file;
|
||||
return module.compileJavaSourceClasses.findSource(source.path) != null;
|
||||
} else {
|
||||
return super.contains(location, file);
|
||||
}
|
||||
}
|
||||
|
||||
private String packageNameOrEmpty(Path file) {
|
||||
return this.module != null ? module.packageNameOrEmpty(file) : "";
|
||||
}
|
||||
|
||||
private String removeExtension(String fileName) {
|
||||
int lastDot = fileName.lastIndexOf(".");
|
||||
return (lastDot == -1 ? fileName : fileName.substring(0, lastDot));
|
||||
}
|
||||
|
||||
private JavaFileObject asJavaFileObject(SourceClassTrie.SourceNode node) {
|
||||
// TODO erase method bodies of files that are not open
|
||||
return new SourceFileObject(node.getFile());
|
||||
}
|
||||
|
||||
public static SourceFileManager forModule(@NonNull ModuleProject project) {
|
||||
Objects.requireNonNull(project);
|
||||
return cachedFileManagers.computeIfAbsent(project, SourceFileManager::createForModule);
|
||||
}
|
||||
|
||||
private static SourceFileManager createForModule(@NonNull ModuleProject project) {
|
||||
LOG.info("Creating source file manager instance for module: {}", project);
|
||||
return new SourceFileManager(project);
|
||||
}
|
||||
|
||||
public static void clearCache() {
|
||||
for (final SourceFileManager fileManager : cachedFileManagers.values()) {
|
||||
CloseUtils.closeIO(fileManager);
|
||||
}
|
||||
|
||||
cachedFileManagers.clear();
|
||||
}
|
||||
|
||||
public void setContext(final Context context) {
|
||||
fileManager.setContext(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.compiler;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.itsaky.androidide.projects.FileManager;
|
||||
import com.itsaky.androidide.utils.DocumentUtils;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.Writer;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import jdkx.lang.model.element.Modifier;
|
||||
import jdkx.lang.model.element.NestingKind;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
|
||||
public class SourceFileObject implements JavaFileObject {
|
||||
/** path is the absolute path to this file on disk */
|
||||
final Path path;
|
||||
/** contents is the text in this file, or null if we should use the text in FileStore */
|
||||
String contents;
|
||||
/** if contents is set, the modified time of contents */
|
||||
Instant modified;
|
||||
|
||||
public SourceFileObject(Path path) {
|
||||
this(path, null, Instant.EPOCH);
|
||||
}
|
||||
|
||||
public SourceFileObject(Path path, String contents, Instant modified) {
|
||||
if (!DocumentUtils.isJavaFile(path)) throw new RuntimeException(path + " is not a java source");
|
||||
this.path = path;
|
||||
this.contents = contents;
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this).add("path", this.path.toString()).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind getKind() {
|
||||
String name = path.getFileName().toString();
|
||||
return kindFromExtension(name);
|
||||
}
|
||||
|
||||
private static Kind kindFromExtension(String name) {
|
||||
for (Kind candidate : Kind.values()) {
|
||||
if (name.endsWith(candidate.extension)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNameCompatible(String simpleName, Kind kind) {
|
||||
return path.getFileName().toString().equals(simpleName + kind.extension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NestingKind getNestingKind() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modifier getAccessLevel() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI toUri() {
|
||||
return this.path.toAbsolutePath().toUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream openInputStream() {
|
||||
if (contents != null) {
|
||||
byte[] bytes = contents.getBytes();
|
||||
return new ByteArrayInputStream(bytes);
|
||||
}
|
||||
return FileManager.INSTANCE.getInputStream(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream openOutputStream() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Reader openReader(boolean ignoreEncodingErrors) {
|
||||
if (contents != null) {
|
||||
return new StringReader(contents);
|
||||
}
|
||||
return FileManager.INSTANCE.getReader(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
|
||||
if (contents != null) {
|
||||
return contents;
|
||||
}
|
||||
return FileManager.INSTANCE.getDocumentContents(this.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Writer openWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastModified() {
|
||||
if (contents != null) {
|
||||
return modified.toEpochMilli();
|
||||
}
|
||||
return FileManager.INSTANCE.getLastModified(this.path).toEpochMilli();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof SourceFileObject)) {
|
||||
return false;
|
||||
}
|
||||
final SourceFileObject that = (SourceFileObject) o;
|
||||
try {
|
||||
return this.path != null && that.path != null
|
||||
&& Files.isSameFile(this.path, that.path)
|
||||
&& Objects.equals(contents, that.contents)
|
||||
&& Objects.equals(modified, that.modified);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(path, contents, modified);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.compiler
|
||||
|
||||
import com.itsaky.androidide.lsp.java.CompilationCancellationException
|
||||
import com.itsaky.androidide.lsp.java.utils.CancelChecker.Companion.isCancelled
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.concurrent.Semaphore
|
||||
|
||||
class SynchronizedTask {
|
||||
|
||||
@Volatile
|
||||
@PublishedApi
|
||||
internal var isCompiling = false
|
||||
|
||||
@PublishedApi
|
||||
internal val semaphore = Semaphore(1)
|
||||
|
||||
@PublishedApi
|
||||
internal var task: CompileTask? = null
|
||||
private set
|
||||
|
||||
companion object {
|
||||
|
||||
@PublishedApi
|
||||
internal val log = LoggerFactory.getLogger(SynchronizedTask::class.java)
|
||||
}
|
||||
|
||||
inline fun run(crossinline taskConsumer: (CompileTask) -> Unit) {
|
||||
try {
|
||||
semaphore.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
throw CompilationCancellationException(e)
|
||||
}
|
||||
try {
|
||||
taskConsumer(task!!)
|
||||
} catch (err: Throwable) {
|
||||
if (!isCancelled(err)) {
|
||||
log.error("An error occurred while working with compilation task", err)
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
semaphore.release()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : Any?> get(crossinline action: (CompileTask) -> T): T {
|
||||
try {
|
||||
semaphore.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
throw CompilationCancellationException(e)
|
||||
}
|
||||
return try {
|
||||
action(task!!)
|
||||
} catch (err: Throwable) {
|
||||
if (!isCancelled(err)) {
|
||||
log.error("An error occurred while working with compilation task", err)
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
semaphore.release()
|
||||
}
|
||||
}
|
||||
|
||||
fun post(action: Runnable) = post { action.run() }
|
||||
|
||||
inline fun post(action: () -> Unit) {
|
||||
try {
|
||||
semaphore.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
throw CompilationCancellationException(e)
|
||||
}
|
||||
isCompiling = true
|
||||
try {
|
||||
if (task != null) {
|
||||
task!!.close()
|
||||
}
|
||||
action()
|
||||
} catch (err: Throwable) {
|
||||
if (!isCancelled(err)) {
|
||||
log.error("An error occurred", err)
|
||||
}
|
||||
throw err
|
||||
} finally {
|
||||
semaphore.release()
|
||||
isCompiling = false
|
||||
}
|
||||
}
|
||||
|
||||
fun setTask(task: CompileTask?) {
|
||||
this.task = task
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val isBusy: Boolean
|
||||
get() = isCompiling || semaphore.availablePermits() == 0
|
||||
|
||||
/**
|
||||
* **FOR INTERNAL USE ONLY!**
|
||||
*/
|
||||
fun logStats() {
|
||||
log.warn("[SynchronizedTask] isCompiling={} queuedLength={}", isCompiling,
|
||||
semaphore.queueLength)
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.edits
|
||||
|
||||
import com.itsaky.androidide.lsp.java.JavaCompilerProvider
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Handles edits for Java completion items.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditHandler() {
|
||||
|
||||
override fun performEdits(
|
||||
item: CompletionItem,
|
||||
editor: CodeEditor,
|
||||
text: Content,
|
||||
line: Int,
|
||||
column: Int,
|
||||
index: Int
|
||||
) {
|
||||
val compiler = JavaCompilerProvider.get(
|
||||
IProjectManager.getInstance().findModuleForFile(file, false) ?: return)
|
||||
performEdits(compiler, editor, item)
|
||||
|
||||
executeCommand(editor, item.command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Java edit handlers which require instance of the compiler should override this method instead
|
||||
* of [performEdits].
|
||||
*
|
||||
* @param compiler The compiler service instance.
|
||||
* @param editor The editor to perform edits on.
|
||||
* @param completionItem The completion item which contains required data.
|
||||
*/
|
||||
abstract fun performEdits(
|
||||
compiler: JavaCompilerService,
|
||||
editor: CodeEditor,
|
||||
completionItem: CompletionItem
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.edits
|
||||
|
||||
import com.itsaky.androidide.editor.api.ILspEditor
|
||||
import com.itsaky.androidide.lsp.edits.DefaultEditHandler
|
||||
import com.itsaky.androidide.lsp.models.Command
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
|
||||
/**
|
||||
* Implementation of [DefaultEditHandler] which avoids reflection in
|
||||
* [DefaultEditHandler.executeCommand].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
open class BaseJavaEditHandler : DefaultEditHandler() {
|
||||
|
||||
override fun executeCommand(editor: CodeEditor, command: Command?) {
|
||||
if (editor is ILspEditor) {
|
||||
editor.executeCommand(command)
|
||||
return
|
||||
}
|
||||
super.executeCommand(editor, command)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.edits
|
||||
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import com.itsaky.androidide.lsp.models.ClassCompletionData
|
||||
import com.itsaky.androidide.lsp.models.CompletionItem
|
||||
import com.itsaky.androidide.lsp.util.RewriteHelper
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Imports the required class for a ClassCompletionItem.
|
||||
*
|
||||
* @param imports The current file imports.
|
||||
* @param file The file in which this edit will be performed.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class ClassImportEditHandler(val imports: Set<String>, file: Path) : AdvancedJavaEditHandler(file) {
|
||||
|
||||
override fun performEdits(
|
||||
compiler: JavaCompilerService,
|
||||
editor: CodeEditor,
|
||||
completionItem: CompletionItem
|
||||
) {
|
||||
val data = completionItem.data as? ClassCompletionData ?: return
|
||||
val className = data.className
|
||||
val edits = EditHelper.addImportIfNeeded(compiler, file, imports, className)
|
||||
|
||||
if (edits.isNotEmpty()) {
|
||||
RewriteHelper.performEdits(edits, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.edits
|
||||
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.lsp.java.utils.EditHelper
|
||||
import io.github.rosemoe.sora.widget.CodeEditor
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Imports multiple classes at once.
|
||||
*
|
||||
* @param classes The fully qualified classnames to import.
|
||||
* @param imported The current imports of the given file.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class MultipleClassImportEditHandler(
|
||||
private val classes: Set<String>,
|
||||
private val imported: Set<String>,
|
||||
file: Path
|
||||
) : AdvancedJavaEditHandler(file) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(MultipleClassImportEditHandler::class.java)
|
||||
}
|
||||
|
||||
override fun performEdits(
|
||||
compiler: JavaCompilerService,
|
||||
editor: CodeEditor,
|
||||
completionItem: com.itsaky.androidide.lsp.models.CompletionItem
|
||||
) {
|
||||
val edits = mutableListOf<com.itsaky.androidide.lsp.models.TextEdit>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
/**
|
||||
* Anything that can be cloned.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface ICloneable {
|
||||
fun clone(): ICloneable
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import com.itsaky.androidide.indexing.IIndexable
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IJavaIndexable : IIndexable
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
|
||||
/**
|
||||
* A java symbol.
|
||||
*
|
||||
* @property accessFlags Access flags of the symbol.
|
||||
* @property apiInfo Info about the APIs in which this symbol was added, deprecated or removed in.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IJavaSymbol : IJavaIndexable {
|
||||
var accessFlags: Int
|
||||
var apiInfo: ApiInfo?
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A Java user type. May be a class, interface, enum, or annotation. Qualified name of a type or a
|
||||
* package name is always represented in the (partially) internal form. For example, the fully qualified
|
||||
* of `java.lang.Object` is `java/lang/Object` and the package name is `java/lang`.
|
||||
*
|
||||
* @property fqn The fully qualified name of the type.
|
||||
* @property name The simple name of the type.
|
||||
* @property packageName The package name of the type.
|
||||
* @property superInterfacesFqn The fully qualified names of the interfaces implemented by the type.
|
||||
* @property fields The fields of the type.
|
||||
* @property methods The methods of the type.
|
||||
* @property isClass Whether the type is a class.
|
||||
* @property isInterface Whether the type is an interface.
|
||||
* @property isAnnotation Whether the type is an annotation.
|
||||
* @property isEnum Whether the type is an enum.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IJavaType<FieldT : IJavaSymbol, MethodT : IJavaSymbol> : IJavaSymbol {
|
||||
var fqn: String?
|
||||
var name: String?
|
||||
var packageName: String?
|
||||
var isInner: Boolean
|
||||
|
||||
/**
|
||||
* The fully qualified name of the superclass of the type.
|
||||
*
|
||||
* Info about fields and methods are availabe in the class file itself. Hhowever, a class may
|
||||
* extend a class (or implement an interface) which may be defined in other jar files. As a result,
|
||||
* we only store the fully qualifed names of the classes and interfaces then add an index based on
|
||||
* these fully qualifed names in the database.
|
||||
*
|
||||
* This property is not [Required] because we also index `java/lang/Object`, which does not have
|
||||
* a superclass.
|
||||
*/
|
||||
var superClassFqn: String?
|
||||
var superInterfacesFqn: RealmList<String>?
|
||||
var fields: RealmList<FieldT>?
|
||||
var methods: RealmList<MethodT>?
|
||||
|
||||
val isClass: Boolean
|
||||
get() = false
|
||||
|
||||
val isInterface: Boolean
|
||||
get() = false
|
||||
|
||||
val isAnnotation: Boolean
|
||||
get() = false
|
||||
|
||||
val isEnum: Boolean
|
||||
get() = false
|
||||
}
|
||||
+36
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
/**
|
||||
* An [IJavaIndexable] model which is shared between other models.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface ISharedJavaIndexable : IJavaIndexable {
|
||||
|
||||
/**
|
||||
* A unique ID for the indexable.
|
||||
*/
|
||||
var id: Int?
|
||||
|
||||
/**
|
||||
* Compute and update the ID of this indexable.
|
||||
*/
|
||||
fun computeId()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import com.itsaky.androidide.indexing.IIndex
|
||||
import com.itsaky.androidide.indexing.IIndexer
|
||||
import com.itsaky.androidide.indexing.IIndexer.Params
|
||||
import com.itsaky.androidide.tasks.cancelIfActive
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JavaIndexer(
|
||||
private val jarPaths: List<File>
|
||||
) : IIndexer<IJavaIndexable, Unit> {
|
||||
|
||||
companion object {
|
||||
const val INDEX_PATH = "/java"
|
||||
}
|
||||
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
override val indexPath: String = INDEX_PATH
|
||||
|
||||
override fun index(index: IIndex<IJavaIndexable>, params: Params): Collection<Deferred<Unit>> {
|
||||
return jarPaths.map { jar ->
|
||||
coroutineScope.async {
|
||||
val worker = JavaJarModelBuilder(jar)
|
||||
// worker.index(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
coroutineScope.cancelIfActive("Cancellation requested")
|
||||
}
|
||||
}
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationElement
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.AnnotationElement.Companion.newAnnotationElement
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.ArrayAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.ClassAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.EnumAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.IAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaAnnotation
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaClass
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaConstant
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaEnum
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaField
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaInterface
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaMethod
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.JavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.classfile.PrimitiveAnnotationElementValue
|
||||
import com.itsaky.androidide.lsp.java.utils.DescriptorUtils
|
||||
import io.realm.RealmAny
|
||||
import io.realm.RealmDictionary
|
||||
import io.realm.RealmList
|
||||
import openjdk.tools.classfile.AccessFlags
|
||||
import openjdk.tools.classfile.Annotation.Annotation_element_value
|
||||
import openjdk.tools.classfile.Annotation.Array_element_value
|
||||
import openjdk.tools.classfile.Annotation.Class_element_value
|
||||
import openjdk.tools.classfile.Annotation.Enum_element_value
|
||||
import openjdk.tools.classfile.Annotation.Primitive_element_value
|
||||
import openjdk.tools.classfile.Annotation.element_value
|
||||
import openjdk.tools.classfile.AnnotationDefault_attribute
|
||||
import openjdk.tools.classfile.Attribute
|
||||
import openjdk.tools.classfile.ClassFile
|
||||
import openjdk.tools.classfile.ConstantPool
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_Double_info
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_Float_info
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_Integer_info
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_Long_info
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_String_info
|
||||
import openjdk.tools.classfile.ConstantPool.CONSTANT_Utf8_info
|
||||
import openjdk.tools.classfile.ConstantPool.CPInfo
|
||||
import openjdk.tools.classfile.ConstantValue_attribute
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/**
|
||||
* [JavaJarModelBuilder] builds models for Java classes in a JAR file.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JavaJarModelBuilder(private val jar: File) {
|
||||
|
||||
// for testing purposes
|
||||
// prefer using consumeTypes(Function)
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
fun buildTypes(): List<IJavaType<*, *>> {
|
||||
val types = mutableListOf<IJavaType<*, *>>()
|
||||
consumeTypes { types.add(it) }
|
||||
return types
|
||||
}
|
||||
|
||||
fun consumeTypes(consumer: (IJavaType<*, *>) -> Unit) {
|
||||
ZipFile(jar).use { jar ->
|
||||
jar.entries().asSequence().filter { it.name.endsWith(".class") }.forEach { entry ->
|
||||
jar.getInputStream(entry).buffered().use { inputStream ->
|
||||
val classFile = ClassFile.read(inputStream)!!
|
||||
|
||||
// build model only if the class is not an anonymous or local class
|
||||
if (classFile.getAttribute(Attribute.EnclosingMethod) == null) {
|
||||
val type = buildType(classFile)
|
||||
consumer(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildType(classFile: ClassFile): IJavaType<*, *> {
|
||||
val classInfo = classFile.constant_pool.getClassInfo(classFile.this_class)
|
||||
val fqn = classInfo.baseName
|
||||
val (pck, name, isInner) = splitFqn(fqn)
|
||||
return buildType(fqn, pck, name, isInner, classFile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the given fully qualified name of a class and return the package name and the simple name
|
||||
* of the class, along with whether the class is an inner class.
|
||||
*/
|
||||
private fun splitFqn(fqn: String): Triple<String, String, Boolean> {
|
||||
val dolIdx = fqn.lastIndexOf('$')
|
||||
|
||||
// start looking for '/' from the index of the last '$'
|
||||
// a '/' will always appear before a '$'
|
||||
val slaIdx = fqn.lastIndexOf('/', startIndex = if (dolIdx == -1) fqn.lastIndex else dolIdx - 1)
|
||||
|
||||
if (slaIdx == -1 && dolIdx != -1) {
|
||||
// fqn == SomeClass$InnerClass (inner class of a class in the root package)
|
||||
// pck is empty, but fqn is not the class's simple name
|
||||
return Triple("", fqn.substring(dolIdx + 1), true)
|
||||
}
|
||||
|
||||
if (slaIdx == -1) {
|
||||
// fqn == SomeClass (class in the root package)
|
||||
// pck is empty and fqn is class's simple name
|
||||
return Triple("", fqn, false)
|
||||
}
|
||||
|
||||
if (dolIdx == -1) {
|
||||
// fqn == com/example/SomeClass
|
||||
return Triple(fqn.substring(0, slaIdx), fqn.substring(slaIdx + 1), false)
|
||||
}
|
||||
|
||||
// fqn == com/example/SomeClass$InnerClass
|
||||
return Triple(fqn.substring(0, slaIdx), fqn.substring(dolIdx + 1), true)
|
||||
}
|
||||
|
||||
private fun buildType(
|
||||
fqn: String,
|
||||
pck: String,
|
||||
name: String,
|
||||
isInner: Boolean,
|
||||
classFile: ClassFile
|
||||
): IJavaType<*, *> {
|
||||
return when {
|
||||
classFile.access_flags.`is`(AccessFlags.ACC_ENUM) -> buildEnum(fqn, name, pck, classFile)
|
||||
classFile.access_flags.`is`(AccessFlags.ACC_ANNOTATION) -> buildAnnotation(
|
||||
fqn,
|
||||
name,
|
||||
pck,
|
||||
classFile
|
||||
)
|
||||
|
||||
classFile.access_flags.`is`(AccessFlags.ACC_INTERFACE) -> buildInterface(
|
||||
fqn,
|
||||
name,
|
||||
pck,
|
||||
classFile
|
||||
)
|
||||
|
||||
else -> buildClass(fqn, name, pck, classFile)
|
||||
}.also {
|
||||
it.isInner = isInner
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildClass(fqn: String, name: String, pck: String, classFile: ClassFile): JavaClass {
|
||||
return JavaClass.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
|
||||
setSuperClass(classFile)
|
||||
setSuperInterfaces(classFile)
|
||||
setFields(classFile)
|
||||
setMethods(classFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildInterface(
|
||||
fqn: String, name: String, pck: String, classFile: ClassFile
|
||||
): JavaInterface {
|
||||
return JavaInterface.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
|
||||
setSuperClass(classFile)
|
||||
setSuperInterfaces(classFile)
|
||||
setFields(classFile)
|
||||
setMethods(classFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAnnotation(
|
||||
fqn: String, name: String, pck: String, classFile: ClassFile
|
||||
): JavaAnnotation {
|
||||
return JavaAnnotation.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
|
||||
setSuperClass(classFile)
|
||||
setSuperInterfaces(classFile)
|
||||
setFields(classFile)
|
||||
setAnnotationElements(classFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildEnum(fqn: String, name: String, pck: String, classFile: ClassFile): JavaEnum {
|
||||
return JavaEnum.newInstance(fqn, name, pck, classFile.access_flags.flags).apply {
|
||||
setSuperClass(classFile)
|
||||
setSuperInterfaces(classFile)
|
||||
setFields(classFile)
|
||||
setMethods(classFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun IJavaType<*, *>.setSuperInterfaces(classFile: ClassFile) {
|
||||
val interfaces = RealmList<String>()
|
||||
for (iface in classFile.interfaces) {
|
||||
val ifaceInfo = classFile.constant_pool.getClassInfo(iface)
|
||||
interfaces.add(ifaceInfo.baseName!!)
|
||||
}
|
||||
|
||||
superInterfacesFqn = interfaces
|
||||
}
|
||||
|
||||
private fun IJavaType<*, *>.setSuperClass(classFile: ClassFile) {
|
||||
if (classFile.super_class == 0) {
|
||||
// From https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.5
|
||||
// If the value of the super_class field is 0, then this class represents the java/lang/Object class itself
|
||||
// and hence, doesn't have a super class.
|
||||
return
|
||||
}
|
||||
|
||||
val superclassInfo = classFile.constant_pool.getClassInfo(classFile.super_class)
|
||||
superClassFqn = superclassInfo.baseName!!
|
||||
}
|
||||
|
||||
private fun IJavaType<*, JavaMethod>.setMethods(classFile: ClassFile) {
|
||||
methods = RealmList<JavaMethod>().apply {
|
||||
classFile.methods.forEach { method ->
|
||||
val descriptor = classFile.constant_pool.getUTF8Value(method.descriptor.index)
|
||||
val methodName = method.getName(classFile.constant_pool)
|
||||
val methodType = DescriptorUtils.returnType(descriptor)
|
||||
val paramTypes = RealmList<JavaType>().apply {
|
||||
addAll(DescriptorUtils.paramTypes(descriptor))
|
||||
}
|
||||
|
||||
add(JavaMethod.newInstance(methodName, paramTypes, methodType, method.access_flags.flags))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun IJavaType<JavaField, *>.setFields(classFile: ClassFile) {
|
||||
fields = RealmList<JavaField>().apply {
|
||||
classFile.fields.forEach { field ->
|
||||
val descriptor = classFile.constant_pool.getUTF8Value(field.descriptor.index)
|
||||
val fieldName = field.getName(classFile.constant_pool)
|
||||
val fieldType = DescriptorUtils.type(descriptor)
|
||||
|
||||
add(JavaField.newField(fieldName, fieldType, field.access_flags.flags).apply {
|
||||
field.attributes.get(Attribute.ConstantValue)?.also { constAttr ->
|
||||
val constantValue = constAttr as ConstantValue_attribute
|
||||
val constant = classFile.constant_pool.get(constantValue.constantvalue_index)
|
||||
val isStringType = fieldType.isStringType()
|
||||
val javaConstant = if (fieldType.isPrimitive() || isStringType) {
|
||||
toJavaConstant(fieldType.kind, constant, if (isStringType) 's' else null)
|
||||
} else {
|
||||
throw IllegalArgumentException("Invalid field type for constant value: $fieldType")
|
||||
}
|
||||
|
||||
this.constantValue = javaConstant
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun JavaAnnotation.setAnnotationElements(classFile: ClassFile) {
|
||||
methods = RealmList<AnnotationElement>().apply {
|
||||
classFile.methods.forEach { method ->
|
||||
val descriptor = classFile.constant_pool.getUTF8Value(method.descriptor.index)
|
||||
val methodName = method.getName(classFile.constant_pool)
|
||||
val methodType = DescriptorUtils.returnType(descriptor)
|
||||
|
||||
add(this@setAnnotationElements.newAnnotationElement(
|
||||
methodName, methodType, method.access_flags.flags
|
||||
).apply {
|
||||
val annotationDefault =
|
||||
method.attributes.get(Attribute.AnnotationDefault) as? AnnotationDefault_attribute?
|
||||
if (annotationDefault != null) {
|
||||
val defaultValue = annotationDefault.default_value
|
||||
val value = toAnnotationElementValue(defaultValue, classFile)
|
||||
this.defaultValue = value.let(RealmAny::valueOf) ?: RealmAny.nullValue()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toAnnotationElementValue(
|
||||
defaultValue: element_value, classFile: ClassFile
|
||||
): IAnnotationElementValue {
|
||||
val char = defaultValue.tag.toChar()
|
||||
var kind = JavaType.kindForType(char)
|
||||
if (char == 's') {
|
||||
kind = JavaType.KIND_REF
|
||||
}
|
||||
|
||||
var value: IAnnotationElementValue? = when (kind) {
|
||||
JavaType.KIND_BOOLEAN,
|
||||
JavaType.KIND_BYTE,
|
||||
JavaType.KIND_CHAR,
|
||||
JavaType.KIND_SHORT,
|
||||
JavaType.KIND_INT,
|
||||
JavaType.KIND_LONG,
|
||||
JavaType.KIND_FLOAT,
|
||||
JavaType.KIND_DOUBLE -> PrimitiveAnnotationElementValue.newInstance(
|
||||
// annotation value kinds are same as JavaType.KIND_*
|
||||
kind = kind,
|
||||
value = toPrimitiveConstant(
|
||||
classFile.constant_pool, kind, char, defaultValue as Primitive_element_value
|
||||
)
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = when (char) {
|
||||
's' -> PrimitiveAnnotationElementValue.newInstance(
|
||||
kind = IAnnotationElementValue.KIND_STRING,
|
||||
value = toPrimitiveConstant(
|
||||
classFile.constant_pool,
|
||||
JavaType.KIND_REF,
|
||||
char,
|
||||
defaultValue as Primitive_element_value
|
||||
)
|
||||
)
|
||||
|
||||
'e' -> {
|
||||
val enum = defaultValue as Enum_element_value
|
||||
EnumAnnotationElementValue.newInstance(
|
||||
name = classFile.constant_pool.getUTF8Value(enum.const_name_index),
|
||||
type = DescriptorUtils.type(classFile.constant_pool.getUTF8Value(enum.type_name_index))
|
||||
)
|
||||
}
|
||||
|
||||
'c' -> {
|
||||
// even though the name is given as "class_info_index", the constant at that index
|
||||
// is actually a CONSTANT_Utf8_info representing a return type descriptor (JLS $4.3.3)
|
||||
// this has been clearly mentioned in the Java Language Specification
|
||||
ClassAnnotationElementValue.newInstance(
|
||||
DescriptorUtils.type(
|
||||
classFile.constant_pool.getUTF8Value(
|
||||
(defaultValue as Class_element_value).class_info_index
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
'@' -> {
|
||||
val annotation = (defaultValue as Annotation_element_value).annotation_value
|
||||
AnnotationAnnotationElementValue.newInstance(type = DescriptorUtils.type(
|
||||
classFile.constant_pool.getUTF8Value(
|
||||
annotation.type_index
|
||||
)
|
||||
),
|
||||
values = RealmDictionary<RealmAny>().apply {
|
||||
for (pair in annotation.element_value_pairs) {
|
||||
this[classFile.constant_pool.getUTF8Value(pair.element_name_index)] =
|
||||
toAnnotationElementValue(pair.value, classFile).let { elementValue ->
|
||||
RealmAny.valueOf(elementValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
'[' -> {
|
||||
ArrayAnnotationElementValue.newInstance(values = RealmList<RealmAny>().apply {
|
||||
(defaultValue as Array_element_value).values.forEach { element ->
|
||||
add(toAnnotationElementValue(element, classFile).let { elementValue ->
|
||||
RealmAny.valueOf(elementValue)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Unknown value tag: ${defaultValue.tag}")
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun toPrimitiveConstant(
|
||||
constantPool: ConstantPool, kind: Int, char: Char, prim: Primitive_element_value
|
||||
): JavaConstant {
|
||||
if (kind == JavaType.KIND_REF && char != 's') {
|
||||
throw IllegalArgumentException("Unknown value tag: $char")
|
||||
}
|
||||
|
||||
val constant = constantPool.get(prim.const_value_index)
|
||||
return toJavaConstant(kind, constant, char)
|
||||
}
|
||||
|
||||
private fun toJavaConstant(
|
||||
kind: Int, constant: CPInfo, char: Char?
|
||||
): JavaConstant {
|
||||
val realmAny = when (kind) {
|
||||
JavaType.KIND_BOOLEAN,
|
||||
JavaType.KIND_BYTE,
|
||||
JavaType.KIND_CHAR,
|
||||
JavaType.KIND_SHORT,
|
||||
JavaType.KIND_INT -> (constant as CONSTANT_Integer_info).value.let {
|
||||
RealmAny.valueOf(it)
|
||||
}
|
||||
|
||||
JavaType.KIND_LONG -> (constant as CONSTANT_Long_info).value.let { RealmAny.valueOf(it) }
|
||||
JavaType.KIND_FLOAT -> (constant as CONSTANT_Float_info).value.let { RealmAny.valueOf(it) }
|
||||
JavaType.KIND_DOUBLE -> (constant as CONSTANT_Double_info).value.let { RealmAny.valueOf(it) }
|
||||
else -> if (char == 's') {
|
||||
if (constant is CONSTANT_String_info) {
|
||||
constant.string
|
||||
} else {
|
||||
(constant as CONSTANT_Utf8_info).value
|
||||
}.let { RealmAny.valueOf(it) }
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown value tag: $char")
|
||||
}
|
||||
}
|
||||
|
||||
return JavaConstant.newInstance(
|
||||
kind = kind,
|
||||
value = realmAny
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.apiinfo
|
||||
|
||||
import com.google.common.base.Objects
|
||||
import com.itsaky.androidide.lsp.java.indexing.ICloneable
|
||||
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* @property since The API in which the symbol was added.
|
||||
* @property deprecatedIn The API in which the symbol was deprecated.
|
||||
* @property removedIn The API in which the symbol was removed.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass
|
||||
open class ApiInfo : ISharedJavaIndexable, ICloneable {
|
||||
|
||||
@Required
|
||||
@PrimaryKey
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("since")
|
||||
var since: Int = 1
|
||||
private set
|
||||
|
||||
@RealmField("deprecatedIn")
|
||||
var deprecatedIn: Int = 0
|
||||
private set
|
||||
|
||||
@RealmField("removedIn")
|
||||
var removedIn: Int = 0
|
||||
private set
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.since, this.removedIn, this.deprecatedIn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the API information from the given [ApiInfo].
|
||||
*/
|
||||
fun update(apiInfo: ApiInfo) = apply {
|
||||
this.since = apiInfo.since
|
||||
this.deprecatedIn = apiInfo.deprecatedIn
|
||||
this.removedIn = apiInfo.removedIn
|
||||
this.computeId()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the API information with the given parameters.
|
||||
*/
|
||||
fun update(
|
||||
since: Int = this.since,
|
||||
deprecatedIn: Int = this.deprecatedIn,
|
||||
removedIn: Int = this.removedIn
|
||||
) = apply {
|
||||
this.since = since
|
||||
this.deprecatedIn = deprecatedIn
|
||||
this.removedIn = removedIn
|
||||
this.computeId()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ApiInfo) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (since != other.since) return false
|
||||
if (deprecatedIn != other.deprecatedIn) return false
|
||||
if (removedIn != other.removedIn) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + since
|
||||
result = 31 * result + deprecatedIn
|
||||
result = 31 * result + removedIn
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ApiInfo(id=$id, since=$since, deprecatedIn=$deprecatedIn, removedIn=$removedIn)"
|
||||
}
|
||||
|
||||
override fun clone(): ApiInfo {
|
||||
return newInstance(since, deprecatedIn, removedIn)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(since: Int, deprecatedIn: Int, removedIn: Int): ApiInfo {
|
||||
return ApiInfo().apply {
|
||||
this.since = since
|
||||
this.deprecatedIn = deprecatedIn
|
||||
this.removedIn = removedIn
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.apiinfo
|
||||
|
||||
import androidx.collection.mutableIntObjectMapOf
|
||||
import jaxp.xml.namespace.QName
|
||||
import jaxp.xml.stream.XMLInputFactory
|
||||
import jaxp.xml.stream.events.Attribute
|
||||
import jaxp.xml.stream.events.EndElement
|
||||
import jaxp.xml.stream.events.StartElement
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.InputStream
|
||||
|
||||
/**
|
||||
* Parser for parsing `api-versions.xml` file from the Android SDK and building [ApiInfo] models.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class ApiInfoParser {
|
||||
|
||||
private val apiInfos = mutableIntObjectMapOf<ApiInfo>()
|
||||
private val apiInfo = HashMap<String, Pair<ApiInfo, HashMap<String, ApiInfo>>>()
|
||||
|
||||
private var apiVersion: Int? = null
|
||||
private var currentClass: String? = null
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(ApiInfoParser::class.java)
|
||||
}
|
||||
|
||||
fun parse(apiVersionsXml: InputStream) {
|
||||
if (apiVersionsXml.available() <= 0) {
|
||||
log.warn("api-versions.xml InputStream is empty")
|
||||
return
|
||||
}
|
||||
|
||||
val inputFactory = XMLInputFactory.newInstance()
|
||||
val reader = inputFactory.createXMLEventReader(apiVersionsXml)
|
||||
|
||||
while (reader.hasNext()) {
|
||||
val event = reader.nextEvent()
|
||||
if (event.isStartElement) {
|
||||
consumeStartElement(event.asStartElement())
|
||||
continue
|
||||
}
|
||||
|
||||
if (event.isEndElement) {
|
||||
consumeEndElement(event.asEndElement())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ApiInfo] for the given class.
|
||||
*/
|
||||
fun getClassInfo(className: String): ApiInfo? {
|
||||
return apiInfo[className]?.first
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [ApiInfo] for the given class and member.
|
||||
*/
|
||||
fun getMemberInfo(className: String, memberName: String): ApiInfo? {
|
||||
return apiInfo[className]?.second?.get(memberName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the [ApiInfo] for the given class and member. This also removes the [ApiInfo]
|
||||
* for the class if the all the members of the class have been removed.
|
||||
*/
|
||||
fun removeMemberInfo(className: String, memberName: String): ApiInfo? {
|
||||
return apiInfo[className]?.second?.remove(memberName).also {
|
||||
if (apiInfo[className]?.second?.isEmpty() == true) {
|
||||
apiInfo.remove(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeStartElement(event: StartElement) {
|
||||
when (event.name.localPart) {
|
||||
"api" -> apiVersion = event.getAttributeByName(QName("version")).value.toInt()
|
||||
"class" -> consumeClass(event)
|
||||
"field" -> consumeMember(event, "field")
|
||||
"method" -> consumeMember(event, "method")
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeClass(event: StartElement) {
|
||||
checkNotNull(apiVersion) {
|
||||
"<class> element must be inside <api> element"
|
||||
}
|
||||
|
||||
check(currentClass == null) {
|
||||
"<class> elements cannot be nested"
|
||||
}
|
||||
|
||||
val (name, versions) = event.parseAttrs()
|
||||
check(!apiInfo.containsKey(name)) {
|
||||
"Duplicate class entry: $name"
|
||||
}
|
||||
|
||||
val apiInfo = apiInfos.getOrPut(versions) {
|
||||
createApiInfo(versions)
|
||||
}
|
||||
|
||||
this.apiInfo[name] = apiInfo to HashMap()
|
||||
this.currentClass = name
|
||||
}
|
||||
|
||||
private fun consumeMember(event: StartElement, memberType: String) {
|
||||
val (name, versions) = event.parseAttrs()
|
||||
val currentClass = checkNotNull(currentClass) {
|
||||
"<${memberType}> element must be inside <class> element"
|
||||
}
|
||||
check(!apiInfo.containsKey(name)) {
|
||||
"Duplicate $memberType entry in class $currentClass: $name"
|
||||
}
|
||||
|
||||
val (_, members) = apiInfo[currentClass]!!
|
||||
val existing = members.put(name, createApiInfo(versions))
|
||||
check(existing == null) {
|
||||
"Duplicate $memberType entry in class $currentClass: $name"
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeEndElement(element: EndElement) {
|
||||
when (element.name.localPart) {
|
||||
"api" -> apiVersion = null
|
||||
"class" -> currentClass = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun StartElement.parseAttrs(): Pair<String, Int> {
|
||||
var name: String? = null
|
||||
var since = 1
|
||||
var deprecated = 0
|
||||
var removed = 0
|
||||
|
||||
attributes.forEach { attribute ->
|
||||
attribute as Attribute
|
||||
|
||||
when (attribute.name.localPart) {
|
||||
"name" -> name = attribute.value
|
||||
"since" -> since = attribute.value.toInt()
|
||||
"deprecated" -> deprecated = attribute.value.toInt()
|
||||
"removed" -> removed = attribute.value.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
checkNotNull(name) {
|
||||
"Missing name attribute"
|
||||
}
|
||||
|
||||
// Android API versions would not exceed 255, right?
|
||||
check(since in 1..255 && deprecated in 0..255 && removed in 0..255) {
|
||||
"Invalid version: $since, $deprecated, $removed"
|
||||
}
|
||||
|
||||
val version = (since shl 16) + (deprecated shl 8) + removed
|
||||
return name!! to version
|
||||
}
|
||||
|
||||
private fun createApiInfo(versions: Int): ApiInfo {
|
||||
val since = (versions shr 16) and 0x000000FF
|
||||
val deprecated = (versions shr 8) and 0x000000FF
|
||||
val removed = versions and 0x000000FF
|
||||
return ApiInfo.newInstance(since = since, deprecatedIn = deprecated, removedIn = removed)
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmAny
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A method defined in an annotation type.
|
||||
*
|
||||
* @property defaultValue The default value of the annotation element.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RealmClass(embedded = true)
|
||||
open class AnnotationElement : IJavaSymbol {
|
||||
|
||||
@Index
|
||||
@Required
|
||||
@RealmField("name")
|
||||
var name: String? = null
|
||||
|
||||
@RealmField("type")
|
||||
var type: JavaType? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@LinkingObjects("methods")
|
||||
val ofClass: RealmResults<JavaAnnotation>? = null
|
||||
|
||||
/**
|
||||
* The default value of the annotation element. One of the following:
|
||||
* - [PrimitiveAnnotationElementValue]
|
||||
* - [EnumAnnotationElementValue]
|
||||
* - [ArrayAnnotationElementValue]
|
||||
* - [ClassAnnotationElementValue]
|
||||
* - [AnnotationAnnotationElementValue]
|
||||
*/
|
||||
@RealmField("defaultValue")
|
||||
var defaultValue: RealmAny? = null
|
||||
|
||||
/**
|
||||
* Returns the signature of the method.
|
||||
*/
|
||||
fun signature(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append(name)
|
||||
sb.append("(")
|
||||
sb.append(")")
|
||||
sb.append(type?.internalForm())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun JavaAnnotation.newAnnotationElement(
|
||||
name: String,
|
||||
type: JavaType,
|
||||
accessFlags: Int,
|
||||
apiInfo: ApiInfo? = null,
|
||||
defaultValue: RealmAny? = null
|
||||
): AnnotationElement {
|
||||
return AnnotationElement().apply {
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.defaultValue = defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A Java annotation type.
|
||||
*
|
||||
* Implementation note:
|
||||
* An annotation type is a Java interface, and hence, it inherits from [JavaInterface] instead of
|
||||
* [IJavaType]. Also, [IJavaType.isInterface] and [IJavaType.isAnnotation] both
|
||||
* return `true` for annotation types.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass
|
||||
open class JavaAnnotation : IJavaType<JavaField, AnnotationElement> {
|
||||
|
||||
@Index
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("fqn")
|
||||
override var fqn: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("name")
|
||||
override var name: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("pck")
|
||||
override var packageName: String? = null
|
||||
|
||||
@RealmField("isInner")
|
||||
override var isInner: Boolean = false
|
||||
|
||||
@RealmField("superClassFqn")
|
||||
override var superClassFqn: String? = null
|
||||
|
||||
@RealmField("superInterfacesFqn")
|
||||
override var superInterfacesFqn: RealmList<String>? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@RealmField("fields")
|
||||
override var fields: RealmList<JavaField>? = null
|
||||
|
||||
@RealmField("methods")
|
||||
override var methods: RealmList<AnnotationElement>? = null
|
||||
|
||||
override val isInterface: Boolean
|
||||
get() = true
|
||||
|
||||
override val isAnnotation: Boolean
|
||||
get() = true
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
fqn: String,
|
||||
name: String,
|
||||
packageName: String,
|
||||
accessFlags: Int,
|
||||
isInner: Boolean = false,
|
||||
superInterfacesFqn: RealmList<String>? = null,
|
||||
apiInfo: ApiInfo? = null,
|
||||
fields: RealmList<JavaField>? = null,
|
||||
methods: RealmList<AnnotationElement>? = null
|
||||
): JavaAnnotation {
|
||||
return JavaAnnotation().apply {
|
||||
this.fqn = fqn
|
||||
this.name = name
|
||||
this.packageName = packageName
|
||||
this.isInner = isInner
|
||||
this.superClassFqn = superClassFqn
|
||||
this.superInterfacesFqn = superInterfacesFqn
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.fields = fields
|
||||
this.methods = methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A Java class type.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass
|
||||
open class JavaClass : IJavaType<JavaField, JavaMethod> {
|
||||
|
||||
@Index
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("fqn")
|
||||
override var fqn: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("name")
|
||||
override var name: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("pck")
|
||||
override var packageName: String? = null
|
||||
|
||||
@RealmField("isInner")
|
||||
override var isInner: Boolean = false
|
||||
|
||||
@RealmField("superClassFqn")
|
||||
override var superClassFqn: String? = null
|
||||
|
||||
@RealmField("superInterfacesFqn")
|
||||
override var superInterfacesFqn: RealmList<String>? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@RealmField("fields")
|
||||
override var fields: RealmList<JavaField>? = null
|
||||
|
||||
@RealmField("methods")
|
||||
override var methods: RealmList<JavaMethod>? = null
|
||||
|
||||
override val isClass: Boolean
|
||||
get() = true
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
fqn: String,
|
||||
name: String,
|
||||
packageName: String,
|
||||
accessFlags: Int,
|
||||
isInner: Boolean = false,
|
||||
superClassFqn: String? = null,
|
||||
superInterfacesFqn: RealmList<String>? = null,
|
||||
apiInfo: ApiInfo? = null,
|
||||
fields: RealmList<JavaField>? = null,
|
||||
methods: RealmList<JavaMethod>? = null
|
||||
): JavaClass {
|
||||
return JavaClass().apply {
|
||||
this.fqn = fqn
|
||||
this.name = name
|
||||
this.packageName = packageName
|
||||
this.isInner = isInner
|
||||
this.superClassFqn = superClassFqn
|
||||
this.superInterfacesFqn = superInterfacesFqn
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.fields = fields
|
||||
this.methods = methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.google.common.base.Objects
|
||||
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
|
||||
import io.realm.RealmAny
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
|
||||
/**
|
||||
* A constant value in Java.
|
||||
*
|
||||
* A unique ID is used as the primary key in order to ensure that a constant is not stored multiple
|
||||
* times in the database. Also, a string constant always has type [JavaType.KIND_REF]. A constant
|
||||
* is never of type [JavaType.KIND_VOID].
|
||||
*
|
||||
* @property id A unique ID which is generated for the constant.
|
||||
* @property kind The kind of contant. See [JavaType] for a list of kinds.
|
||||
* @property value The value of the constant. Cast this value based on the value of [kind].
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RealmClass
|
||||
open class JavaConstant : ISharedJavaIndexable {
|
||||
|
||||
@PrimaryKey
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
var kind: Int = JavaType.KIND_UNKNOWN
|
||||
private set
|
||||
|
||||
@RealmField("value")
|
||||
var value: RealmAny? = null
|
||||
private set
|
||||
|
||||
companion object {
|
||||
fun newInstance(kind: Int, value: RealmAny): JavaConstant {
|
||||
return JavaConstant().apply {
|
||||
this.kind = kind
|
||||
this.value = value
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun update(kind: Int = this.kind, value: RealmAny? = this.value) {
|
||||
this.kind = kind
|
||||
this.value = value
|
||||
this.computeId()
|
||||
}
|
||||
|
||||
fun update(src: JavaConstant) {
|
||||
this.kind = src.kind
|
||||
this.value = src.value
|
||||
this.computeId()
|
||||
}
|
||||
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.kind, this.value)
|
||||
}
|
||||
|
||||
// boolean, byte, short, int are stored as int in the bytecode
|
||||
|
||||
fun asBoolean(): Boolean? {
|
||||
return safeCast(JavaType.KIND_BOOLEAN) { asInteger() == 1 }
|
||||
}
|
||||
|
||||
fun asByte(): Byte? {
|
||||
return safeCast(JavaType.KIND_BYTE) { asInteger()?.toByte() }
|
||||
}
|
||||
|
||||
fun asShort(): Short? {
|
||||
return safeCast(JavaType.KIND_SHORT) { asInteger()?.toShort() }
|
||||
}
|
||||
|
||||
fun asInt(): Int? {
|
||||
return safeCast(JavaType.KIND_INT) { asInteger() }
|
||||
}
|
||||
|
||||
fun asLong(): Long? {
|
||||
return safeCast(JavaType.KIND_LONG) { asLong() }
|
||||
}
|
||||
|
||||
fun asFloat(): Float? {
|
||||
return safeCast(JavaType.KIND_FLOAT) { asFloat() }
|
||||
}
|
||||
|
||||
fun asDouble(): Double? {
|
||||
return safeCast(JavaType.KIND_DOUBLE) { asDouble() }
|
||||
}
|
||||
|
||||
fun asString(): String? {
|
||||
return safeCast(JavaType.KIND_REF) { asString() }
|
||||
}
|
||||
|
||||
fun asChar(): Char? {
|
||||
return safeCast(JavaType.KIND_CHAR) { asInteger()?.toChar() }
|
||||
}
|
||||
|
||||
private inline fun <R> safeCast(kind: Int, action: RealmAny.() -> R): R? {
|
||||
return value?.let {
|
||||
if (this.kind == kind) {
|
||||
try {
|
||||
it.action()
|
||||
} catch (ex: ClassCastException) {
|
||||
throw IllegalStateException("Kind == $kind but safeCast failed", ex)
|
||||
}
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is JavaConstant) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (value != other.value) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + (value?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "JavaConstant(id=$id, kind=$kind, value=$value)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A Java enum type.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RealmClass
|
||||
open class JavaEnum : IJavaType<JavaField, JavaMethod> {
|
||||
|
||||
@Index
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("fqn")
|
||||
override var fqn: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("name")
|
||||
override var name: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("pck")
|
||||
override var packageName: String? = null
|
||||
|
||||
@RealmField("isInner")
|
||||
override var isInner: Boolean = false
|
||||
|
||||
@RealmField("superClassFqn")
|
||||
override var superClassFqn: String? = null
|
||||
|
||||
@RealmField("superInterfacesFqn")
|
||||
override var superInterfacesFqn: RealmList<String>? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@RealmField("fields")
|
||||
override var fields: RealmList<JavaField>? = null
|
||||
|
||||
@RealmField("methods")
|
||||
override var methods: RealmList<JavaMethod>? = null
|
||||
|
||||
override val isEnum: Boolean
|
||||
get() = true
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
fqn: String,
|
||||
name: String,
|
||||
packageName: String,
|
||||
accessFlags: Int,
|
||||
isInner: Boolean = false,
|
||||
superClassFqn: String? = null,
|
||||
superInterfacesFqn: RealmList<String>? = null,
|
||||
apiInfo: ApiInfo? = null,
|
||||
fields: RealmList<JavaField>? = null,
|
||||
methods: RealmList<JavaMethod>? = null
|
||||
): JavaEnum {
|
||||
return JavaEnum().apply {
|
||||
this.fqn = fqn
|
||||
this.name = name
|
||||
this.packageName = packageName
|
||||
this.isInner = isInner
|
||||
this.superClassFqn = superClassFqn
|
||||
this.superInterfacesFqn = superInterfacesFqn
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.fields = fields
|
||||
this.methods = methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RealmClass(embedded = true)
|
||||
open class JavaField : IJavaSymbol {
|
||||
|
||||
@Index
|
||||
@RealmField("name")
|
||||
var name: String? = null
|
||||
|
||||
@RealmField("type")
|
||||
var type: JavaType? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@RealmField("constantValue")
|
||||
var constantValue: JavaConstant? = null
|
||||
|
||||
@LinkingObjects("fields")
|
||||
val ofClass: RealmResults<JavaClass>? = null
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newField(
|
||||
name: String,
|
||||
type: JavaType,
|
||||
accessFlags: Int,
|
||||
apiInfo: ApiInfo? = null,
|
||||
constantValue: JavaConstant? = null
|
||||
): JavaField {
|
||||
return JavaField().apply {
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.constantValue = constantValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.annotations.RealmModule
|
||||
|
||||
/**
|
||||
* [RealmModule] for the indexing databased for Java.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
@RealmModule(
|
||||
library = true, classes = [
|
||||
AnnotationElement::class,
|
||||
AnnotationAnnotationElementValue::class,
|
||||
ArrayAnnotationElementValue::class,
|
||||
ClassAnnotationElementValue::class,
|
||||
EnumAnnotationElementValue::class,
|
||||
PrimitiveAnnotationElementValue::class,
|
||||
ApiInfo::class,
|
||||
JavaAnnotation::class,
|
||||
JavaClass::class,
|
||||
JavaConstant::class,
|
||||
JavaEnum::class,
|
||||
JavaField::class,
|
||||
JavaInterface::class,
|
||||
JavaMethod::class,
|
||||
JavaType::class,
|
||||
]
|
||||
)
|
||||
internal class JavaIndexingRealmModule
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaType
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* A Java interface type.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass
|
||||
open class JavaInterface : IJavaType<JavaField, JavaMethod> {
|
||||
|
||||
@Index
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("fqn")
|
||||
override var fqn: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("name")
|
||||
override var name: String? = null
|
||||
|
||||
@Required
|
||||
@RealmField("pck")
|
||||
override var packageName: String? = null
|
||||
|
||||
@RealmField("isInner")
|
||||
override var isInner: Boolean = false
|
||||
|
||||
@RealmField("superClassFqn")
|
||||
override var superClassFqn: String? = null
|
||||
|
||||
@RealmField("superInterfacesFqn")
|
||||
override var superInterfacesFqn: RealmList<String>? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@RealmField("fields")
|
||||
override var fields: RealmList<JavaField>? = null
|
||||
|
||||
@RealmField("methods")
|
||||
override var methods: RealmList<JavaMethod>? = null
|
||||
|
||||
override val isInterface: Boolean
|
||||
get() = true
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
fqn: String,
|
||||
name: String,
|
||||
packageName: String,
|
||||
accessFlags: Int,
|
||||
isInner: Boolean = false,
|
||||
superInterfacesFqn: RealmList<String>? = null,
|
||||
apiInfo: ApiInfo? = null,
|
||||
fields: RealmList<JavaField>? = null,
|
||||
methods: RealmList<JavaMethod>? = null
|
||||
): JavaInterface {
|
||||
return JavaInterface().apply {
|
||||
this.fqn = fqn
|
||||
this.name = name
|
||||
this.packageName = packageName
|
||||
this.isInner = isInner
|
||||
this.superInterfacesFqn = superInterfacesFqn
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
this.fields = fields
|
||||
this.methods = methods
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.itsaky.androidide.lsp.java.indexing.IJavaSymbol
|
||||
import com.itsaky.androidide.lsp.java.indexing.apiinfo.ApiInfo
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmResults
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.LinkingObjects
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass(embedded = true)
|
||||
open class JavaMethod : IJavaSymbol {
|
||||
@Index
|
||||
@Required
|
||||
@RealmField("name")
|
||||
var name: String? = null
|
||||
|
||||
@RealmField("paramTypes")
|
||||
var paramsTypes: RealmList<JavaType>? = null
|
||||
|
||||
@RealmField("returnType")
|
||||
var returnType: JavaType? = null
|
||||
|
||||
@RealmField("accessFlags")
|
||||
override var accessFlags: Int = 0
|
||||
|
||||
@RealmField("apiInfo")
|
||||
override var apiInfo: ApiInfo? = null
|
||||
|
||||
@LinkingObjects("methods")
|
||||
val ofClass: RealmResults<JavaClass>? = null
|
||||
|
||||
/**
|
||||
* Returns the signature of the method.
|
||||
*/
|
||||
fun signature(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append(name)
|
||||
sb.append("(")
|
||||
paramsTypes?.forEach {
|
||||
sb.append(it.internalForm())
|
||||
}
|
||||
sb.append(")")
|
||||
sb.append(returnType?.internalForm())
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
name: String,
|
||||
paramsTypes: RealmList<JavaType>,
|
||||
returnType: JavaType,
|
||||
accessFlags: Int,
|
||||
apiInfo: ApiInfo? = null,
|
||||
): JavaMethod {
|
||||
return JavaMethod().apply {
|
||||
this.name = name
|
||||
this.paramsTypes = paramsTypes
|
||||
this.returnType = returnType
|
||||
this.accessFlags = accessFlags
|
||||
this.apiInfo = apiInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.google.common.base.Objects
|
||||
import com.itsaky.androidide.lsp.java.indexing.ICloneable
|
||||
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
|
||||
import io.realm.annotations.Index
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
|
||||
@RealmClass
|
||||
open class JavaType : ISharedJavaIndexable, ICloneable {
|
||||
|
||||
@Required
|
||||
@PrimaryKey
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@Index
|
||||
@Required
|
||||
@RealmField("name")
|
||||
var name: String? = null
|
||||
|
||||
@RealmField("kind")
|
||||
var kind: Int = KIND_UNKNOWN
|
||||
|
||||
@RealmField("arrayDims")
|
||||
var arrayDims: Int = 0
|
||||
|
||||
/**
|
||||
* Returns the internal representation of the type.
|
||||
*/
|
||||
fun internalForm(): String {
|
||||
val sb = StringBuilder()
|
||||
if (arrayDims > 0) {
|
||||
sb.append("[".repeat(arrayDims))
|
||||
}
|
||||
|
||||
if (kind == KIND_REF) {
|
||||
sb.append("L")
|
||||
}
|
||||
|
||||
sb.append(name)
|
||||
|
||||
if (kind == KIND_REF) {
|
||||
sb.append(";")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the type represents a primitive type.
|
||||
*/
|
||||
fun isPrimitive(): Boolean {
|
||||
return when (kind) {
|
||||
KIND_BOOLEAN,
|
||||
KIND_BYTE,
|
||||
KIND_CHAR,
|
||||
KIND_DOUBLE,
|
||||
KIND_FLOAT,
|
||||
KIND_INT,
|
||||
KIND_LONG,
|
||||
KIND_SHORT -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the type represents a void type.
|
||||
*/
|
||||
fun isVoid(): Boolean {
|
||||
return kind == KIND_VOID
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the type represents the `java.lang.String` type.
|
||||
*/
|
||||
fun isStringType(): Boolean {
|
||||
return name == STRING.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the type represents the `java.lang.Object` type.
|
||||
*/
|
||||
fun isObjectType(): Boolean {
|
||||
return name == OBJECT.name
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the type represents the `java.lang.Class` type.
|
||||
*/
|
||||
fun isClassType(): Boolean {
|
||||
return name == CLASS.name
|
||||
}
|
||||
|
||||
override fun clone(): JavaType {
|
||||
return newInstance(name = name, kind = kind, arrayDims = arrayDims)
|
||||
}
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.name, this.arrayDims)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is JavaType) return false
|
||||
|
||||
if (name != other.name) return false
|
||||
if (kind != other.kind) return false
|
||||
if (arrayDims != other.arrayDims) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = name?.hashCode() ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + arrayDims
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "JavaType(id=$id, name=$name, kind=$kind, arrayDims=$arrayDims)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND_UNKNOWN = -1
|
||||
const val KIND_BOOLEAN = 0
|
||||
const val KIND_BYTE = 1
|
||||
const val KIND_CHAR = 2
|
||||
const val KIND_DOUBLE = 3
|
||||
const val KIND_FLOAT = 4
|
||||
const val KIND_INT = 5
|
||||
const val KIND_LONG = 6
|
||||
const val KIND_SHORT = 7
|
||||
const val KIND_VOID = 8
|
||||
const val KIND_REF = 9
|
||||
|
||||
// kind values used in IAnnotationElementValue
|
||||
// defined here for convenience
|
||||
internal const val __KIND_STRING = 10
|
||||
internal const val __KIND_CLASS = 11
|
||||
internal const val __KIND_ENUM = 12
|
||||
internal const val __KIND_ANNOTATION = 13
|
||||
internal const val __KIND_ARRAY = 14
|
||||
|
||||
const val TYPE_BOOLEAN = 'Z'
|
||||
const val TYPE_BYTE = 'B'
|
||||
const val TYPE_CHAR = 'C'
|
||||
const val TYPE_DOUBLE = 'D'
|
||||
const val TYPE_FLOAT = 'F'
|
||||
const val TYPE_INT = 'I'
|
||||
const val TYPE_LONG = 'J'
|
||||
const val TYPE_SHORT = 'S'
|
||||
const val TYPE_VOID = 'V'
|
||||
|
||||
@JvmStatic
|
||||
val BOOLEAN = newInstance(name = TYPE_BOOLEAN, kind = KIND_BOOLEAN)
|
||||
|
||||
@JvmStatic
|
||||
val BYTE = newInstance(name = TYPE_BYTE, kind = KIND_BYTE)
|
||||
|
||||
@JvmStatic
|
||||
val CHAR = newInstance(name = TYPE_CHAR, kind = KIND_CHAR)
|
||||
|
||||
@JvmStatic
|
||||
val DOUBLE = newInstance(name = TYPE_DOUBLE, kind = KIND_DOUBLE)
|
||||
|
||||
@JvmStatic
|
||||
val FLOAT = newInstance(name = TYPE_FLOAT, kind = KIND_FLOAT)
|
||||
|
||||
@JvmStatic
|
||||
val INT = newInstance(name = TYPE_INT, kind = KIND_INT)
|
||||
|
||||
@JvmStatic
|
||||
val LONG = newInstance(name = TYPE_LONG, kind = KIND_LONG)
|
||||
|
||||
@JvmStatic
|
||||
val SHORT = newInstance(name = TYPE_SHORT, kind = KIND_SHORT)
|
||||
|
||||
@JvmStatic
|
||||
val VOID = newInstance(name = TYPE_VOID, kind = KIND_VOID)
|
||||
|
||||
@JvmStatic
|
||||
val STRING = newInstance(name = "java/lang/String", kind = KIND_REF)
|
||||
|
||||
@JvmStatic
|
||||
val OBJECT = newInstance(name = "java/lang/Object", kind = KIND_REF)
|
||||
|
||||
@JvmStatic
|
||||
val CLASS = newInstance(name = "java/lang/Class", kind = KIND_REF)
|
||||
|
||||
@JvmStatic
|
||||
fun kindForType(type: Char) = when (type) {
|
||||
TYPE_BOOLEAN -> KIND_BOOLEAN
|
||||
TYPE_BYTE -> KIND_BYTE
|
||||
TYPE_CHAR -> KIND_CHAR
|
||||
TYPE_DOUBLE -> KIND_DOUBLE
|
||||
TYPE_FLOAT -> KIND_FLOAT
|
||||
TYPE_INT -> KIND_INT
|
||||
TYPE_LONG -> KIND_LONG
|
||||
TYPE_SHORT -> KIND_SHORT
|
||||
TYPE_VOID -> KIND_VOID
|
||||
else -> KIND_UNKNOWN
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun typeForKind(kind: Int) = when (kind) {
|
||||
KIND_BOOLEAN -> TYPE_BOOLEAN
|
||||
KIND_BYTE -> TYPE_BYTE
|
||||
KIND_CHAR -> TYPE_CHAR
|
||||
KIND_DOUBLE -> TYPE_DOUBLE
|
||||
KIND_FLOAT -> TYPE_FLOAT
|
||||
KIND_INT -> TYPE_INT
|
||||
KIND_LONG -> TYPE_LONG
|
||||
KIND_SHORT -> TYPE_SHORT
|
||||
KIND_VOID -> TYPE_VOID
|
||||
else -> TYPE_VOID
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun primitiveFor(char: Char): JavaType? {
|
||||
return when (char) {
|
||||
TYPE_BOOLEAN -> BOOLEAN
|
||||
TYPE_BYTE -> BYTE
|
||||
TYPE_CHAR -> CHAR
|
||||
TYPE_DOUBLE -> DOUBLE
|
||||
TYPE_FLOAT -> FLOAT
|
||||
TYPE_INT -> INT
|
||||
TYPE_LONG -> LONG
|
||||
TYPE_SHORT -> SHORT
|
||||
TYPE_VOID -> VOID
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(name: String?, kind: Int, arrayDims: Int = 0): JavaType {
|
||||
return JavaType().apply {
|
||||
this.name = name
|
||||
this.kind = kind
|
||||
this.arrayDims = arrayDims
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(name: Char, kind: Int, arrayDims: Int = 0): JavaType {
|
||||
return newInstance(name.toString(), kind, arrayDims)
|
||||
}
|
||||
}
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.indexing.classfile
|
||||
|
||||
import com.google.common.base.Objects
|
||||
import com.itsaky.androidide.db.utils.isEqualTo
|
||||
import com.itsaky.androidide.db.utils.hash
|
||||
import com.itsaky.androidide.lsp.java.indexing.ISharedJavaIndexable
|
||||
import io.realm.RealmAny
|
||||
import io.realm.RealmDictionary
|
||||
import io.realm.RealmList
|
||||
import io.realm.annotations.PrimaryKey
|
||||
import io.realm.annotations.RealmClass
|
||||
import io.realm.annotations.RealmField
|
||||
import io.realm.annotations.Required
|
||||
|
||||
sealed interface IAnnotationElementValue : ISharedJavaIndexable {
|
||||
var kind: Int
|
||||
|
||||
companion object {
|
||||
const val KIND_UNKNOWN = JavaType.KIND_UNKNOWN
|
||||
const val KIND_BOOLEAN = JavaType.KIND_BOOLEAN
|
||||
const val KIND_BYTE = JavaType.KIND_BYTE
|
||||
const val KIND_CHAR = JavaType.KIND_CHAR
|
||||
const val KIND_DOUBLE = JavaType.KIND_DOUBLE
|
||||
const val KIND_FLOAT = JavaType.KIND_FLOAT
|
||||
const val KIND_INT = JavaType.KIND_INT
|
||||
const val KIND_LONG = JavaType.KIND_LONG
|
||||
const val KIND_SHORT = JavaType.KIND_SHORT
|
||||
|
||||
const val KIND_STRING = JavaType.__KIND_STRING
|
||||
const val KIND_CLASS = JavaType.__KIND_CLASS
|
||||
const val KIND_ENUM = JavaType.__KIND_ENUM
|
||||
const val KIND_ANNOTATION = JavaType.__KIND_ANNOTATION
|
||||
const val KIND_ARRAY = JavaType.__KIND_ARRAY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RealmClass
|
||||
open class PrimitiveAnnotationElementValue : IAnnotationElementValue {
|
||||
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
override var kind: Int = 0
|
||||
|
||||
@RealmField("value")
|
||||
var value: JavaConstant? = null
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.value)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is PrimitiveAnnotationElementValue) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (value != other.value) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + (value?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "PrimitiveAnnotationElementValue(id=$id, kind=$kind, value=$value)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(kind: Int, value: JavaConstant): PrimitiveAnnotationElementValue {
|
||||
return PrimitiveAnnotationElementValue().apply {
|
||||
this.kind = kind
|
||||
this.value = value
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RealmClass
|
||||
open class EnumAnnotationElementValue : IAnnotationElementValue {
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
override var kind: Int = IAnnotationElementValue.KIND_ENUM
|
||||
|
||||
@RealmField("name")
|
||||
var name: String? = null
|
||||
|
||||
@RealmField("type")
|
||||
var type: JavaType? = null
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.name, this.type)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is EnumAnnotationElementValue) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (name != other.name) return false
|
||||
if (type != other.type) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + (name?.hashCode() ?: 0)
|
||||
result = 31 * result + (type?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "EnumAnnotationElementValue(id=$id, kind=$kind, name=$name, type=$type)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(name: String, type: JavaType): EnumAnnotationElementValue {
|
||||
return EnumAnnotationElementValue().apply {
|
||||
this.name = name
|
||||
this.type = type
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RealmClass
|
||||
open class ArrayAnnotationElementValue : IAnnotationElementValue {
|
||||
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
override var kind: Int = IAnnotationElementValue.KIND_ARRAY
|
||||
|
||||
@RealmField("numValues")
|
||||
var numValues: Int = 0
|
||||
|
||||
/**
|
||||
* The elements of the array element value. Always of type [IAnnotationElementValue].
|
||||
*/
|
||||
@RealmField("values")
|
||||
var values: RealmList<RealmAny>? = null
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.numValues, this.values)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ArrayAnnotationElementValue) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (numValues != other.numValues) return false
|
||||
if (values != other.values) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + numValues
|
||||
result = 31 * result + (values?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ArrayAnnotationElementValue(id=$id, kind=$kind, numValues=$numValues, values=$values)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(values: RealmList<RealmAny>): ArrayAnnotationElementValue {
|
||||
return ArrayAnnotationElementValue().apply {
|
||||
this.numValues = values.size
|
||||
this.values = values
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RealmClass
|
||||
open class ClassAnnotationElementValue : IAnnotationElementValue {
|
||||
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
override var kind: Int = IAnnotationElementValue.KIND_CLASS
|
||||
|
||||
@RealmField("type")
|
||||
var type: JavaType? = null
|
||||
|
||||
override fun computeId() {
|
||||
this.id = Objects.hashCode(this.kind, this.type)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ClassAnnotationElementValue) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (type != other.type) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + (type?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ClassAnnotationElementValue(id=$id, kind=$kind, type=$type)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(type: JavaType): ClassAnnotationElementValue {
|
||||
return ClassAnnotationElementValue().apply {
|
||||
this.type = type
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@RealmClass
|
||||
open class AnnotationAnnotationElementValue : IAnnotationElementValue {
|
||||
|
||||
@PrimaryKey
|
||||
@Required
|
||||
@RealmField("id")
|
||||
override var id: Int? = null
|
||||
|
||||
@RealmField("kind")
|
||||
override var kind: Int = IAnnotationElementValue.KIND_ANNOTATION
|
||||
|
||||
@RealmField("type")
|
||||
var type: JavaType? = null
|
||||
|
||||
/**
|
||||
* The named elements of the annotation element value. Always of type [IAnnotationElementValue].
|
||||
*/
|
||||
@RealmField("values")
|
||||
var values: RealmDictionary<RealmAny>? = null
|
||||
|
||||
override fun computeId() {
|
||||
// RealmDictionary seems to not override hashCode()
|
||||
this.id = Objects.hashCode(this.kind, this.type, this.values?.hash())
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is AnnotationAnnotationElementValue) return false
|
||||
|
||||
if (id != other.id) return false
|
||||
if (kind != other.kind) return false
|
||||
if (type != other.type) return false
|
||||
if (!values.isEqualTo(other)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id ?: 0
|
||||
result = 31 * result + kind
|
||||
result = 31 * result + (type?.hashCode() ?: 0)
|
||||
result = 31 * result + (values?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "AnnotationAnnotationElementValue(id=$id, kind=$kind, type=$type, values=$values)"
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(
|
||||
type: JavaType,
|
||||
values: RealmDictionary<RealmAny>
|
||||
): AnnotationAnnotationElementValue {
|
||||
return AnnotationAnnotationElementValue().apply {
|
||||
this.type = type
|
||||
this.values = values
|
||||
this.computeId()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.models
|
||||
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompilationTaskProcessor
|
||||
import com.itsaky.androidide.lsp.java.compiler.DefaultCompilationTaskProcessor
|
||||
import jdkx.tools.JavaFileObject
|
||||
import openjdk.tools.javac.util.Context
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Data sent to compiler to request a compilation.
|
||||
*
|
||||
* @param sources The source files to compile.
|
||||
* @param partialRequest Data that will be used to a partial reparse.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
data class CompilationRequest
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
@JvmField val sources: Collection<JavaFileObject>,
|
||||
@JvmField val partialRequest: PartialReparseRequest? = null,
|
||||
@JvmField
|
||||
val compilationTaskProcessor: CompilationTaskProcessor = DefaultCompilationTaskProcessor(),
|
||||
@JvmField var configureContext: Consumer<Context>? = null
|
||||
)
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.models
|
||||
|
||||
import com.itsaky.androidide.lsp.edits.IEditHandler
|
||||
import com.itsaky.androidide.lsp.java.edits.BaseJavaEditHandler
|
||||
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.ICompletionData
|
||||
import com.itsaky.androidide.lsp.models.InsertTextFormat
|
||||
import com.itsaky.androidide.lsp.models.MatchLevel
|
||||
import com.itsaky.androidide.lsp.models.TextEdit
|
||||
|
||||
/**
|
||||
* Completion item model for java completion items.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JavaCompletionItem(
|
||||
label: String,
|
||||
detail: String,
|
||||
insertText: String?,
|
||||
insertTextFormat: InsertTextFormat?,
|
||||
sortText: String?,
|
||||
command: Command?,
|
||||
kind: CompletionItemKind,
|
||||
matchLevel: MatchLevel,
|
||||
additionalTextEdits: List<TextEdit>?,
|
||||
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
|
||||
)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Configuration for the java language server.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class JavaServerConfiguration {
|
||||
|
||||
private Set<Path> classPaths;
|
||||
private Set<Path> sourceDirs;
|
||||
|
||||
public JavaServerConfiguration() {
|
||||
this(Collections.emptySet(), Collections.emptySet());
|
||||
}
|
||||
|
||||
public JavaServerConfiguration(Set<Path> classPaths, Set<Path> sourcePaths) {
|
||||
this.classPaths = classPaths;
|
||||
this.sourceDirs = sourcePaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getClassPaths());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JavaServerConfiguration that = (JavaServerConfiguration) o;
|
||||
return Objects.equals(getClassPaths(), that.getClassPaths());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JavaServerConfiguration{" + "classPaths=" + classPaths + '}';
|
||||
}
|
||||
|
||||
public Set<Path> getClassPaths() {
|
||||
return classPaths;
|
||||
}
|
||||
|
||||
public JavaServerConfiguration setClassPaths(Set<Path> classPaths) {
|
||||
this.classPaths = classPaths;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Set<Path> getSourceDirs() {
|
||||
return sourceDirs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.googlejavaformat.java.JavaFormatterOptions;
|
||||
import com.itsaky.androidide.lsp.util.PrefBasedServerSettings;
|
||||
import com.itsaky.androidide.managers.PreferenceManager;
|
||||
import com.itsaky.androidide.preferences.internal.JavaPreferences;
|
||||
import com.itsaky.androidide.utils.VMUtils;
|
||||
|
||||
/**
|
||||
* Server settings for the java language server.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class JavaServerSettings extends PrefBasedServerSettings {
|
||||
|
||||
public static final String KEY_JAVA_PREF_GOOGLE_CODE_STYLE = JavaPreferences.GOOGLE_CODE_STYLE;
|
||||
public static final int CODE_STYLE_AOSP = 0;
|
||||
public static final int CODE_STYLE_GOOGLE = 1;
|
||||
private static JavaServerSettings instance;
|
||||
|
||||
@NonNull
|
||||
public static JavaServerSettings getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new JavaServerSettings();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean diagnosticsEnabled() {
|
||||
return VMUtils.isJvm() || JavaPreferences.INSTANCE.isJavaDiagnosticsEnabled();
|
||||
}
|
||||
|
||||
public JavaFormatterOptions getFormatterOptions() {
|
||||
return JavaFormatterOptions.builder().formatJavadoc(true).style(getStyle()).build();
|
||||
}
|
||||
|
||||
public JavaFormatterOptions.Style getStyle() {
|
||||
if (getCodeStyle() == JavaServerSettings.CODE_STYLE_AOSP) {
|
||||
|
||||
return JavaFormatterOptions.Style.AOSP;
|
||||
}
|
||||
|
||||
return JavaFormatterOptions.Style.GOOGLE;
|
||||
}
|
||||
|
||||
private int getCodeStyle() {
|
||||
final PreferenceManager prefs = getPrefs();
|
||||
if (prefs != null) {
|
||||
if (prefs.getBoolean(KEY_JAVA_PREF_GOOGLE_CODE_STYLE, false)) {
|
||||
return CODE_STYLE_GOOGLE;
|
||||
}
|
||||
}
|
||||
|
||||
return CODE_STYLE_AOSP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.models
|
||||
|
||||
/**
|
||||
* Data provided to the compiler to request a partial reparse if needed.
|
||||
*
|
||||
* @param cursor The position of the cursor (1-based).
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
data class PartialReparseRequest(@JvmField val cursor: Long, @JvmField val contents: String)
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser
|
||||
|
||||
import jdkx.tools.JavaFileObject
|
||||
|
||||
/**
|
||||
* A parser for parsing Java source code.
|
||||
*
|
||||
* @param T The type of result that this parser produces.
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
interface IJavaParser<T> : 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
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser;
|
||||
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import openjdk.source.util.JavacTask;
|
||||
|
||||
public class ParseTask {
|
||||
public final JavacTask task;
|
||||
public final CompilationUnitTree root;
|
||||
|
||||
public ParseTask(JavacTask task, CompilationUnitTree root) {
|
||||
this.task = task;
|
||||
this.root = root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<? extends JavaFileObject> __) {
|
||||
// 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("<init>")) {
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser.ts
|
||||
|
||||
import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent
|
||||
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
|
||||
import com.itsaky.androidide.lsp.java.parser.IJavaParser
|
||||
import com.itsaky.androidide.treesitter.TSParser
|
||||
import com.itsaky.androidide.treesitter.java.TSLanguageJava
|
||||
import com.itsaky.androidide.utils.StopWatch
|
||||
import jdkx.tools.JavaFileObject
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* [IJavaParser] which uses tree sitter to parse source files.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object TSJavaParser : IJavaParser<TSParseResult> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser.ts
|
||||
|
||||
import com.itsaky.androidide.treesitter.TSQuery
|
||||
import com.itsaky.androidide.treesitter.TSQueryCursor
|
||||
import com.itsaky.androidide.treesitter.TSQueryMatch
|
||||
import com.itsaky.androidide.treesitter.TSTree
|
||||
import com.itsaky.androidide.treesitter.java.TSLanguageJava
|
||||
|
||||
/**
|
||||
* Helper class to prune method bodies in Java source code using.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
object TSMethodPruner {
|
||||
|
||||
private const val METHOD_BODIES_QUERY = "(method_declaration body: (block) @method.body)"
|
||||
|
||||
fun prune(content: StringBuilder, tree: TSTree, cursor: Int) {
|
||||
val root = tree.rootNode
|
||||
TSQuery.create(TSLanguageJava.getInstance(), METHOD_BODIES_QUERY).use { query ->
|
||||
check(query.canAccess()) { "Invalid method bodies query" }
|
||||
TSQueryCursor.create().use { queryCursor ->
|
||||
queryCursor.exec(query, root)
|
||||
|
||||
var match: TSQueryMatch? = queryCursor.nextMatch()
|
||||
while (match != null) {
|
||||
val capture = match.captures[0]
|
||||
val start = capture.node.startByte / 2
|
||||
val end = capture.node.endByte / 2
|
||||
|
||||
if (cursor in start until end) {
|
||||
// cursor is located in this method, so do not prune
|
||||
match = queryCursor.nextMatch()
|
||||
continue
|
||||
}
|
||||
|
||||
// +1 and -1 to avoid removing the curly braces from the body
|
||||
eraseRegion(content, start + 1, end - 1)
|
||||
match = queryCursor.nextMatch()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun eraseRegion(content: StringBuilder, start: Int, end: Int) {
|
||||
for (i in start until end) {
|
||||
if (!content[i].isWhitespace()) {
|
||||
content.setCharAt(i, ' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser.ts
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* [LruCache] for caching parse results of [TSJavaParser].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class TSParseCache(maxSize: Int) : LruCache<URI, TSParseResult>(maxSize) {
|
||||
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: URI,
|
||||
oldValue: TSParseResult,
|
||||
newValue: TSParseResult?
|
||||
) {
|
||||
// Release the tree instance
|
||||
oldValue.tree.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.parser.ts
|
||||
|
||||
import com.itsaky.androidide.treesitter.TSTree
|
||||
import jdkx.tools.JavaFileObject
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
* Parse result of [TSJavaParser].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class TSParseResult(file: JavaFileObject, val tree: TSTree) : AutoCloseable {
|
||||
val uri: URI = file.toUri()
|
||||
val fileModified: Long = file.lastModified
|
||||
|
||||
override fun close() {
|
||||
tree.close()
|
||||
}
|
||||
}
|
||||
+42
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers
|
||||
|
||||
import com.itsaky.androidide.lookup.Lookup
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.progress.ICancelChecker
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Base class for java service providers.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class BaseJavaServiceProvider(
|
||||
protected val file: Path,
|
||||
protected val compiler: JavaCompilerService,
|
||||
protected val settings: IServerSettings
|
||||
) {
|
||||
|
||||
/** Abort the completion if cancelled. */
|
||||
fun abortCompletionIfCancelled() {
|
||||
val checker = Lookup.getDefault().lookup(ICancelChecker::class.java)
|
||||
checker?.abortIfCancelled()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers
|
||||
|
||||
import com.itsaky.androidide.progress.ICancelChecker
|
||||
|
||||
/**
|
||||
* A service provider which supports cancellation.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
abstract class CancelableServiceProvider(cancelChecker: ICancelChecker) :
|
||||
ICancelChecker by cancelChecker
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers;
|
||||
|
||||
import static com.google.common.collect.Range.closedOpen;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.googlejavaformat.java.Formatter;
|
||||
import com.google.googlejavaformat.java.FormatterException;
|
||||
import com.google.googlejavaformat.java.Replacement;
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings;
|
||||
import com.itsaky.androidide.lsp.java.models.JavaServerSettings;
|
||||
import com.itsaky.androidide.lsp.models.CodeFormatResult;
|
||||
import com.itsaky.androidide.lsp.models.FormatCodeParams;
|
||||
import com.itsaky.androidide.lsp.models.IndexedTextEdit;
|
||||
import com.itsaky.androidide.models.Range;
|
||||
import com.itsaky.androidide.utils.StopWatch;
|
||||
import java.util.Collection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Formats Java code using Google Java Format.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
public class CodeFormatProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CodeFormatProvider.class);
|
||||
|
||||
private final JavaServerSettings settings;
|
||||
|
||||
public CodeFormatProvider(IServerSettings settings) {
|
||||
assert settings instanceof JavaServerSettings;
|
||||
this.settings = (JavaServerSettings) settings;
|
||||
}
|
||||
|
||||
public CodeFormatResult format(FormatCodeParams params) {
|
||||
try {
|
||||
final StopWatch watch = new StopWatch("Code formatting");
|
||||
final String content = params.getContent().toString();
|
||||
final Formatter formatter = new Formatter(settings.getFormatterOptions());
|
||||
|
||||
if (params.getRange() == Range.NONE) {
|
||||
String formatted;
|
||||
try {
|
||||
formatted = formatter.formatSource(content);
|
||||
} catch (FormatterException e) {
|
||||
e.printStackTrace();
|
||||
formatted = content;
|
||||
}
|
||||
return CodeFormatResult.forWholeContent(content, formatted);
|
||||
}
|
||||
|
||||
final Collection<com.google.common.collect.Range<Integer>> ranges =
|
||||
getCharRanges(content, params.getRange());
|
||||
|
||||
final ImmutableList<Replacement> 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<Replacement> replacements) {
|
||||
final CodeFormatResult result = new CodeFormatResult(true);
|
||||
for (final Replacement replacement : replacements) {
|
||||
final com.google.common.collect.Range<Integer> 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<com.google.common.collect.Range<Integer>> 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));
|
||||
}
|
||||
}
|
||||
+351
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers;
|
||||
|
||||
import static com.itsaky.androidide.lsp.api.HelpersKt.describeSnippet;
|
||||
import static com.itsaky.androidide.progress.ProgressManager.abortIfCancelled;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.blankj.utilcode.util.ReflectUtils;
|
||||
import com.itsaky.androidide.lsp.api.AbstractServiceProvider;
|
||||
import com.itsaky.androidide.lsp.api.ICompletionProvider;
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings;
|
||||
import com.itsaky.androidide.lsp.internal.model.CachedCompletion;
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompletionInfo;
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerConfig;
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
|
||||
import com.itsaky.androidide.lsp.java.compiler.SourceFileObject;
|
||||
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
|
||||
import com.itsaky.androidide.lsp.java.models.CompilationRequest;
|
||||
import com.itsaky.androidide.lsp.java.models.PartialReparseRequest;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.IJavaCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.IdentifierCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.ImportCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.KeywordCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.MemberReferenceCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.MemberSelectCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.completion.SwitchConstantCompletionProvider;
|
||||
import com.itsaky.androidide.lsp.java.utils.ASTFixer;
|
||||
import com.itsaky.androidide.lsp.java.utils.CancelChecker;
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindCompletionsAt;
|
||||
import com.itsaky.androidide.lsp.models.CompletionParams;
|
||||
import com.itsaky.androidide.lsp.models.CompletionResult;
|
||||
import com.itsaky.androidide.utils.DocumentUtils;
|
||||
import io.github.rosemoe.sora.lang.completion.snippet.CodeSnippet;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import openjdk.source.tree.Tree;
|
||||
import openjdk.source.util.TreePath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CompletionProvider extends AbstractServiceProvider implements ICompletionProvider {
|
||||
|
||||
public static final int MAX_COMPLETION_ITEMS = CompletionResult.MAX_ITEMS;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CompletionProvider.class);
|
||||
private final AtomicBoolean completing = new AtomicBoolean(false);
|
||||
private JavaCompilerService compiler;
|
||||
private CachedCompletion cache;
|
||||
private Consumer<CachedCompletion> nextCacheConsumer;
|
||||
|
||||
public CompletionProvider() {
|
||||
super();
|
||||
}
|
||||
|
||||
public synchronized CompletionProvider reset(JavaCompilerService compiler,
|
||||
IServerSettings settings, CachedCompletion cache,
|
||||
Consumer<CachedCompletion> nextCacheConsumer
|
||||
) {
|
||||
this.compiler = compiler;
|
||||
this.cache = cache;
|
||||
this.nextCacheConsumer = nextCacheConsumer;
|
||||
|
||||
super.applySettings(settings);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canComplete(Path file) {
|
||||
return ICompletionProvider.super.canComplete(file) && DocumentUtils.isJavaFile(file);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CompletionResult complete(@NonNull CompletionParams params) {
|
||||
final var synchronizedTask = compiler.getSynchronizedTask();
|
||||
if (synchronizedTask.isBusy()) {
|
||||
LOG.error("Cannot complete, a compilation task is already in progress");
|
||||
synchronizedTask.logStats();
|
||||
return CompletionResult.EMPTY;
|
||||
}
|
||||
|
||||
completing.set(true);
|
||||
try {
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
return completeInternal(params);
|
||||
} catch (Throwable err) {
|
||||
if (CancelChecker.isCancelled(err)) {
|
||||
LOG.info("Completion request cancelled");
|
||||
} else {
|
||||
LOG.error("An error occurred while computing completions", err);
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
completing.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionResult completeInternal(final @NonNull CompletionParams params) {
|
||||
Path file = params.getFile();
|
||||
int line = params.getPosition().getLine();
|
||||
int column = params.getPosition().getColumn();
|
||||
LOG.info("Complete at {}({},{})...", file.getFileName(), line, column);
|
||||
|
||||
Instant started = Instant.now();
|
||||
|
||||
if (this.cache != null && this.cache.canUseCache(params)) {
|
||||
final String prefix = params.requirePrefix();
|
||||
final String partial = partialIdentifier(prefix, prefix.length());
|
||||
final CompletionResult result = CompletionResult.mapAndFilter(this.cache.getResult(), partial,
|
||||
item -> {
|
||||
final var description = item.getSnippetDescription();
|
||||
var deleteSelected = true;
|
||||
var allowCommands = false;
|
||||
CodeSnippet snippet = null;
|
||||
|
||||
if (description != null) {
|
||||
deleteSelected = description.getDeleteSelected();
|
||||
allowCommands = description.getAllowCommandExecution();
|
||||
snippet = description.getSnippet();
|
||||
}
|
||||
|
||||
item.setSnippetDescription(
|
||||
describeSnippet(partial, deleteSelected, snippet, allowCommands));
|
||||
});
|
||||
|
||||
result.markCached();
|
||||
|
||||
if (!result.isIncomplete() && !result.getItems().isEmpty()) {
|
||||
LOG.info("...using cached completion");
|
||||
logCompletionDuration(started, result);
|
||||
return result;
|
||||
} else {
|
||||
LOG.info("...cached completions are empty");
|
||||
}
|
||||
} else {
|
||||
LOG.info("...cannot use cached completions");
|
||||
}
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
final long cursor = params.getPosition().requireIndex();
|
||||
final var sourceObject = new SourceFileObject(file);
|
||||
final var contentBuilder = new StringBuilder(sourceObject.getCharContent(true));
|
||||
|
||||
int endOfLine = endOfLine(contentBuilder, (int) cursor);
|
||||
contentBuilder.insert(endOfLine, ';');
|
||||
|
||||
final StringBuilder contents;
|
||||
final var context = compiler.compiler.currentContext;
|
||||
if (context != null) {
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
contents = new ASTFixer(context).fix(contentBuilder);
|
||||
} else {
|
||||
contents = contentBuilder;
|
||||
}
|
||||
|
||||
final String contentString = contents.toString();
|
||||
final PartialReparseRequest partialRequest = new PartialReparseRequest(
|
||||
cursor - params.requirePrefix().length(), contentString);
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
|
||||
CompletionResult result = compileAndComplete(contentString, params, partialRequest);
|
||||
if (result == null) {
|
||||
result = CompletionResult.EMPTY;
|
||||
}
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
logCompletionDuration(started, result);
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
if (this.nextCacheConsumer != null) {
|
||||
this.nextCacheConsumer.accept(CachedCompletion.cache(params, result));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String partialIdentifier(String contents, int end) {
|
||||
int start = end;
|
||||
while (start > 0 && Character.isJavaIdentifierPart(contents.charAt(start - 1))) {
|
||||
start--;
|
||||
}
|
||||
return contents.substring(start, end);
|
||||
}
|
||||
|
||||
private void logCompletionDuration(Instant started, @NonNull CompletionResult result) {
|
||||
long elapsedMs = Duration.between(started, Instant.now()).toMillis();
|
||||
LOG.info("Found {} items{}{}in {} ms",
|
||||
result.getItems().size(),
|
||||
result.isIncomplete() ? " (incomplete) " : "",
|
||||
result.isCached() ? " (cached) " : " ",
|
||||
elapsedMs
|
||||
);
|
||||
}
|
||||
|
||||
private int endOfLine(@NonNull CharSequence contents, int cursor) {
|
||||
while (cursor < contents.length()) {
|
||||
char c = contents.charAt(cursor);
|
||||
if (c == '\r' || c == '\n') {
|
||||
break;
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private CompletionResult compileAndComplete(String contents, CompletionParams params,
|
||||
PartialReparseRequest partialRequest
|
||||
) {
|
||||
final long cursor = params.getPosition().requireIndex();
|
||||
final var file = params.getFile();
|
||||
final var started = Instant.now();
|
||||
final var source = new SourceFileObject(file, contents, Instant.now());
|
||||
final var partial = partialIdentifier(contents, (int) cursor);
|
||||
final var endsWithParen = endsWithParen(contents, (int) cursor);
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
|
||||
final CompilationRequest request = new CompilationRequest(Collections.singletonList(source),
|
||||
partialRequest);
|
||||
request.configureContext = ctx -> {
|
||||
final var config = JavaCompilerConfig.instance(ctx);
|
||||
config.setCompletionInfo(new CompletionInfo(params.getPosition()));
|
||||
};
|
||||
|
||||
SynchronizedTask synchronizedTask = compiler.compile(request);
|
||||
return synchronizedTask.get(task -> {
|
||||
if (task == null || task.task == null || task.task.getContext() == null) {
|
||||
LOG.warn("Compilation resulted in an invalid JavacTask");
|
||||
return CompletionResult.EMPTY;
|
||||
}
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
LOG.info("...compiled in {}ms", Duration.between(started, Instant.now()).toMillis());
|
||||
TreePath path = new FindCompletionsAt(task.task).scan(task.root(), cursor);
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
String newPartial = partial;
|
||||
if (path.getLeaf().getKind() == Tree.Kind.IMPORT) {
|
||||
newPartial = qualifiedPartialIdentifier(contents, (int) cursor);
|
||||
if (newPartial.endsWith(ASTFixer.IDENT)) {
|
||||
newPartial = newPartial.substring(0, newPartial.length() - ASTFixer.IDENT.length());
|
||||
}
|
||||
}
|
||||
|
||||
final var result = doComplete(file, contents, cursor, newPartial, endsWithParen, task, path);
|
||||
|
||||
// IMPORTANT: Unregister the completion info from the compiler configuration
|
||||
if (task.task.getContext() != null) {
|
||||
final var compilerConfig = JavaCompilerConfig.instance(task.task.getContext());
|
||||
compilerConfig.setCompletionInfo(null);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CompletionResult doComplete(final Path file, final String contents, final long cursor,
|
||||
final String partial, final boolean endsWithParen,
|
||||
final CompileTask task, final TreePath path
|
||||
) {
|
||||
final Class<? extends IJavaCompletionProvider> klass;
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
switch (path.getLeaf().getKind()) {
|
||||
case IDENTIFIER:
|
||||
klass = IdentifierCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_SELECT:
|
||||
klass = MemberSelectCompletionProvider.class;
|
||||
break;
|
||||
case MEMBER_REFERENCE:
|
||||
klass = MemberReferenceCompletionProvider.class;
|
||||
break;
|
||||
case SWITCH:
|
||||
klass = SwitchConstantCompletionProvider.class;
|
||||
break;
|
||||
case IMPORT:
|
||||
klass = ImportCompletionProvider.class;
|
||||
break;
|
||||
default:
|
||||
klass = KeywordCompletionProvider.class;
|
||||
break;
|
||||
}
|
||||
|
||||
final IJavaCompletionProvider provider = ReflectUtils.reflect(klass)
|
||||
.newInstance(file, cursor, compiler, getSettings())
|
||||
.get();
|
||||
|
||||
if (provider instanceof ImportCompletionProvider) {
|
||||
((ImportCompletionProvider) provider).setImportPath(
|
||||
qualifiedPartialIdentifier(contents, (int) cursor));
|
||||
}
|
||||
|
||||
abortIfCancelled();
|
||||
abortCompletionIfCancelled();
|
||||
return provider.complete(task, path, partial, endsWithParen);
|
||||
}
|
||||
|
||||
private boolean endsWithParen(@NonNull String contents, int cursor) {
|
||||
for (int i = cursor; i < contents.length(); i++) {
|
||||
if (!Character.isJavaIdentifierPart(contents.charAt(i))) {
|
||||
return contents.charAt(i) == '(';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String qualifiedPartialIdentifier(String contents, int end) {
|
||||
int start = end;
|
||||
while (start > 0 && isQualifiedIdentifierChar(contents.charAt(start - 1))) {
|
||||
start--;
|
||||
}
|
||||
return contents.substring(start, end);
|
||||
}
|
||||
|
||||
private boolean isQualifiedIdentifierChar(char c) {
|
||||
return c == '.' || Character.isJavaIdentifierPart(c);
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.itsaky.androidide.lsp.api.IServerSettings;
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService;
|
||||
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
|
||||
import com.itsaky.androidide.lsp.java.providers.definition.ErroneousDefinitionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.definition.IJavaDefinitionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.definition.LocalDefinitionProvider;
|
||||
import com.itsaky.androidide.lsp.java.providers.definition.RemoteDefinitionProvider;
|
||||
import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
|
||||
import com.itsaky.androidide.lsp.models.DefinitionParams;
|
||||
import com.itsaky.androidide.lsp.models.DefinitionResult;
|
||||
import com.itsaky.androidide.models.Location;
|
||||
import com.itsaky.androidide.models.Position;
|
||||
import com.itsaky.androidide.progress.ICancelChecker;
|
||||
import com.itsaky.androidide.utils.DocumentUtils;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import jdkx.lang.model.element.Element;
|
||||
import jdkx.lang.model.element.TypeElement;
|
||||
import jdkx.lang.model.type.TypeKind;
|
||||
import jdkx.tools.JavaFileObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class DefinitionProvider extends CancelableServiceProvider {
|
||||
|
||||
public static final List<Location> 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<Location> locations = findDefinition();
|
||||
|
||||
LOG.debug("Found {} definitions...", locations.size());
|
||||
return new DefinitionResult(locations);
|
||||
}
|
||||
|
||||
public List<Location> 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<JavaFileObject> 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 "";
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<DiagnosticItem> {
|
||||
val result = mutableListOf<DiagnosticItem>()
|
||||
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<DiagnosticItem>
|
||||
) {
|
||||
val notThrown = mutableMapOf<TreePath?, String>()
|
||||
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<DiagnosticItem>
|
||||
) {
|
||||
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("<init>")) {
|
||||
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<out JavaFileObject?>,
|
||||
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<out JavaFileObject?>,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.itsaky.androidide.lsp.java.providers
|
||||
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask
|
||||
import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService
|
||||
import com.itsaky.androidide.lsp.java.providers.DiagnosticsProvider.findDiagnostics
|
||||
import com.itsaky.androidide.lsp.java.utils.CancelChecker
|
||||
import com.itsaky.androidide.lsp.models.DiagnosticResult
|
||||
import com.itsaky.androidide.progress.ProgressManager
|
||||
import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled
|
||||
import com.itsaky.androidide.projects.FileManager
|
||||
import com.itsaky.androidide.projects.IProjectManager
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.file.Path
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Code analyzer for java source code.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class JavaDiagnosticProvider {
|
||||
|
||||
private val analyzeTimestamps = mutableMapOf<Path, Instant>()
|
||||
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 compiler = JavaCompilerService(module)
|
||||
|
||||
abortIfCancelled()
|
||||
|
||||
log.debug("Analyzing: {}", file)
|
||||
|
||||
val modifiedAt = FileManager.getLastModified(file)
|
||||
val analyzedAt = analyzeTimestamps[file]
|
||||
|
||||
if (analyzedAt?.isAfter(modifiedAt) == true) {
|
||||
log.debug("Using cached analyze results...")
|
||||
return cachedDiagnostics
|
||||
}
|
||||
|
||||
analyzingThread?.let { analyzingThread ->
|
||||
if (analyzing.get()) {
|
||||
log.debug("Cancelling currently analyzing thread...")
|
||||
ProgressManager.instance.cancel(analyzingThread)
|
||||
this.analyzingThread = null
|
||||
}
|
||||
}
|
||||
|
||||
analyzing.set(true)
|
||||
|
||||
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 cancel() {
|
||||
this.analyzingThread?.cancel()
|
||||
}
|
||||
|
||||
fun clearTimestamp(file: Path) {
|
||||
analyzeTimestamps.remove(file)
|
||||
}
|
||||
|
||||
private fun doAnalyze(file: Path, task: CompileTask): DiagnosticResult {
|
||||
val result =
|
||||
if (!isTaskValid(task)) {
|
||||
// Do not use Collections.emptyList ()
|
||||
// The returned list is accessed and the list returned by Collections.emptyList()
|
||||
// throws exception when trying to access.
|
||||
log.info("Using cached diagnostics")
|
||||
cachedDiagnostics
|
||||
} else
|
||||
DiagnosticResult(
|
||||
file,
|
||||
findDiagnostics(task, file).sortedBy {
|
||||
it.range
|
||||
}
|
||||
)
|
||||
return result.also {
|
||||
log.info("Analyze file completed. Found {} diagnostic items", result.diagnostics.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTaskValid(task: CompileTask?): Boolean {
|
||||
abortIfCancelled()
|
||||
return task?.task != null && task.roots != null && task.roots.size > 0
|
||||
}
|
||||
|
||||
inner class AnalyzingThread(val compiler: JavaCompilerService, val file: Path) :
|
||||
Thread("JavaAnalyzerThread") {
|
||||
|
||||
var result: DiagnosticResult = DiagnosticResult.NO_UPDATE
|
||||
|
||||
fun cancel() {
|
||||
ProgressManager.instance.cancel(this)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
result =
|
||||
try {
|
||||
compiler.compile(file).get { task -> doAnalyze(file, task) }
|
||||
} catch (err: Throwable) {
|
||||
if (CancelChecker.isCancelled(err)) {
|
||||
log.error("Analyze request cancelled")
|
||||
} else {
|
||||
log.warn("Unable to analyze file", err)
|
||||
}
|
||||
DiagnosticResult.NO_UPDATE
|
||||
} finally {
|
||||
compiler.destroy()
|
||||
analyzing.set(false)
|
||||
}
|
||||
.also {
|
||||
cachedDiagnostics = it
|
||||
analyzeTimestamps[file] = Instant.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindBiggerRange;
|
||||
import com.itsaky.androidide.lsp.models.ExpandSelectionParams;
|
||||
import com.itsaky.androidide.models.Range;
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Selection provider implementation for Java.
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* This file is part of AndroidIDE.
|
||||
*
|
||||
* AndroidIDE is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* AndroidIDE is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.itsaky.androidide.lsp.java.providers;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompileTask;
|
||||
import com.itsaky.androidide.lsp.java.compiler.CompilerProvider;
|
||||
import com.itsaky.androidide.lsp.java.compiler.SynchronizedTask;
|
||||
import com.itsaky.androidide.lsp.java.utils.CancelChecker;
|
||||
import com.itsaky.androidide.lsp.java.utils.FindHelper;
|
||||
import com.itsaky.androidide.lsp.java.utils.NavigationHelper;
|
||||
import com.itsaky.androidide.lsp.java.visitors.FindReferences;
|
||||
import com.itsaky.androidide.lsp.models.ReferenceParams;
|
||||
import com.itsaky.androidide.lsp.models.ReferenceResult;
|
||||
import com.itsaky.androidide.models.Location;
|
||||
import com.itsaky.androidide.progress.ICancelChecker;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import jdkx.lang.model.element.Element;
|
||||
import jdkx.lang.model.element.TypeElement;
|
||||
import openjdk.source.tree.CompilationUnitTree;
|
||||
import openjdk.source.util.TreePath;
|
||||
|
||||
public class ReferenceProvider extends CancelableServiceProvider {
|
||||
|
||||
public static final List<Location> 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<Location> locations;
|
||||
try {
|
||||
locations = find();
|
||||
} catch (Exception err) {
|
||||
if (!CancelChecker.isCancelled(err)) {
|
||||
throw err;
|
||||
}
|
||||
locations = new ArrayList<>();
|
||||
}
|
||||
|
||||
return new ReferenceResult(locations);
|
||||
}
|
||||
|
||||
public List<Location> 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<List<Location>> 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("<init>")) {
|
||||
memberName = parentClass.getSimpleName().toString();
|
||||
}
|
||||
|
||||
String finalMemberName = memberName;
|
||||
return () -> findMemberReferences(className, finalMemberName);
|
||||
}
|
||||
|
||||
return () -> NOT_SUPPORTED;
|
||||
});
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private List<Location> 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<Location> 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<Location> findReferences(CompileTask task) {
|
||||
abortIfCancelled();
|
||||
Element element = NavigationHelper.findElement(task, file, line, column, this);
|
||||
List<TreePath> paths = new ArrayList<>();
|
||||
for (CompilationUnitTree root : task.roots) {
|
||||
abortIfCancelled();
|
||||
new FindReferences(task.task, element).scan(root, paths);
|
||||
}
|
||||
List<Location> locations = new ArrayList<>();
|
||||
for (TreePath p : paths) {
|
||||
abortIfCancelled();
|
||||
locations.add(FindHelper.location(task, p));
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user