fix(projects): introduce workspace API and remove 'Project' model

This commit is contained in:
Akash Yadav
2024-07-01 15:58:06 +05:30
parent dba601b71e
commit 1165f12ccb
51 changed files with 674 additions and 538 deletions
@@ -47,8 +47,8 @@ class FindInProjectAction() : EditorActivityAction() {
return
}
val project = IProjectManager.getInstance().rootProject
if (project == null || project.subProjects.isEmpty()) {
val project = IProjectManager.getInstance().getWorkspace()
if (project == null || project.getSubProjects().isEmpty()) {
markInvisible()
return
}
@@ -26,6 +26,7 @@ import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.openApplicationModuleChooser
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.android.androidAppProjects
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.flashError
import org.slf4j.LoggerFactory
@@ -58,8 +59,11 @@ class LaunchAppAction(context: Context, override val order: Int) : EditorActivit
visible = true
val projectManager = IProjectManager.getInstance()
enabled = projectManager.getAndroidAppModules().isNotEmpty()
enabled = IProjectManager.getInstance()
.getWorkspace()
?.androidAppProjects()
?.iterator()
?.hasNext() == true
}
override suspend fun execAction(data: ActionData) {
@@ -41,8 +41,12 @@ inline fun openApplicationModuleChooser(data: ActionData,
*/
inline fun openApplicationModuleChooser(context: Context,
crossinline callback: (AndroidModule) -> Unit) {
val projectManager = IProjectManager.getInstance()
val applications = projectManager.getAndroidAppModules()
val applications = IProjectManager.getInstance()
.getWorkspace()
?.androidProjects()
?.filter(AndroidModule::isApplication)
?.toList() ?: emptyList()
if (applications.isEmpty()) {
flashError(R.string.msg_launch_failure_no_app_module)
ILogger.ROOT.error("Cannot run application. No application modules found in project.")
@@ -61,7 +65,8 @@ inline fun openApplicationModuleChooser(context: Context,
context,
context.getString(R.string.title_choose_application),
applications.map { it.path }.toTypedArray(),
0) { selection ->
0
) { selection ->
val app = applications[selection]
ILogger.ROOT.info("Selected application: '{}'", app.path)
callback(app)
@@ -58,7 +58,7 @@ class TerminalSidebarAction(context: Context, override val order: Int) : Abstrac
)
putExtra(
TERMUX_ACTIVITY.EXTRA_SESSION_NAME,
IProjectManager.getInstance().rootProject?.rootProject?.name
IProjectManager.getInstance().getWorkspace()?.getRootProject()?.name
)
putExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, isFailsafe)
}
@@ -32,7 +32,7 @@ import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.utils.DialogUtils
@@ -196,7 +196,7 @@ class MainActivity : EdgeToEdgeIDEActivity() {
}
internal fun openProject(root: File) {
ProjectManagerImpl.getInstance().projectPath = root.absolutePath
IProjectManager.getInstance().openProject(root)
startActivity(Intent(this, EditorActivityKt::class.java))
}
@@ -84,7 +84,6 @@ import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
import com.itsaky.androidide.preferences.internal.BuildPreferences
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.tasks.cancelIfActive
import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout
@@ -325,9 +324,8 @@ abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSele
registerLanguageServers()
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_PROJECT_PATH)) {
ProjectManagerImpl.getInstance().projectPath = savedInstanceState.getString(
KEY_PROJECT_PATH
)!!
IProjectManager.getInstance()
.openProject(savedInstanceState.getString(KEY_PROJECT_PATH)!!)
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
@@ -39,9 +39,9 @@ import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.projects.GradleProject
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.internal.ProjectManagerImpl
import com.itsaky.androidide.services.builder.GradleBuildService
import com.itsaky.androidide.services.builder.GradleBuildServiceConnnection
import com.itsaky.androidide.services.builder.gradleDistributionParams
@@ -320,7 +320,8 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
// use the default variant selections
if (currentVariants == null) {
log.debug(
"No variant selection information available. Default build variants will be selected.")
"No variant selection information available. Default build variants will be selected."
)
initializeProject(emptyMap())
return
}
@@ -355,7 +356,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
*/
fun initializeProject(buildVariants: Map<String, String>) {
val manager = ProjectManagerImpl.getInstance()
val projectDir = File(manager.projectPath)
val projectDir = manager.projectDir
if (!projectDir.exists()) {
log.error("GradleProject directory does not exist. Cannot initialize project")
return
@@ -471,7 +472,8 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
if (pid != metadata.pid) {
log.warn(
"Tooling server pid mismatch. Expected: {}, Actual: {}. Replacing memory watcher...",
pid, metadata.pid)
pid, metadata.pid
)
memoryUsageWatcher.watchProcess(metadata.pid, PROC_GRADLE_TOOLING)
resetMemUsageChart()
}
@@ -495,7 +497,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
editorActivityScope.launch(Dispatchers.IO) {
manager.setupProject()
manager.notifyProjectUpdate()
updateBuildVariants(manager.androidBuildVariants)
updateBuildVariants(manager.requireWorkspace().getAndroidVariantSelections())
com.itsaky.androidide.tasks.runOnUiThread {
postProjectInit(true, null)
@@ -552,7 +554,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
protected open fun createFindInProjectDialog(): AlertDialog? {
val manager = ProjectManagerImpl.getInstance()
if (manager.rootProject == null) {
if (manager.getWorkspace() == null) {
log.warn("No root project model found. Is the project initialized?")
flashError(getString(string.msg_project_not_initialized))
return null
@@ -560,7 +562,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
val moduleDirs =
try {
manager.rootProject!!.subProjects.stream().map(GradleProject::projectDir)
manager.getWorkspace()!!.getSubProjects().stream().map(GradleProject::projectDir)
.collect(Collectors.toList())
} catch (e: Throwable) {
flashError(getString(string.msg_no_modules))
@@ -655,13 +657,13 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
val manager = ProjectManagerImpl.getInstance()
GeneralPreferences.lastOpenedProject = manager.projectDirPath
try {
val project = manager.rootProject
if (project == null) {
val workspace = manager.getWorkspace()
if (workspace == null) {
log.warn("GradleProject not initialized. Skipping initial setup...")
return
}
var projectName = project.rootProject.name
var projectName = workspace.getRootProject().name
if (projectName.isEmpty()) {
projectName = manager.projectDir.name
}
@@ -177,10 +177,10 @@ class RunTasksDialogFragment : BottomSheetDialogFragment() {
viewModel.displayedChild = CHILD_LOADING
executeAsync({
val project = IProjectManager.getInstance().rootProject
val workspace = IProjectManager.getInstance().getWorkspace()
?: return@executeAsync emptyList<Checkable<GradleTask>>()
return@executeAsync project.subProjects
return@executeAsync workspace.getSubProjects()
.flatMap<GradleProject, GradleTask> { it.tasks }
.map<GradleTask, Checkable<GradleTask>> {
Checkable<GradleTask>(false, it)
@@ -19,7 +19,7 @@ package com.itsaky.androidide.indexing.platform
import com.google.auto.service.AutoService
import com.itsaky.androidide.indexing.IIndexService
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.IWorkspace
import org.slf4j.LoggerFactory
import java.io.File
@@ -36,9 +36,9 @@ internal class PlatformIndexService : IIndexService {
override val displayName: String
get() = "Android Platform Indexing Service"
override fun scanFiles(project: Project): Collection<File> {
override fun scanFiles(workspace: IWorkspace): Collection<File> {
return mutableListOf<File>().apply {
project.findAndroidModules().forEach { androidModule ->
workspace.androidProjects().forEach { androidModule ->
add(androidModule.getPlatformDir()?.also {
log.debug("Adding {} to the list of indexable paths", it)
} ?: return@forEach)
@@ -47,7 +47,7 @@ internal class PlatformIndexService : IIndexService {
}
override suspend fun indexFiles(
project: Project,
workspace: IWorkspace,
files: Collection<File>
) {
}
@@ -17,7 +17,7 @@
package com.itsaky.androidide.indexing
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.IWorkspace
import kotlinx.coroutines.Deferred
import java.io.File
@@ -36,16 +36,16 @@ interface IIndexService {
/**
* Called to scan files that need to be indexed by this index service.
*
* @param project The root project model which can be used to query the project properties.
* @param workspace The root workspace model which can be used to query the workspace properties.
* @return A [Deferred] collection of files that need to be indexed.
*/
fun scanFiles(project: Project): Collection<File>
fun scanFiles(workspace: IWorkspace): Collection<File>
/**
* Called to index the given files.
*
* @param project The root project model which can be used to query the project properties.
* @param workspace The root workspace model which can be used to query the workspace properties.
* @param files The files to index.
*/
suspend fun indexFiles(project: Project, files: Collection<File>)
suspend fun indexFiles(workspace: IWorkspace, files: Collection<File>)
}
@@ -19,17 +19,20 @@ package com.itsaky.androidide.lsp.api;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent;
import com.itsaky.androidide.projects.Project;
import com.itsaky.androidide.projects.IWorkspace;
import com.itsaky.androidide.utils.ILogger;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Thread-safe implementation of {@link ILanguageServerRegistry}.
@@ -117,14 +120,14 @@ public class DefaultLanguageServerRegistry extends ILanguageServerRegistry {
@Subscribe(threadMode = ThreadMode.BACKGROUND)
@SuppressWarnings("unused")
public void onProjectInitialized(ProjectInitializedEvent event) {
final var project = event.get(Project.class);
if (project == null) {
final var workspace = event.get(IWorkspace.class);
if (workspace == null) {
return;
}
ILogger.ROOT.debug("Dispatching ProjectInitializedEvent to language servers...");
for (final var server : mRegister.values()) {
server.setupWithProject(project);
server.setupWorkspace(workspace);
}
}
}
@@ -46,7 +46,7 @@ import com.itsaky.androidide.lsp.models.ReferenceResult
import com.itsaky.androidide.lsp.models.SignatureHelp
import com.itsaky.androidide.lsp.models.SignatureHelpParams
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.IWorkspace
import java.nio.file.Path
/**
@@ -90,14 +90,14 @@ interface ILanguageServer {
fun applySettings(settings: IServerSettings?)
/**
* Setup this language server with the given project. Servers are not expected to keep a reference
* to the provided project. Instead, use
* [getRootProject()][com.itsaky.androidide.projects.IProjectManager.rootProject] to
* obtain the project instance.
* Setup this language server with the given workspace. Servers are not expected to keep a reference
* to the provided workspace. Instead, use
* [getRootWorkspace()][com.itsaky.androidide.projects.IProjectManager.workspace] to
* obtain the workspace instance.
*
* @param project The initialized project.
* @param workspace The initialized workspace.
*/
fun setupWithProject(project: Project)
fun setupWorkspace(workspace: IWorkspace)
/**
* Compute code completions for the given completion params.
@@ -32,7 +32,8 @@ fun setupLookupForCompletion(file: File) {
}
fun setupLookupForCompletion(file: Path) {
val module = IProjectManager.getInstance().findModuleForFile(file, false) ?: return
val module =
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false) ?: return
val lookup = Lookup.getDefault()
lookup.update(ModuleProject.COMPLETION_MODULE_KEY, module)
@@ -20,13 +20,10 @@ package com.itsaky.androidide.projects
import androidx.annotation.RestrictTo
import com.android.builder.model.v2.models.ProjectSyncIssues
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.utils.ServiceLoader
import java.io.File
import java.nio.file.Path
/**
* Project manager.
@@ -51,21 +48,16 @@ interface IProjectManager {
}
}
/**
* The root project model.
*/
val rootProject: Project?
/**
* The path to the project's root directory.
*/
val projectDirPath: String
get() = projectDir.path
/**
* The project's root directory.
*/
val projectDir: File
get() = File(projectDirPath)
/**
* Issues that were encountered during project synchronization.
@@ -73,11 +65,14 @@ interface IProjectManager {
val projectSyncIssues: ProjectSyncIssues?
/**
* Build variant information about Android modules in the project.
*
* The entries in this map are the Gradle project paths mapped to the corresponding [BuildVariantInfo]s.
* Open the given project directory.
*/
val androidBuildVariants: Map<String, BuildVariantInfo>
fun openProject(directory: File)
/**
* Same as [openProject].
*/
fun openProject(path: String) = openProject(File(path))
/**
* Setup the project with the given [project proxy][project] from the Tooling API.
@@ -90,81 +85,19 @@ interface IProjectManager {
)
/**
* Get the list of the modules in this project which are Android projects.
* Get the workspace instance.
*
* @return The list of Android modules.
* @return The configured workspace, or `null`.
*/
fun getAndroidModules(): List<AndroidModule>
fun getWorkspace(): IWorkspace?
/**
* Get the list of modules in this project which are Android application modules.
* Get the workspace instance.
*
* @return The list of Android application modules.
* @return The configured workspace.
* @throws IWorkspace.NotConfiguredException If the workspace has not been configured yet.
*/
fun getAndroidAppModules(): List<AndroidModule>
/**
* Get the list of modules in this project which are Android library modules.
*
* @return The list of Android library modules.
*/
fun getAndroidLibraryModules(): List<AndroidModule>
/**
* Find the module for the given file.
*
* @param file The file to find the module for.
* @return The module project, or `null` if not found.
*/
fun findModuleForFile(file: File): ModuleProject? {
return findModuleForFile(file, true)
}
/**
* Find the module for the given file.
*
* @param file The file to find the module for.
* @param checkExistance Whether to check if the file exists or not.
* @return The [ModuleProject] for the given file, or `null` if cannot be found.
*/
fun findModuleForFile(file: File, checkExistance: Boolean): ModuleProject?
/**
* Find the module for the given file.
*
* @param file The file to find the module for.
* @return The module project, or `null` if not found.
*/
fun findModuleForFile(file: Path): ModuleProject? {
return findModuleForFile(file, true)
}
/**
* Find the module for the given file.
*
* @param file The file to find the module for.
* @param checkExistance Whether to check if the file exists or not.
* @return The [ModuleProject] for the given file, or `null` if cannot be found.
*/
fun findModuleForFile(file: Path, checkExistance: Boolean): ModuleProject? {
return findModuleForFile(file.toFile(), checkExistance)
}
/**
* Check if any of the module projects contain the given [file] in their source folder.
*
* @param file The file to check.
* @return `true` if the given [file] is a source file in any of the mdoules, `false` otherwise.
*/
fun containsSourceFile(file: Path): Boolean
/**
* Checks if the given file is a resource file in any of the included Android modules.
*
* @param file The file to check.
* @return `true` if the given file is a resource file in any of the Android modules, `false` otherwise.
*/
fun isAndroidResource(file: File): Boolean
fun requireWorkspace(): IWorkspace = getWorkspace() ?: throw IWorkspace.NotConfiguredException()
/**
* Notify the project manager that the given <code>file</code> was created.
@@ -0,0 +1,131 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects
import com.android.builder.model.v2.models.ProjectSyncIssues
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import java.io.File
import java.nio.file.Path
/**
* Workspace represents everything related to the project opened in the IDE.
*
* @author Akash Yadav
*/
interface IWorkspace {
// TODO: Add support for composite (included) Gradle builds
/**
* Get the project directory for the workspace. This is usually the root project directory.
*/
fun getProjectDir(): File
/**
* Get the root project model.
*/
fun getRootProject(): GradleProject
/**
* Get the subprojects included in the root project.
*/
fun getSubProjects(): List<GradleProject>
/**
* Get the issues that were encountered while synchronizing the project with Gradle files.
*/
fun getProjectSyncIssues(): ProjectSyncIssues
/**
* Get the build variants that are selected (configured/synchronized) for each of the Android modules
* in this workspace. The keys in the returned map are the paths of the module projects and the values
* are the information about the project's variants.
*/
fun getAndroidVariantSelections(): Map<String, BuildVariantInfo>
/**
* Finds the project by the given path.
*
* @return The project with the given path or `null` if no project is available with that path.
*/
fun findProject(path: String): GradleProject?
/**
* Get the project with the given project path.
*
* @param path The project path.
* @return The project with the given path.
* @throws ProjectNotFoundException If the project could not be found.
*/
fun getProject(path: String): GradleProject =
findProject(path) ?: throw ProjectNotFoundException(path)
/**
* List all the [AndroidModule]s in this project. If this project is itself an Android module,
* then it also added to the list at index `0`.
*
* @return A sequence of android modules.
*/
fun androidProjects(): Sequence<AndroidModule>
/**
* Find the module project for the given file path.
*
* @param file The file to find the module for.
* @param checkExistance Whether to check if the file exists or not.
* @return The [ModuleProject] if found, `null` otherwise.
*/
fun findModuleForFile(file: Path, checkExistance: Boolean = false): ModuleProject?
/**
* Find the module project for the given file path.
*
* @param file The file to find the module for.
* @param checkExistance Whether to check if the file exists or not.
* @return The [ModuleProject] if found, `null` otherwise.
*/
fun findModuleForFile(file: File, checkExistance: Boolean = false): ModuleProject?
/**
* Check if any of the module projects contain the given [file] in their source folder.
*
* @param file The file to check.
* @return `true` if the given [file] is a source file in any of the mdoules, `false` otherwise.
*/
fun containsSourceFile(file: Path): Boolean
/**
* Checks if the given file is a resource file in any of the included Android modules.
*
* @param file The file to check.
* @return `true` if the given file is a resource file in any of the Android modules, `false` otherwise.
*/
fun isAndroidResource(file: File): Boolean
/**
* Thrown by [IWorkspace] if the project with a given path could not be found in the workspace.
*/
class ProjectNotFoundException(path: String) :
RuntimeException("Could not find project with path: $path")
/**
* Thrown by [IProjectManager] when trying to access the workspace and it is not configured yet.
*/
class NotConfiguredException() : RuntimeException("Workspace not configured")
}
@@ -1,122 +0,0 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects
import com.android.builder.model.v2.ide.ProjectType
import com.android.builder.model.v2.models.ProjectSyncIssues
import com.itsaky.androidide.projects.android.AndroidModule
import java.io.File
import java.nio.file.Path
/**
* Model for representing the whole project that is opened in the IDE (including the root project).
*
* @property rootProject The root Gradle project.
* @property subProjects List of all project that are included the project.
* @property projectSyncIssues The issues that occurred while syncing the project.
* @author Akash Yadav
*/
data class Project(
val rootProject: GradleProject,
val subProjects: List<GradleProject>,
val projectSyncIssues: ProjectSyncIssues
) {
/**
* Finds the project by the given path.
*
* @return The project with the given path or `null` if no project is available with that path.
*/
fun findByPath(path: String): GradleProject? {
return this.subProjects.find { it.path == path }
}
/**
* Find the first [AndroidModule] in this project. If this project is itself an Android module,
* return `this` instance.
*
* @return The first Android module.
*/
fun findFirstAndroidModule(): AndroidModule? {
val android = this.subProjects.find { it is AndroidModule }
if (android != null) {
return android as AndroidModule
}
return null
}
/**
* Find the first [AndroidModule] in this project whose `projectType` is
* [com.android.builder.model.v2.ide.ProjectType.APPLICATION]. If this project is itself an
* Android app module, return `this` instance.
*
* @return The first Android module.
*/
fun findFirstAndroidAppModule(): AndroidModule? {
val app = this.subProjects.find { it is AndroidModule && it.projectType == ProjectType.APPLICATION }
if (app != null) {
return app as AndroidModule
}
return null
}
/**
* Finds all the [AndroidModule]s in this project. If this project is itself an Android module,
* then it also added to the list at index `0`.
*
* @return The list of android modules.
*/
fun findAndroidModules(): List<AndroidModule> {
return subProjects.filterIsInstance(AndroidModule::class.java).toMutableList()
}
fun findModuleForFile(file: Path): ModuleProject? {
return findModuleForFile(file.toFile())
}
@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
for (module in subProjects) {
if (module !is ModuleProject) {
continue
}
val moduleDir = module.projectDir.path
if (path.startsWith(moduleDir) && longestPath.length < moduleDir.length) {
longestPath = moduleDir
moduleWithLongestPath = module
}
}
if (longestPath.isEmpty() || moduleWithLongestPath == null) {
return null
}
return moduleWithLongestPath
}
}
@@ -31,8 +31,8 @@ import com.itsaky.androidide.builder.model.DefaultSourceSetContainer
import com.itsaky.androidide.builder.model.DefaultViewBindingOptions
import com.itsaky.androidide.builder.model.UNKNOWN_PACKAGE
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.IWorkspace
import com.itsaky.androidide.projects.ModuleProject
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.tooling.api.ProjectType.Android
import com.itsaky.androidide.tooling.api.models.BasicAndroidVariantMetadata
import com.itsaky.androidide.tooling.api.models.GradleTask
@@ -111,16 +111,21 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
val variants: List<BasicAndroidVariantMetadata> = listOf(),
val configuredVariant: BasicAndroidVariantMetadata?,
val classesJar: File?
) :
ModuleProject(
name,
description,
path,
projectDir,
buildDir,
buildScript,
tasks
) {
) : ModuleProject(
name, description, path, projectDir, buildDir, buildScript, tasks
) {
/**
* Whether this project is an Android library project.
*/
val isLibrary: Boolean
get() = this.projectType == ProjectType.LIBRARY
/**
* Whether this project is an Android application project.
*/
val isApplication: Boolean
get() = this.projectType == ProjectType.APPLICATION
companion object {
@@ -166,8 +171,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
override fun getSourceDirectories(): Set<File> {
if (mainSourceSet == null) {
log.warn(
"No main source set is available for project {}. Cannot get source directories.",
name
"No main source set is available for project {}. Cannot get source directories.", name
)
return mutableSetOf()
}
@@ -202,7 +206,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
}
override fun getCompileClasspaths(): Set<File> {
val project = IProjectManager.getInstance().rootProject ?: return emptySet()
val project = IProjectManager.getInstance().getWorkspace() ?: return emptySet()
val result = mutableSetOf<File>()
result.addAll(getModuleClasspaths())
@@ -210,11 +214,11 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
return result
}
private fun collectLibraries(root: Project, libraries: Set<String>, result: MutableSet<File>) {
private fun collectLibraries(root: IWorkspace, libraries: Set<String>, result: MutableSet<File>) {
for (library in libraries) {
val lib = this.libraryMap[library] ?: continue
if (lib.type == PROJECT) {
val module = root.findByPath(lib.projectInfo!!.projectPath) ?: continue
val module = root.findProject(lib.projectInfo!!.projectPath) ?: continue
if (module !is ModuleProject) {
continue
}
@@ -231,7 +235,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
}
override fun getCompileModuleProjects(): List<ModuleProject> {
val root = IProjectManager.getInstance().rootProject ?: return emptyList()
val root = IProjectManager.getInstance().getWorkspace() ?: return emptyList()
val result = mutableListOf<ModuleProject>()
for (library in this.libraries) {
@@ -240,7 +244,7 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
continue
}
val module = root.findByPath(lib.projectInfo!!.projectPath) ?: continue
val module = root.findProject(lib.projectInfo!!.projectPath) ?: continue
if (module !is ModuleProject) {
continue
}
@@ -362,27 +366,19 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
fun getDependencyResourceTables(): Set<ResourceTable> {
return mutableSetOf<ResourceTable>().also {
var deps: Int
it.addAll(
libraryMap.values
.filter { library ->
library.type == ANDROID_LIBRARY &&
library.androidLibraryData!!.resFolder.exists() &&
library.findPackageName() != UNKNOWN_PACKAGE
}
.also { libs -> deps = libs.size }
.mapNotNull { library ->
ResourceTableRegistry.getInstance()
.let { registry ->
registry.isLoggingEnabled = false
registry.forPackage(
library.packageName,
library.androidLibraryData!!.resFolder,
).also {
registry.isLoggingEnabled = true
}
it.addAll(libraryMap.values.filter { library ->
library.type == ANDROID_LIBRARY && library.androidLibraryData!!.resFolder.exists() && library.findPackageName() != UNKNOWN_PACKAGE
}.also { libs -> deps = libs.size }.mapNotNull { library ->
ResourceTableRegistry.getInstance().let { registry ->
registry.isLoggingEnabled = false
registry.forPackage(
library.packageName,
library.androidLibraryData!!.resFolder,
).also {
registry.isLoggingEnabled = true
}
}
)
}
})
log.info("Created {} resource tables for {} dependencies of module '{}'", it.size, deps, path)
}
@@ -409,19 +405,17 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
* @param pck The package to look for.
*/
fun findAllResourceTableForPackage(
pck: String,
hasGroup: AaptResourceType? = null
pck: String, hasGroup: AaptResourceType? = null
): List<ResourceTable> {
if (pck == SdkConstants.ANDROID_PKG) {
return getFrameworkResourceTable()?.let { listOf(it) } ?: emptyList()
}
val tables: List<ResourceTable> =
mutableListOf<ResourceTable>().apply {
getResourceTable()?.let { add(it) }
addAll(getSourceResourceTables())
addAll(getDependencyResourceTables())
}
val tables: List<ResourceTable> = mutableListOf<ResourceTable>().apply {
getResourceTable()?.let { add(it) }
addAll(getSourceResourceTables())
addAll(getDependencyResourceTables())
}
val result = mutableListOf<ResourceTable>()
for (table in tables) {
@@ -495,7 +489,8 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
fun getSelectedVariant(): BasicAndroidVariantMetadata? {
val projectManager = IProjectManager.getInstance()
val info = projectManager.androidBuildVariants[this.path]
val info =
(projectManager.getWorkspace()?.getAndroidVariantSelections() ?: emptyMap())[this.path]
if (info == null) {
log.error(
"Failed to find selected build variant for module: '{}'", this.path
@@ -520,4 +515,4 @@ open class AndroidModule( // Class must be open because BaseXMLTest mocks this..
* @return The Android SDK platform directory for this Android module, or `null` if none is found.
*/
fun getPlatformDir() = bootClassPaths.firstOrNull { it.name == "android.jar" }?.parentFile
}
}
@@ -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.projects.android
import com.itsaky.androidide.projects.IWorkspace
/**
* Get a sequence of all Android application projects in this workspace.
*/
fun IWorkspace.androidAppProjects() = androidProjects().filterIsAndroidApp()
/**
* Get a sequence of all Android library projects in this workspace.
*/
fun IWorkspace.androidLibraryProjects() = androidProjects().filterIsAndroidLib()
/**
* Returns a sequence containing only Android application projects.
*/
fun Sequence<AndroidModule>.filterIsAndroidApp() = filter(AndroidModule::isApplication)
/**
* Returns a sequence containing only Android library projects.
*/
fun Sequence<AndroidModule>.filterIsAndroidLib() = filter(AndroidModule::isApplication)
@@ -31,15 +31,14 @@ import com.itsaky.androidide.eventbus.events.project.ProjectInitializedEvent
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.CachingProject
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.IWorkspace
import com.itsaky.androidide.projects.ModuleProject
import com.itsaky.androidide.projects.R
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.ModuleProject
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.ProjectType
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.utils.DocumentUtils
@@ -58,8 +57,6 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.slf4j.LoggerFactory
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.Locale
import kotlin.io.path.extension
import kotlin.io.path.isDirectory
@@ -74,52 +71,53 @@ import kotlin.io.path.pathString
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class ProjectManagerImpl : IProjectManager, EventReceiver {
lateinit var projectPath: String
private var _workspace: WorkspaceImpl? = null
private var _projectDir: File? = null
var projectInitialized: Boolean = false
var cachedInitResult: InitializeResult? = null
override var rootProject: Project? = null
private set
override var androidBuildVariants: Map<String, BuildVariantInfo> = emptyMap()
private set
override val projectDirPath: String
get() = projectPath
override val projectDir: File
get() = checkNotNull(_projectDir) {
"Cannot get project directory. Path has not been set."
}
override val projectSyncIssues: ProjectSyncIssues?
get() = rootProject?.projectSyncIssues
get() = getWorkspace()?.getProjectSyncIssues()
companion object {
private val log = LoggerFactory.getLogger(ProjectManagerImpl::class.java)
override fun getWorkspace(): IWorkspace? {
return _workspace
}
@JvmStatic
fun getInstance(): ProjectManagerImpl = IProjectManager.getInstance() as ProjectManagerImpl
override fun openProject(directory: File) {
// IMP: Always use canonical path
this._projectDir = directory.canonicalFile
}
override suspend fun setupProject(project: IProject) {
this.rootProject = withStopWatch("Transform project proxy") {
this._workspace = withStopWatch("Transform project proxy") {
withContext(Dispatchers.IO) {
ProjectTransformer().transform(CachingProject(project))
WorkspaceModelBuilder.build(projectDir, CachingProject(project))
}
}
val rootProject = this.rootProject ?: return
val rootProject = this.getWorkspace() ?: return
// build variants must be updated before the sources and classpaths are indexed
updateBuildVariants { buildVariants ->
androidBuildVariants = buildVariants
_workspace!!.setVariantSelections(buildVariants)
}
log.info(
"Found {} project sync issues: {}", rootProject.projectSyncIssues.syncIssues.size,
rootProject.projectSyncIssues.syncIssues
"Found {} project sync issues: {}",
rootProject.getProjectSyncIssues().syncIssues.size,
rootProject.getProjectSyncIssues().syncIssues
)
withStopWatch("Setup project") {
val indexerScope = CoroutineScope(Dispatchers.Default)
val modulesFlow = flow {
rootProject.subProjects.filterIsInstance<ModuleProject>().forEach {
rootProject.getSubProjects().filterIsInstance<ModuleProject>().forEach {
emit(it)
}
}
@@ -138,71 +136,15 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
}
}
override fun getAndroidModules(): List<AndroidModule> {
val rootProject = this.rootProject ?: return emptyList()
return rootProject.subProjects.mapNotNull { module ->
if (module.type != ProjectType.Android) {
return@mapNotNull null
}
return@mapNotNull module as AndroidModule
}
}
override fun getAndroidAppModules(): List<AndroidModule> {
return getAndroidModules().filter { it.projectType == com.android.builder.model.v2.ide.ProjectType.APPLICATION }
}
override fun getAndroidLibraryModules(): List<AndroidModule> {
return getAndroidModules().filter { it.projectType == com.android.builder.model.v2.ide.ProjectType.LIBRARY }
}
override fun findModuleForFile(file: File, checkExistance: Boolean): ModuleProject? {
if (!isInitialized()) {
return null
}
return this.rootProject!!.findModuleForFile(file, checkExistance)
}
override fun containsSourceFile(file: Path): Boolean {
if (!isInitialized()) {
return false
}
if (!Files.exists(file)) {
return false
}
for (module in this.rootProject!!.subProjects) {
if (module !is ModuleProject) {
continue
}
val source = module.compileJavaSourceClasses.findSource(file)
if (source != null) {
return true
}
}
return false
}
override fun isAndroidResource(file: File): Boolean {
val module = findModuleForFile(file) ?: return false
if (module is AndroidModule) {
return module.getResourceDirectories().find { file.path.startsWith(it.path) } != null
}
return true
}
override fun destroy() {
log.info("Destroying project manager")
this.rootProject = null
this._workspace?.setVariantSelections(emptyMap())
this._workspace = null
this._projectDir = null
this.cachedInitResult = null
this.projectInitialized = false
(this.androidBuildVariants as? MutableMap?)?.clear()
}
@JvmOverloads
@@ -223,7 +165,7 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
return
}
val tasks = getAndroidModules().flatMap { module ->
val tasks = getWorkspace()?.androidProjects()?.flatMap { module ->
val variant = module.getSelectedVariant()
if (variant == null) {
log.error(
@@ -243,28 +185,25 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
if (module.viewBindingOptions.isEnabled) "dataBindingGenBaseClasses$variantNameCapitalized" else null,
"process${variantNameCapitalized}Resources"
).mapNotNull { it?.let { "${module.path}:${it}" } }
}
}?.toList() ?: emptyList()
builder
.executeTasks(*tasks.toTypedArray())
.whenComplete { result, taskErr ->
if (result == null || !result.isSuccessful || taskErr != null) {
log.warn(
"Execution for tasks failed: {} {}", tasks,
taskErr ?: ""
)
} else {
notifyProjectUpdate()
}
builder.executeTasks(*tasks.toTypedArray()).whenComplete { result, taskErr ->
if (result == null || !result.isSuccessful || taskErr != null) {
log.warn(
"Execution for tasks failed: {} {}", tasks, taskErr ?: ""
)
} else {
notifyProjectUpdate()
}
}
}
fun notifyProjectUpdate() {
executeAsync {
rootProject?.apply {
subProjects.forEach { subproject ->
getWorkspace()?.apply {
getSubProjects().forEach { subproject ->
if (subproject is ModuleProject) {
subproject.indexSources()
}
@@ -272,26 +211,25 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
}
val event = ProjectInitializedEvent()
event.put(Project::class.java, rootProject)
event.put(IWorkspace::class.java, getWorkspace())
EventBus.getDefault().post(event)
}
}
private fun updateBuildVariants(onUpdated: (Map<String, BuildVariantInfo>) -> Unit = {}) {
val rootProject = checkNotNull(this.rootProject) {
val rootProject = checkNotNull(this.getWorkspace()) {
"Cannot update build variants. Root project model is null."
}
val buildVariants = mutableMapOf<String, BuildVariantInfo>()
rootProject.subProjects.forEach { subproject ->
rootProject.getSubProjects().forEach { subproject ->
if (subproject is AndroidModule) {
// variant names are not expected to be modified
val variantNames = ImmutableList.builder<String>()
.addAll(subproject.variants.map { variant -> variant.name }).build()
val variantName = subproject.configuredVariant?.name
?: IAndroidProject.DEFAULT_VARIANT
val variantName = subproject.configuredVariant?.name ?: IAndroidProject.DEFAULT_VARIANT
buildVariants[subproject.path] =
BuildVariantInfo(subproject.path, variantNames, variantName)
@@ -301,12 +239,10 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
onUpdated(buildVariants)
}
private fun isInitialized() = rootProject != null
private fun generateSourcesIfNecessary(event: FileEvent) {
val builder = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) ?: return
val file = event.file
if (!isAndroidResource(file)) {
if (getWorkspace()?.isAndroidResource(file) != true) {
return
}
@@ -325,16 +261,14 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
return@apply
}
val module = findModuleForFile(this, false) ?: return@apply
val module = getWorkspace()?.findModuleForFile(this, false) ?: return@apply
if (module !is AndroidModule) {
return@apply
}
val isResource =
module.mainSourceSet?.sourceProvider?.resDirectories?.any {
this.pathString.contains(it.path)
}
?: false
val isResource = module.mainSourceSet?.sourceProvider?.resDirectories?.any {
this.pathString.contains(it.path)
} ?: false
if (isResource) {
module.updateResourceTable()
@@ -360,7 +294,7 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
generateSourcesIfNecessary(event)
if (DocumentUtils.isJavaFile(event.file.toPath())) {
findModuleForFile(event.file, false)?.let {
getWorkspace()?.findModuleForFile(event.file, false)?.let {
val sourceRoot = it.findSourceRoot(event.file) ?: return@let
// add the source node entry
@@ -378,9 +312,10 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
// 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, false)
?.compileJavaSourceClasses
?.findSource(event.file.toPath())
getWorkspace()?.findModuleForFile(
event.file,
false
)?.compileJavaSourceClasses?.findSource(event.file.toPath())
?.let { it.parent?.removeChild(it) }
}
}
@@ -394,18 +329,26 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
// 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, false)
?.compileJavaSourceClasses
?.findSource(event.file.toPath())
getWorkspace()?.findModuleForFile(
event.file,
false
)?.compileJavaSourceClasses?.findSource(event.file.toPath())
?.let { it.parent?.removeChild(it) }
}
if (DocumentUtils.isJavaFile(event.newFile.toPath())) {
findModuleForFile(event.newFile, false)?.let {
getWorkspace()?.findModuleForFile(event.newFile, false)?.let {
val sourceRoot = it.findSourceRoot(event.newFile) ?: return@let
// add the new source node entry
it.compileJavaSourceClasses.append(event.newFile.toPath(), sourceRoot)
}
}
}
companion object {
private val log = LoggerFactory.getLogger(ProjectManagerImpl::class.java)
@JvmStatic
fun getInstance(): ProjectManagerImpl = IProjectManager.getInstance() as ProjectManagerImpl
}
}
@@ -0,0 +1,143 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.projects.internal
import com.android.builder.model.v2.models.ProjectSyncIssues
import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableMap
import com.itsaky.androidide.projects.GradleProject
import com.itsaky.androidide.projects.IWorkspace
import com.itsaky.androidide.projects.ModuleProject
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
/**
* Model for representing the whole project that is opened in the IDE (including the root project).
*
* @property rootProject The root Gradle project.
* @property subProjects List of all project that are included the project.
* @property projectSyncIssues The issues that occurred while syncing the project.
* @author Akash Yadav
*/
internal class WorkspaceImpl(
private val projectDir: File,
private val rootProject: GradleProject,
private val subProjects: List<GradleProject>,
private val projectSyncIssues: ProjectSyncIssues
) : IWorkspace {
private val variantSelections = mutableMapOf<String, BuildVariantInfo>()
internal fun setVariantSelections(selections: Map<String, BuildVariantInfo>) {
this.variantSelections.apply {
clear()
putAll(selections)
}
}
override fun getProjectDir(): File {
return this.projectDir
}
override fun getRootProject(): GradleProject {
return this.rootProject
}
override fun getSubProjects(): List<GradleProject> {
return ImmutableList.copyOf(this.subProjects)
}
override fun getProjectSyncIssues(): ProjectSyncIssues {
return this.projectSyncIssues
}
override fun getAndroidVariantSelections(): Map<String, BuildVariantInfo> {
return ImmutableMap.copyOf(this.variantSelections)
}
override fun findProject(path: String): GradleProject? {
return this.subProjects.find { it.path == path }
}
override fun androidProjects(): Sequence<AndroidModule> {
return subProjects.asSequence().filterIsInstance<AndroidModule>()
}
override fun findModuleForFile(file: Path, checkExistance: Boolean): ModuleProject? {
return findModuleForFile(file.toFile(), checkExistance)
}
override fun findModuleForFile(file: File, checkExistance: Boolean): ModuleProject? {
if (!file.exists() && checkExistance) {
return null
}
val path = file.canonicalPath
var longestPath = ""
var moduleWithLongestPath: ModuleProject? = null
for (module in subProjects) {
if (module !is ModuleProject) {
continue
}
val moduleDir = module.projectDir.canonicalPath
if (path.startsWith(moduleDir) && longestPath.length < moduleDir.length) {
longestPath = moduleDir
moduleWithLongestPath = module
}
}
if (longestPath.isEmpty() || moduleWithLongestPath == null) {
return null
}
return moduleWithLongestPath
}
override fun containsSourceFile(file: Path): Boolean {
if (!Files.exists(file)) {
return false
}
for (module in subProjects) {
if (module !is ModuleProject) {
continue
}
val source = module.compileJavaSourceClasses.findSource(file)
if (source != null) {
return true
}
}
return false
}
override fun isAndroidResource(file: File): Boolean {
val module = findModuleForFile(file, true) ?: return false
if (module is AndroidModule) {
return module.getResourceDirectories().find { file.path.startsWith(it.path) } != null
}
return true
}
}
@@ -17,10 +17,9 @@
package com.itsaky.androidide.projects.internal
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.GradleProject
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.java.JavaModule
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.IGradleProject
import com.itsaky.androidide.tooling.api.IJavaProject
@@ -31,6 +30,7 @@ import com.itsaky.androidide.tooling.api.models.BasicProjectMetadata
import com.itsaky.androidide.tooling.api.models.JavaProjectMetadata
import com.itsaky.androidide.tooling.api.models.params.StringParameter
import org.slf4j.LoggerFactory
import java.io.File
import java.util.concurrent.CopyOnWriteArrayList
/**
@@ -38,13 +38,14 @@ import java.util.concurrent.CopyOnWriteArrayList
*
* @author Akash Yadav
*/
class ProjectTransformer {
internal object WorkspaceModelBuilder {
companion object {
private val log = LoggerFactory.getLogger(ProjectTransformer::class.java)
}
private val log = LoggerFactory.getLogger(WorkspaceModelBuilder::class.java)
fun transform(project: IProject): Project? {
fun build(
projectDir: File,
project: IProject
): WorkspaceImpl? {
try {
val allProjects = project.getProjects().get()
val selectionResult = project.selectProject(StringParameter("")).get()
@@ -56,10 +57,12 @@ class ProjectTransformer {
ProjectType.Gradle -> transform(project.asGradleProject())
ProjectType.Android -> transform(project.asAndroidProject())
else -> throw IllegalStateException(
"Root project must be either an Android project or a Gradle project")
"Root project must be either an Android project or a Gradle project"
)
}
return Project(
return WorkspaceImpl(
projectDir,
rootProject,
CopyOnWriteArrayList(transform(allProjects, project)),
project.getProjectSyncIssues().get()
@@ -107,12 +107,12 @@ class JavaModule(
}
override fun getCompileModuleProjects(): List<ModuleProject> {
val root = IProjectManager.getInstance().rootProject ?: return emptyList()
val workspace = IProjectManager.getInstance().getWorkspace() ?: return emptyList()
return this.dependencies
.filterIsInstance(JavaModuleProjectDependency::class.java)
.filterIsInstance<JavaModuleProjectDependency>()
.filter { it.scope == SCOPE_COMPILE }
.mapNotNull { root.findByPath(it.projectPath) }
.filterIsInstance(ModuleProject::class.java)
.mapNotNull { workspace.findProject(it.projectPath) }
.filterIsInstance<ModuleProject>()
}
fun getDependencyClasspaths(): Set<File> {
@@ -18,13 +18,17 @@
package com.itsaky.androidide.projects.util
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.android.androidAppProjects
/**
* **For testing purposes only!**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@VisibleForTesting
fun findAppModule(): AndroidModule? {
return IProjectManager.getInstance().getAndroidAppModules().firstOrNull { it.path == ":app" }
return IProjectManager.getInstance().getWorkspace()?.androidAppProjects()
?.firstOrNull { it.path == ":app" }
}
@@ -19,11 +19,12 @@ package com.itsaky.androidide.projects.api
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.java.JavaModule
import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.java.JavaModule
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.models.ToolingApiTestLauncherParams
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.utils.FileProvider
import kotlinx.coroutines.runBlocking
@@ -38,24 +39,28 @@ import kotlin.io.path.writeText
/** @author Akash Yadav */
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.DEFAULT_VALUE_STRING)
class ModuleProjectTest {
class ModuleWorkspaceTest {
@Test
fun test() {
ToolingApiTestLauncher.launchServer() {
val params = ToolingApiTestLauncherParams()
ToolingApiTestLauncher.launchServer(params) {
assertThat(result?.isSuccessful).isTrue()
Lookup.getDefault().register(BuildService.KEY_PROJECT_PROXY, project)
val projectManager = IProjectManager.getInstance()
projectManager.openProject(params.projectDir.toFile())
verifyProjectManagerAPIs()
assertThat(project).isNotNull()
val rootProject = IProjectManager.getInstance().rootProject
val rootProject = projectManager.getWorkspace()
assertThat(rootProject).isNotNull()
val root = rootProject!!.rootProject
val root = rootProject!!.getRootProject()
assertThat(root).isNotNull()
assertThat(root.name).isEqualTo("TestApp")
@@ -63,7 +68,7 @@ class ModuleProjectTest {
assertThat(rootDir).isNotNull()
assertThat(Files.isSameFile(rootDir.toPath(), FileProvider.testProjectRoot())).isTrue()
val app = rootProject.findByPath(":app")
val app = rootProject.findProject(":app")
assertThat(app).isNotNull()
assertThat(app).isInstanceOf(AndroidModule::class.java)
@@ -79,7 +84,7 @@ class ModuleProjectTest {
assertThat(app.compilerSettings.javaBytecodeVersion)
.isEqualTo(app.compilerSettings.targetCompatibility)
val anotherAndroidLib = rootProject.findByPath(":another-android-library")
val anotherAndroidLib = rootProject.findProject(":another-android-library")
assertThat(anotherAndroidLib).isNotNull()
assertThat(anotherAndroidLib).isInstanceOf(AndroidModule::class.java)
@@ -154,7 +159,7 @@ class ModuleProjectTest {
.hasSize(3)
}
val javaLibrary = rootProject.findByPath(":java-library")
val javaLibrary = rootProject.findProject(":java-library")
assertThat(javaLibrary).isNotNull()
assertThat(javaLibrary).isInstanceOf(JavaModule::class.java)
@@ -181,7 +186,7 @@ class ModuleProjectTest {
}
}
rootProject.findByPath(":another-java-library").run {
rootProject.findProject(":another-java-library").run {
assertThat(this).isInstanceOf(JavaModule::class.java)
assertThat((this as JavaModule).compilerSettings).isNotNull()
assertThat(compilerSettings.javaSourceVersion).isEqualTo("1.8")
@@ -207,19 +212,18 @@ class ModuleProjectTest {
Files.delete(testCls_renamed)
}
val projectManager = IProjectManager.getInstance()
runBlocking { projectManager.setupProject() }
runBlocking { IProjectManager.getInstance().setupProject() }
val rootProject = IProjectManager.getInstance().rootProject
assertThat(rootProject).isNotNull()
val workspace = IProjectManager.getInstance().getWorkspace()
assertThat(workspace).isNotNull()
val root = rootProject?.rootProject
val root = workspace?.getRootProject()
assertThat(root).isNotNull()
assertThat(root!!.path).isEqualTo(":")
val source =
root.projectDir.toPath().resolve("app/src/main/java/com/itsaky/test/app/MainActivity.java")
val module = rootProject.findModuleForFile(source)
val module = workspace.findModuleForFile(source)
assertThat(module).isNotNull()
assertThat(module!!.path).isEqualTo(":app")
assertThat(module.projectDir.path).isEqualTo(File(root.projectDir, "app").path)
@@ -234,30 +238,30 @@ class ModuleProjectTest {
assertThat(Files.isSameFile(source, sourceNode.file)).isTrue()
// make sure the files are not indexed
assertThat(IProjectManager.getInstance().containsSourceFile(testCls)).isFalse()
assertThat(IProjectManager.getInstance().containsSourceFile(testCls_renamed)).isFalse()
assertThat(workspace.containsSourceFile(testCls)).isFalse()
assertThat(workspace.containsSourceFile(testCls_renamed)).isFalse()
testCls.writeText("public class Test { }")
projectManager.notifyFileCreated(testCls.toFile())
assertThat(IProjectManager.getInstance().containsSourceFile(testCls)).isTrue()
assertThat(IProjectManager.getInstance().containsSourceFile(testCls_renamed)).isFalse()
assertThat(IProjectManager.getInstance().findModuleForFile(testCls, true)).isEqualTo(module)
assertThat(IProjectManager.getInstance().findModuleForFile(testCls_renamed, true)).isNull()
IProjectManager.getInstance().notifyFileCreated(testCls.toFile())
assertThat(workspace.containsSourceFile(testCls)).isTrue()
assertThat(workspace.containsSourceFile(testCls_renamed)).isFalse()
assertThat(workspace.findModuleForFile(testCls, true)).isEqualTo(module)
assertThat(workspace.findModuleForFile(testCls_renamed, true)).isNull()
Files.move(testCls, testCls_renamed)
projectManager.notifyFileRenamed(testCls.toFile(), testCls_renamed.toFile())
assertThat(IProjectManager.getInstance().containsSourceFile(testCls)).isFalse()
assertThat(IProjectManager.getInstance().containsSourceFile(testCls_renamed)).isTrue()
assertThat(IProjectManager.getInstance().findModuleForFile(testCls, true)).isNull()
assertThat(IProjectManager.getInstance().findModuleForFile(testCls_renamed, true)).isEqualTo(
IProjectManager.getInstance().notifyFileRenamed(testCls.toFile(), testCls_renamed.toFile())
assertThat(workspace.containsSourceFile(testCls)).isFalse()
assertThat(workspace.containsSourceFile(testCls_renamed)).isTrue()
assertThat(workspace.findModuleForFile(testCls, true)).isNull()
assertThat(workspace.findModuleForFile(testCls_renamed, true)).isEqualTo(
module
)
Files.delete(testCls_renamed)
projectManager.notifyFileDeleted(testCls_renamed.toFile())
assertThat(IProjectManager.getInstance().containsSourceFile(testCls)).isFalse()
assertThat(IProjectManager.getInstance().containsSourceFile(testCls_renamed)).isFalse()
assertThat(IProjectManager.getInstance().findModuleForFile(testCls, true)).isNull()
assertThat(IProjectManager.getInstance().findModuleForFile(testCls_renamed, true)).isNull()
IProjectManager.getInstance().notifyFileDeleted(testCls_renamed.toFile())
assertThat(workspace.containsSourceFile(testCls)).isFalse()
assertThat(workspace.containsSourceFile(testCls_renamed)).isFalse()
assertThat(workspace.findModuleForFile(testCls, true)).isNull()
assertThat(workspace.findModuleForFile(testCls_renamed, true)).isNull()
}
}
@@ -60,8 +60,8 @@ 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.IWorkspace
import com.itsaky.androidide.projects.ModuleProject
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.utils.DocumentUtils
import com.itsaky.androidide.utils.VMUtils
import kotlinx.coroutines.CoroutineScope
@@ -131,10 +131,10 @@ class JavaLanguageServer : ILanguageServer {
this._settings = settings
}
override fun setupWithProject(project: Project) {
override fun setupWorkspace(workspace: IWorkspace) {
LSPEditorActions.ensureActionsMenuRegistered(JavaCodeActionsMenu)
// Once we have project initialized
// Once we have workspace initialized
// Destory the NO_MODULE_COMPILER instance
JavaCompilerService.NO_MODULE_COMPILER.destroy()
@@ -144,15 +144,15 @@ class JavaLanguageServer : ILanguageServer {
// 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
// Maybe this could be improved by using data from the AndroidModule workspace 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) {
for (subModule in workspace.getSubProjects()) {
if (subModule !is ModuleProject || subModule.path == workspace.getRootProject().path) {
continue
}
SourceFileManager.forModule(subModule)
@@ -173,7 +173,8 @@ class JavaLanguageServer : ILanguageServer {
}
completionProvider.reset(
compiler, settings, cachedCompletion) { cachedCompletion: CachedCompletion ->
compiler, settings, cachedCompletion
) { cachedCompletion: CachedCompletion ->
updateCachedCompletion(cachedCompletion)
}
@@ -239,9 +240,9 @@ class JavaLanguageServer : ILanguageServer {
if (!DocumentUtils.isJavaFile(file)) {
return JavaCompilerService.NO_MODULE_COMPILER
}
val root = getInstance().rootProject
val workspace = getInstance().getWorkspace()
?: return JavaCompilerService.NO_MODULE_COMPILER
val module = root.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER
val module = workspace.findModuleForFile(file!!) ?: return JavaCompilerService.NO_MODULE_COMPILER
return JavaCompilerProvider.get(module)
}
@@ -271,7 +272,7 @@ class JavaLanguageServer : ILanguageServer {
// TODO Find an alternative to efficiently update changeDelta in JavaCompilerService instance
JavaCompilerService.NO_MODULE_COMPILER.onDocumentChange(event)
val module = getInstance()
.findModuleForFile(event.changedFile)
.getWorkspace()?.findModuleForFile(event.changedFile, true)
if (module != null) {
val compiler = JavaCompilerProvider.get(module)
compiler.onDocumentChange(event)
@@ -105,7 +105,8 @@ abstract class BaseJavaCodeAction : EditorActionItem {
}
protected fun ActionData.requireCompiler(): JavaCompilerService {
val module = IProjectManager.getInstance().findModuleForFile(requireFile(), false)
val module =
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(requireFile(), false)
requireNotNull(module) {
"Cannot get compiler instance. Unable to find module for file: ${requireFile().name}"
}
@@ -60,7 +60,7 @@ abstract class FieldBasedAction : BaseJavaCodeAction() {
com.itsaky.androidide.models.Range::class.java,
CodeEditor::class.java
) ||
IProjectManager.getInstance().rootProject == null
IProjectManager.getInstance().getWorkspace() == null
) {
markInvisible()
return
@@ -73,7 +73,8 @@ abstract class FieldBasedAction : BaseJavaCodeAction() {
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()
val module =
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false) ?: return Any()
return JavaCompilerProvider.get(module).compile(file).get { task ->
val triple = findFields(task, file, range)
@@ -67,7 +67,7 @@ class AddImportAction : BaseJavaCodeAction() {
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false)
?: run {
markInvisible()
return
@@ -99,7 +99,7 @@ class AddImportAction : BaseJavaCodeAction() {
)!!
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false)
?: run {
markInvisible()
return Any()
@@ -140,7 +140,7 @@ class AddImportAction : BaseJavaCodeAction() {
val file = data.requireFile()
val module =
IProjectManager.getInstance().findModuleForFile(file, false)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false)
?: run {
markInvisible()
return
@@ -64,7 +64,9 @@ class AddThrowsAction : BaseJavaCodeAction() {
val diagnostic = data[DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get { task ->
val needsThrow = CodeActionUtils.findMethod(task, diagnostic.range)
@@ -66,7 +66,9 @@ class CreateMissingMethodAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get {
CreateMissingMethod(file, findPosition(it, diagnostic.range.start))
@@ -67,7 +67,9 @@ class FieldToBlockAction : BaseJavaCodeAction() {
override suspend fun execAction(data: ActionData): Any {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val diagnostic = data[DiagnosticItem::class.java]!!
val file = data.requirePath()
@@ -65,7 +65,9 @@ class RemoveClassAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get {
@@ -65,7 +65,9 @@ class RemoveMethodAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get {
@@ -66,7 +66,9 @@ class RemoveUnusedThrowsAction : BaseJavaCodeAction() {
val d = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get { task ->
val notThrown = CodeActionUtils.extractNotThrownExceptionName(d.message)
@@ -65,7 +65,9 @@ class SuppressUncheckedWarningAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get { task ->
val warnedMethod = CodeActionUtils.findMethod(task, diagnostic.range)
@@ -70,7 +70,9 @@ class VariableToStatementAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val path = data.requirePath()
return compiler.compile(path).get {
@@ -82,7 +82,9 @@ class GenerateConstructorAction : FieldBasedAction() {
private fun generateConstructor(data: ActionData, selected: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return
)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
@@ -65,7 +65,9 @@ class GenerateMissingConstructorAction : BaseJavaCodeAction() {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get { task ->
val needsConstructor =
@@ -89,7 +89,9 @@ class GenerateSettersAndGettersAction : FieldBasedAction() {
private fun generateForFields(data: ActionData, names: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return
)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
@@ -84,7 +84,9 @@ class GenerateToStringMethodAction : FieldBasedAction() {
private fun generateToString(data: ActionData, selected: MutableSet<String>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return
)
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
@@ -95,7 +95,9 @@ class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return Any()
)
val file = data.requirePath()
return compiler.compile(file).get { task ->
@@ -203,7 +205,9 @@ class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
private fun overrideMethods(data: ActionData, checkedMethods: MutableList<MethodPtr>) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return
)
val file = data.requirePath()
compiler.compile(file).run { task ->
@@ -270,7 +274,9 @@ class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
) {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(data.requireFile(), false)
?: return
)
val editor = data[CodeEditor::class.java]!!
val file = data.requirePath()
val text = editor.text
@@ -41,7 +41,8 @@ abstract class AdvancedJavaEditHandler(protected val file: Path) : BaseJavaEditH
index: Int
) {
val compiler = JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(file, false) ?: return)
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false) ?: return
)
performEdits(compiler, editor, item)
executeCommand(editor, item.command)
@@ -19,7 +19,7 @@ package com.itsaky.androidide.lsp.java.indexing.internal
import com.google.auto.service.AutoService
import com.itsaky.androidide.indexing.IIndexService
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.IWorkspace
import java.io.File
/**
@@ -31,12 +31,12 @@ internal class JavaIndexServiceImpl : IIndexService {
override val displayName: String
get() = "Java LSP Indexing Service"
override fun scanFiles(project: Project): Collection<File> {
override fun scanFiles(workspace: IWorkspace): Collection<File> {
return emptySet()
}
override suspend fun indexFiles(
project: Project,
workspace: IWorkspace,
files: Collection<File>
) {
}
@@ -23,12 +23,17 @@ import com.itsaky.androidide.models.Position;
import com.itsaky.androidide.models.Range;
import com.itsaky.androidide.projects.IProjectManager;
import com.itsaky.androidide.projects.ModuleProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@@ -42,8 +47,6 @@ 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 {
@@ -78,20 +81,23 @@ public class Parser {
* Create a task that compiles a single file
*/
private static JavacTask singleFileTask(JavaFileObject file) {
final ModuleProject module = IProjectManager.getInstance()
.findModuleForFile(Paths.get(file.toUri()));
final var workspace = IProjectManager.getInstance().getWorkspace();
final var module = workspace != null
? workspace.findModuleForFile(Paths.get(file.toUri()), false)
: null;
if (module != null) {
FILE_MANAGER = SourceFileManager.forModule(module);
}
return (JavacTask)
COMPILER.getTask(
null,
FILE_MANAGER,
Parser::ignoreError,
Collections.emptyList(),
Collections.emptyList(),
Collections.singletonList(file));
COMPILER.getTask(
null,
FILE_MANAGER,
Parser::ignoreError,
Collections.emptyList(),
Collections.emptyList(),
Collections.singletonList(file));
}
private static void ignoreError(jdkx.tools.Diagnostic<? extends JavaFileObject> __) {
@@ -215,7 +221,7 @@ public class Parser {
int endCol = (int) lines.getColumnNumber(end);
return new Range(
new Position(startLine - 1, startCol - 1), new Position(endLine - 1, endCol - 1));
new Position(startLine - 1, startCol - 1), new Position(endLine - 1, endCol - 1));
}
private static int indexOf(CharSequence contents, String name, int start) {
@@ -49,7 +49,7 @@ class JavaDiagnosticProvider {
fun analyze(file: Path): DiagnosticResult {
val module = IProjectManager.getInstance().findModuleForFile(file, false)
val module = IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false)
?: return DiagnosticResult.NO_UPDATE
val compiler = JavaCompilerService(module)
@@ -39,12 +39,12 @@ class JavaCompilerProviderTest {
@Test
fun `test module specific compilers`() {
val rootProject = IProjectManager.getInstance().rootProject!!
val appModule = rootProject.findByPath(":app")!! as ModuleProject
val androidLib = rootProject.findByPath(":android-library")!! as ModuleProject
val anotherAndroidLib = rootProject.findByPath(":another-android-library")!! as ModuleProject
val javaLib = rootProject.findByPath(":java-library")!! as ModuleProject
val anotherJavaLib = rootProject.findByPath(":another-java-library")!! as ModuleProject
val workspace = IProjectManager.getInstance().getWorkspace()!!
val appModule = workspace.getProject(":app") as ModuleProject
val androidLib = workspace.getProject(":android-library") as ModuleProject
val anotherAndroidLib = workspace.getProject(":another-android-library") as ModuleProject
val javaLib = workspace.getProject(":java-library") as ModuleProject
val anotherJavaLib = workspace.getProject(":another-java-library") as ModuleProject
val compilers = mutableSetOf<JavaCompilerService>()
for (module in listOf(appModule, androidLib, anotherAndroidLib, javaLib, anotherJavaLib)) {
@@ -38,6 +38,7 @@ import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.models.ToolingApiTestLauncherParams
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IToolingApiServer
import com.itsaky.androidide.utils.Environment
@@ -96,7 +97,8 @@ abstract class LSPTest(
mockkStatic(EditorPreferences::tabSize)
every { EditorPreferences.tabSize } returns 4
ToolingApiTestLauncher.launchServer {
val params = ToolingApiTestLauncherParams()
ToolingApiTestLauncher.launchServer(params) {
assertThat(result?.isSuccessful).isTrue()
@@ -116,13 +118,14 @@ abstract class LSPTest(
throw IllegalStateException("Expected IProjectManager instance to be an EventReceiver")
}
projectManager.openProject(params.projectDir.toFile())
runBlocking { projectManager.setupProject(project) }
// We need to manually setup the language server with the project here
// ProjectManager.notifyProjectUpdate()
ILanguageServerRegistry.getDefault()
.getServer(getServerId())!!
.setupWithProject(projectManager.rootProject!!)
.setupWorkspace(projectManager.getWorkspace()!!)
isInitialized = true
}
@@ -92,12 +92,12 @@ fun processXmlFile(file: File, expectedType: AaptResourceType): Pair<XmlProcesso
throw InflateException("File is not a layout file.")
}
if (IProjectManager.getInstance().rootProject == null) {
if (IProjectManager.getInstance().getWorkspace() == null) {
throw InflateException("GradleProject is not initialized!")
}
val module =
IProjectManager.getInstance().findModuleForFile(file, false) as? AndroidModule
IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false) as? AndroidModule
?: throw InflateException("Cannot find module for given file. Is the project initialized?")
val resFile =
ResourceFile(
@@ -33,7 +33,7 @@ fun startParse(file: File) {
if (isParsing) {
return
}
(IProjectManager.getInstance().findModuleForFile(file, false) as? AndroidModule)?.let {
(IProjectManager.getInstance().getWorkspace()?.findModuleForFile(file, false) as? AndroidModule)?.let {
startParse(it)
}
}
@@ -27,6 +27,7 @@ import com.itsaky.androidide.projects.android.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.projects.util.findAppModule
import com.itsaky.androidide.testing.tooling.ToolingApiTestLauncher
import com.itsaky.androidide.testing.tooling.models.ToolingApiTestLauncherParams
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
import org.robolectric.Robolectric
@@ -43,11 +44,17 @@ object XmlInflaterTest {
return
}
ToolingApiTestLauncher.launchServer {
val params = ToolingApiTestLauncherParams()
ToolingApiTestLauncher.launchServer(params) {
assertThat(result?.isSuccessful).isTrue()
Lookup.getDefault().register(BuildService.KEY_PROJECT_PROXY, project)
runBlocking { IProjectManager.getInstance().setupProject(project) }
val projectManager = IProjectManager.getInstance()
projectManager.openProject(params.projectDir.toFile())
runBlocking { projectManager.setupProject(project) }
init.set(true)
}
}
@@ -41,7 +41,7 @@ import com.itsaky.androidide.lsp.xml.providers.AdvancedEditProvider.onContentCha
import com.itsaky.androidide.lsp.xml.providers.CodeFormatProvider
import com.itsaky.androidide.lsp.xml.providers.XmlCompletionProvider
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.projects.Project
import com.itsaky.androidide.projects.IWorkspace
import com.itsaky.androidide.utils.DocumentUtils
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@@ -81,10 +81,9 @@ class XMLLanguageServer : ILanguageServer {
this.settings = settings
}
override fun setupWithProject(project: Project) {}
override fun setupWorkspace(workspace: IWorkspace) {}
override fun complete(params: CompletionParams?): CompletionResult {
val completionProvider: ICompletionProvider
completionProvider = if (!getSettings().completionsEnabled()) {
val completionProvider: ICompletionProvider = if (!getSettings().completionsEnabled()) {
NoCompletionsProvider()
} else {
XmlCompletionProvider(getSettings())