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:
Akash Yadav
2024-05-29 13:11:44 +05:30
parent c0c185cfb7
commit 1efd6a04d5
2457 changed files with 1069 additions and 709 deletions
+1
View File
@@ -0,0 +1 @@
/build
+83
View File
@@ -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)
}
View File
+21
View File
@@ -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
@@ -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()
}
}
}
}
@@ -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
)
)
})
}
@@ -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)
}
}
+17
View File
@@ -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",
"}"
]
}
}
@@ -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
}
@@ -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
}
}
@@ -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
}
}
@@ -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()
}
}
}
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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()
}
}
}
}
@@ -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)
}
}
@@ -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>>)
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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
}
}
@@ -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)
}
}
@@ -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);
}
@@ -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) })
}
}
}
@@ -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;
}
}
@@ -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)
}
}
@@ -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)
}
}
}
@@ -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
}
@@ -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")
}
}
@@ -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()
}
}
}
}
@@ -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)
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
}
@@ -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
}
}
}
}
@@ -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
@@ -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
}
}
}
}
@@ -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)
}
}
}
@@ -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
)
}
@@ -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()
}
}
@@ -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()
}
}
@@ -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
@@ -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));
}
}
@@ -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);
}
}
@@ -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 "";
}
}
@@ -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
}
}
}
@@ -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()
}
}
}
}
@@ -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();
});
}
}
@@ -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