diff --git a/app/src/main/assets/data/common/tooling-api-all.jar b/app/src/main/assets/data/common/tooling-api-all.jar index 2aa15b4fa..4397b7c88 100644 Binary files a/app/src/main/assets/data/common/tooling-api-all.jar and b/app/src/main/assets/data/common/tooling-api-all.jar differ diff --git a/shared/src/main/java/com/itsaky/androidide/utils/ClassTrie.kt b/shared/src/main/java/com/itsaky/androidide/utils/ClassTrie.kt index 01f1b80c5..5249f33c1 100644 --- a/shared/src/main/java/com/itsaky/androidide/utils/ClassTrie.kt +++ b/shared/src/main/java/com/itsaky/androidide/utils/ClassTrie.kt @@ -220,6 +220,10 @@ open class ClassTrie(val root: Node = Node()) { return children.computeIfAbsent(name) { Node(it, qualifiedName, this) } } + open fun removeChild(child: Node) { + this.children.remove(child.name) + } + open fun allClassNames(): Set { return this.allClassNodes().map { it.qualifiedName }.toSet() } diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt index 6e21af10e..973098fc9 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/ProjectManager.kt @@ -32,9 +32,11 @@ import com.itsaky.androidide.projects.util.ProjectTransformer import com.itsaky.androidide.tasks.executeAsync import com.itsaky.androidide.tooling.api.IProject import com.itsaky.androidide.tooling.api.messages.result.InitializeResult +import com.itsaky.androidide.utils.DocumentUtils import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.flashError import java.io.File +import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.extension import kotlin.io.path.isDirectory @@ -148,10 +150,6 @@ object ProjectManager : EventReceiver { return app } - fun getApplicationResDirectories(): Set { - return getApplicationModule()?.getResourceDirectories() ?: emptySet() - } - fun notifyProjectUpdate() { executeAsync { @@ -170,16 +168,18 @@ object ProjectManager : EventReceiver { } } - fun findModuleForFile(file: File): ModuleProject? { + @JvmOverloads + fun findModuleForFile(file: File, checkExistance: Boolean = false): ModuleProject? { if (!checkInit()) { return null } - return this.rootProject!!.findModuleForFile(file) + return this.rootProject!!.findModuleForFile(file, checkExistance) } - fun findModuleForFile(file: Path): ModuleProject? { - return findModuleForFile(file.toFile()) + @JvmOverloads + fun findModuleForFile(file: Path, checkExistance: Boolean = false): ModuleProject? { + return findModuleForFile(file.toFile(), checkExistance) } fun containsSourceFile(file: Path): Boolean { @@ -187,6 +187,10 @@ object ProjectManager : EventReceiver { return false } + if (!Files.exists(file)) { + return false + } + for (module in this.rootProject!!.subModules) { if (module !is ModuleProject) { continue @@ -245,18 +249,55 @@ object ProjectManager : EventReceiver { @Subscribe(threadMode = BACKGROUND) fun onFileCreated(event: FileCreationEvent) { generateSourcesIfNecessary(event) + + if (DocumentUtils.isJavaFile(event.file.toPath())) { + findModuleForFile(event.file)?.let { + val sourceRoot = it.findSourceRoot(event.file) ?: return@let + + // add the source node entry + it.compileJavaSourceClasses.append(event.file.toPath(), sourceRoot) + } + } } @Suppress("unused") @Subscribe(threadMode = BACKGROUND) fun onFileDeleted(event: FileDeletionEvent) { generateSourcesIfNecessary(event) + + // Remove the source node entry + // Do not check for Java file DocumentUtils.isJavaFile(...) as it checks for file existence as + // well. As the file is already deleted, it will always return false + if (event.file.extension == "java") { + findModuleForFile(event.file) + ?.compileJavaSourceClasses + ?.findSource(event.file.toPath()) + ?.let { it.parent?.removeChild(it) } + } } @Suppress("unused") @Subscribe(threadMode = BACKGROUND) fun onFileRenamed(event: FileRenameEvent) { generateSourcesIfNecessary(event) + + // Do not check for Java file DocumentUtils.isJavaFile(...) as it checks for file existence as + // well. As the file is already renamed to another filename, it will always return false + if (event.file.extension == "java") { + // remove the source node entry + findModuleForFile(event.file) + ?.compileJavaSourceClasses + ?.findSource(event.file.toPath()) + ?.let { it.parent?.removeChild(it) } + } + + if (DocumentUtils.isJavaFile(event.newFile.toPath())) { + findModuleForFile(event.newFile)?.let { + val sourceRoot = it.findSourceRoot(event.newFile) ?: return@let + // add the new source node entry + it.compileJavaSourceClasses.append(event.newFile.toPath(), sourceRoot) + } + } } private fun generateSourcesIfNecessary(event: FileEvent) { @@ -269,6 +310,10 @@ object ProjectManager : EventReceiver { generateSources(builder) } + private fun ModuleProject.findSourceRoot(file: File): Path? { + return getCompileSourceDirectories().find { file.path.startsWith(it.path) }?.toPath() + } + private fun isResource(file: File): Boolean { val module = findModuleForFile(file) ?: return false if (module is AndroidModule) { diff --git a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/Project.kt b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/Project.kt index 1485cd856..44dba1bc2 100644 --- a/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/Project.kt +++ b/subprojects/projects/src/main/java/com/itsaky/androidide/projects/api/Project.kt @@ -33,7 +33,7 @@ import java.util.concurrent.* * @param name The display name of the project. * @param description The project description. * @param path The project path (same as Gradle project paths). For example, `:app`, - * `:module:submodule`, etc. Root project is always represented by path `:`. + * `:module:submodule`, etc. Root project is always represented by path `:`. * @param projectDir The project directory. * @param buildDir The build directory of the project. * @param buildScript The Gradle buildscript file of the project. @@ -124,7 +124,13 @@ open class Project( return findModuleForFile(file.toFile()) } - fun findModuleForFile(file: File): ModuleProject? { + @JvmOverloads + fun findModuleForFile(file: File, checkExistance: Boolean = false): ModuleProject? { + + if (!file.exists() && checkExistance) { + return null + } + val path = file.path var longestPath = "" var moduleWithLongestPath: ModuleProject? = null diff --git a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt index 86f383ccb..d583bfeed 100644 --- a/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt +++ b/subprojects/projects/src/test/java/com/itsaky/androidide/projects/api/ModuleProjectTest.kt @@ -18,18 +18,23 @@ package com.itsaky.androidide.projects.api import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.eventbus.events.file.FileCreationEvent +import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent +import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import com.itsaky.androidide.lookup.Lookup import com.itsaky.androidide.projects.ProjectManager import com.itsaky.androidide.projects.builder.BuildService import com.itsaky.androidide.tooling.api.messages.InitializeProjectMessage import com.itsaky.androidide.tooling.testing.ToolingApiTestLauncher import com.itsaky.androidide.utils.SourceClassTrie.SourceNode -import java.io.File -import java.nio.file.Files import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.io.path.writeText /** @author Akash Yadav */ @RunWith(RobolectricTestRunner::class) @@ -40,9 +45,8 @@ class ModuleProjectTest { fun test() { val (server, project) = ToolingApiTestLauncher().launchServer() server.initialize(InitializeProjectMessage(File("../../tests/test-project").absolutePath)).get() - + Lookup.DEFAULT.register(BuildService.KEY_PROJECT_PROXY, project) - ProjectManager.setupProject() verifyProjectManagerAPIs() @@ -177,18 +181,35 @@ class ModuleProjectTest { } private fun verifyProjectManagerAPIs() { + + val testCls = + Paths.get("../../tests/test-project/app/src/main/java/com/itsaky/test/app/Test.java") + .toAbsolutePath() + .normalize() + val testCls_renamed = testCls.parent!!.resolve("TestRenamed.java") + + if (Files.exists(testCls)) { + Files.delete(testCls) + } + + if (Files.exists(testCls_renamed)) { + Files.delete(testCls_renamed) + } + + ProjectManager.setupProject() + val root = ProjectManager.rootProject assertThat(root).isNotNull() assertThat(root!!.path).isEqualTo(":") val source = root.projectDir.toPath().resolve("app/src/main/java/com/itsaky/test/app/MainActivity.java") - val moduleForFile = root.findModuleForFile(source) - assertThat(moduleForFile).isNotNull() - assertThat(moduleForFile!!.path).isEqualTo(":app") - assertThat(moduleForFile.projectDir.path).isEqualTo(File(root.projectDir, "app").path) + val module = root.findModuleForFile(source) + assertThat(module).isNotNull() + assertThat(module!!.path).isEqualTo(":app") + assertThat(module.projectDir.path).isEqualTo(File(root.projectDir, "app").path) - val sourceNode = moduleForFile.compileJavaSourceClasses.findSource(source) + val sourceNode = module.compileJavaSourceClasses.findSource(source) assertThat(sourceNode).isNotNull() assertThat(sourceNode!!.isClass).isTrue() assertThat(sourceNode.name).isEqualTo("MainActivity") @@ -196,5 +217,30 @@ class ModuleProjectTest { assertThat(sourceNode.qualifiedName).isEqualTo("com.itsaky.test.app.MainActivity") assertThat(sourceNode.children).isEmpty() assertThat(Files.isSameFile(source, sourceNode.file)).isTrue() + + // make sure the files are not indexed + assertThat(ProjectManager.containsSourceFile(testCls)).isFalse() + assertThat(ProjectManager.containsSourceFile(testCls_renamed)).isFalse() + + testCls.writeText("public class Test { }") + ProjectManager.onFileCreated(FileCreationEvent(testCls.toFile())) + assertThat(ProjectManager.containsSourceFile(testCls)).isTrue() + assertThat(ProjectManager.containsSourceFile(testCls_renamed)).isFalse() + assertThat(ProjectManager.findModuleForFile(testCls, true)).isEqualTo(module) + assertThat(ProjectManager.findModuleForFile(testCls_renamed, true)).isNull() + + Files.move(testCls, testCls_renamed) + ProjectManager.onFileRenamed(FileRenameEvent(testCls.toFile(), testCls_renamed.toFile())) + assertThat(ProjectManager.containsSourceFile(testCls)).isFalse() + assertThat(ProjectManager.containsSourceFile(testCls_renamed)).isTrue() + assertThat(ProjectManager.findModuleForFile(testCls, true)).isNull() + assertThat(ProjectManager.findModuleForFile(testCls_renamed, true)).isEqualTo(module) + + Files.delete(testCls_renamed) + ProjectManager.onFileDeleted(FileDeletionEvent(testCls_renamed.toFile())) + assertThat(ProjectManager.containsSourceFile(testCls)).isFalse() + assertThat(ProjectManager.containsSourceFile(testCls_renamed)).isFalse() + assertThat(ProjectManager.findModuleForFile(testCls, true)).isNull() + assertThat(ProjectManager.findModuleForFile(testCls_renamed, true)).isNull() } } diff --git a/tests/test-project/app/src/main/java/com/itsaky/test/app/Test.java b/tests/test-project/app/src/main/java/com/itsaky/test/app/Test.java new file mode 100644 index 000000000..96154701e --- /dev/null +++ b/tests/test-project/app/src/main/java/com/itsaky/test/app/Test.java @@ -0,0 +1 @@ +public class Test { } \ No newline at end of file