gradle.kts: solve the linked gradle builds hell

This commit is contained in:
Sergey Rostov
2020-05-01 18:18:39 +03:00
parent 6213c6187d
commit b7e1cd7489
27 changed files with 603 additions and 566 deletions
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.progress.util.BackgroundTaskUtil
@@ -1,144 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionContributor
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.adjustByDefinition
import java.io.File
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.jdkHome
import kotlin.script.experimental.jvm.jvm
sealed class GradleBuildRoot {
abstract val pathPrefix: String
/**
* Add Gradle Project
* for other scripts too
*
* definitely not precompiled scripts (it is detected as in sources roots)
* may be also included scripts not returned by gradle: todo proper notification
*/
class Unlinked(override val pathPrefix: String) : GradleBuildRoot()
abstract class Linked : GradleBuildRoot() {
@Volatile
var importing = false
}
/**
* Notification: please update to Gradle 6.0
* default loader, cases:
* - not loaded: todo: Notification: Load configration to get code insights
* - loaded, not up-to-date: Notifaction: Reload configuraiton
* - loaded, up-to-date: Nothing
*/
class Legacy(override val pathPrefix: String) : Linked()
/**
* not imported:
* Notification: Import Gradle project to get code insights
* during import:
* - todo: disable action on importing. don't miss failed import
* - pause analyzing, todo: change status text to: importing gradle project
*
* todo:
* detect precompiled scripts (in sources roots)
*/
class New(override val pathPrefix: String) : Linked()
// precompiled scripts not detected by gradle
class Imported(
val project: Project,
val dir: VirtualFile,
val context: GradleKtsContext,
val data: GradleImportedBuildRootData
) : Linked() {
override val pathPrefix: String = dir.path
fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
val javaHome = context.javaHome
javaHome?.let { builder.addSdk(it) }
val definitions = ScriptDefinitionContributor.EP_NAME.getExtensions(project)
.filterIsInstance<GradleScriptDefinitionsContributor>()
.single().definitions.toList()
builder.classes.addAll(data.templateClasspath)
data.models.forEach { script ->
val vFile = LocalFileSystem.getInstance().findFileByPath(script.file)
val def = if (vFile != null) {
val src = VirtualFileScriptSource(vFile)
definitions.firstOrNull { it.isScript(src) }
} else null
builder.scripts[script.file] = ScriptInfo(this, def, script)
builder.classes.addAll(script.classPath)
builder.sources.addAll(script.sourcePath)
}
}
class ScriptInfo(
val buildRoot: Imported,
scriptDefinition: ScriptDefinition?,
val model: KotlinDslScriptModel
) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) {
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper? {
val javaHome = buildRoot.context.javaHome
val scriptFile = File(model.file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
if (definition == null) return null
return ScriptCompilationConfigurationWrapper.FromCompilationConfiguration(
VirtualFileScriptSource(virtualFile),
definition.compilationConfiguration.with {
if (javaHome != null) {
jvm.jdkHome(javaHome)
}
defaultImports(model.imports)
dependencies(JvmDependency(model.classPath.map { File(it) }))
ide.dependenciesSources(JvmDependency(model.sourcePath.map { File(it) }))
}.adjustByDefinition(definition)
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ScriptInfo
if (buildRoot.pathPrefix != other.buildRoot.pathPrefix) return false
if (model != other.model) return false
if (definition != other.definition) return false
return true
}
override fun hashCode(): Int {
var result = buildRoot.pathPrefix.hashCode()
result = 31 * result + model.hashCode()
result = 31 * result + definition.hashCode()
return result
}
}
}
}
@@ -38,7 +38,6 @@ private const val kotlinDslNotificationGroupId = "Gradle Kotlin DSL Scripts"
private var Project.notificationPanel: ScriptConfigurationChangedNotification? private var Project.notificationPanel: ScriptConfigurationChangedNotification?
by UserDataProperty<Project, ScriptConfigurationChangedNotification>(Key.create("load.script.configuration.panel")) by UserDataProperty<Project, ScriptConfigurationChangedNotification>(Key.create("load.script.configuration.panel"))
fun showNotificationForProjectImport(project: Project) { fun showNotificationForProjectImport(project: Project) {
val existingPanel = project.notificationPanel val existingPanel = project.notificationPanel
if (existingPanel != null) { if (existingPanel != null) {
@@ -21,19 +21,17 @@ import com.intellij.testFramework.LightVirtualFileBase
import org.jetbrains.kotlin.idea.KotlinIcons import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModelResolver import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModelResolver
import org.jetbrains.kotlin.psi.UserDataProperty import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.plugins.gradle.service.project.GradlePartialResolverPolicy import org.jetbrains.plugins.gradle.service.project.GradlePartialResolverPolicy
import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverExtension
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
import java.util.function.Predicate
fun runPartialGradleImport(project: Project) { fun runPartialGradleImport(project: Project) {
getGradleProjectSettings(project).forEach { getGradleProjectSettings(project).forEach { gradleProjectSettings ->
ExternalSystemUtil.refreshProject( ExternalSystemUtil.refreshProject(
it.externalProjectPath, gradleProjectSettings.externalProjectPath,
ImportSpecBuilder(project, GradleConstants.SYSTEM_ID) ImportSpecBuilder(project, GradleConstants.SYSTEM_ID)
.projectResolverPolicy( .projectResolverPolicy(
GradlePartialResolverPolicy(Predicate<GradleProjectResolverExtension?> { it is KotlinDslScriptModelResolver }) GradlePartialResolverPolicy { it is KotlinDslScriptModelResolver }
) )
) )
} }
@@ -81,6 +79,6 @@ class LoadConfigurationAction : AnAction(
if (virtualFile is LightVirtualFileBase) return false if (virtualFile is LightVirtualFileBase) return false
if (virtualFile == null || !virtualFile.isValid) return false if (virtualFile == null || !virtualFile.isValid) return false
return GradleScriptingSupportProvider.getInstance(project).isConfigurationOutOfDate(virtualFile) return GradleBuildRootsManager.getInstance(project).isConfigurationOutOfDate(virtualFile)
} }
} }
@@ -1,10 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import java.io.File
class GradleKtsContext(val javaHome: File?)
@@ -11,6 +11,7 @@ import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionSourceAsContributor import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionSourceAsContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.core.script.loadDefinitionsFromTemplates import org.jetbrains.kotlin.idea.core.script.loadDefinitionsFromTemplates
@@ -40,6 +41,21 @@ import kotlin.script.experimental.location.ScriptExpectedLocation
import kotlin.script.templates.standard.ScriptTemplateWithArgs import kotlin.script.templates.standard.ScriptTemplateWithArgs
class GradleScriptDefinitionsContributor(private val project: Project) : ScriptDefinitionSourceAsContributor { class GradleScriptDefinitionsContributor(private val project: Project) : ScriptDefinitionSourceAsContributor {
companion object {
fun getDefinitions(project: Project) =
ScriptDefinitionContributor.EP_NAME.getExtensions(project)
.filterIsInstance<GradleScriptDefinitionsContributor>()
.single().definitions.toList()
fun getDefinitionsTemplateClasspath(project: Project): MutableSet<String> =
mutableSetOf<String>().also { result ->
getDefinitions(project).forEach { definition ->
definition.asLegacyOrNull<KotlinScriptDefinitionFromAnnotatedTemplate>()
?.templateClasspath
?.mapNotNullTo(result) { it.path }
}
}
}
override val id: String = "Gradle Kotlin DSL" override val id: String = "Gradle Kotlin DSL"
private val failedToLoad = AtomicBoolean(false) private val failedToLoad = AtomicBoolean(false)
@@ -128,8 +144,8 @@ class GradleScriptDefinitionsContributor(private val project: Project) : ScriptD
private fun kotlinStdlibAndCompiler(gradleLibDir: File): List<File> { private fun kotlinStdlibAndCompiler(gradleLibDir: File): List<File> {
// additionally need compiler jar to load gradle resolver // additionally need compiler jar to load gradle resolver
return gradleLibDir.listFiles { file -> return gradleLibDir.listFiles { file ->
file.name.startsWith("kotlin-compiler-embeddable") || file.name.startsWith("kotlin-stdlib") file.name.startsWith("kotlin-compiler-embeddable") || file.name.startsWith("kotlin-stdlib")
} }
.firstOrNull()?.let(::listOf).orEmpty() .firstOrNull()?.let(::listOf).orEmpty()
} }
@@ -9,6 +9,7 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
class GradleScriptListener(project: Project) : ScriptChangeListener(project) { class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
init { init {
@@ -16,16 +17,20 @@ class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
project.service<GradleScriptInputsWatcher>().startWatching() project.service<GradleScriptInputsWatcher>().startWatching()
} }
override fun isApplicable(vFile: VirtualFile): Boolean { override fun isApplicable(vFile: VirtualFile) = GradleBuildRootsManager.getInstance(project).isApplicable(vFile)
return GradleScriptingSupportProvider.getInstance(project).isApplicable(vFile)
}
override fun editorActivated(vFile: VirtualFile) { override fun editorActivated(vFile: VirtualFile) = checkUpToDate(vFile)
// do nothing
}
override fun documentChanged(vFile: VirtualFile) { override fun documentChanged(vFile: VirtualFile) = checkUpToDate(vFile)
GradleScriptingSupportProvider.getInstance(project).getScriptInfo(vFile)?.model?.inputs fun checkUpToDate(vFile: VirtualFile) {
val upToDate = GradleBuildRootsManager.getInstance(project)
.getScriptInfo(vFile)?.model?.inputs?.isUpToDate(project, vFile) ?: return
if (upToDate) {
hideNotificationForProjectImport(project)
} else {
showNotificationForProjectImport(project)
}
} }
} }
@@ -1,263 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
import org.jetbrains.plugins.gradle.settings.*
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import java.nio.file.Paths
/**
* Manages [roots] gradle build roots:
* - populated after Gradle project sync, by calling [update]
* - stored in FS and loaded in [init]
*/
class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Provider() {
val manager: CompositeScriptConfigurationManager
get() = ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager
private val updater
get() = manager.updater
val roots = RootsIndex()
////////////
/// ScriptingSupport.Provider implementation:
override fun updateScriptDefinitions() {
// nothing related to script definition and project roots are cached
}
override fun isApplicable(file: VirtualFile): Boolean {
val buildRoot = findScriptBuildRoot(file) ?: return false
if (buildRoot is GradleBuildRoot.Legacy) return false
return true
}
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)) {
is GradleBuildRoot.Linked -> root.importing
else -> false
}
}
override fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
roots.values.forEach { root ->
if (root is GradleBuildRoot.Imported) {
root.collectConfigurations(builder)
}
}
}
//////////////////
private val VirtualFile.localPath
get() = path
fun getScriptInfo(file: VirtualFile): GradleBuildRoot.Imported.ScriptInfo? =
manager.getLightScriptInfo(file.localPath) as? GradleBuildRoot.Imported.ScriptInfo
fun findScriptBuildRoot(file: VirtualFile): GradleBuildRoot? {
if (!isGradleKotlinScript(file)) return null
val path = file.localPath
val found = roots.findNearestRoot(path)
if (found != null) return found
return findNewScriptBuildRoot(path)
}
private fun findNewScriptBuildRoot(scriptPath: String): GradleBuildRoot {
val newRoot = updater.update {
loadScriptBuildRoot(scriptPath)
}
roots[newRoot.pathPrefix] = newRoot
return newRoot
}
private fun updateBuildRoot(rootPath: String) {
val settings = getGradleProjectSettings(rootPath) ?: return
val newRoot = loadLinkedRoot(settings)
roots[newRoot.pathPrefix] = newRoot
if (newRoot is GradleBuildRoot.Imported) {
updater.ensureUpdateScheduled()
}
}
private fun loadScriptBuildRoot(scriptPath: String): GradleBuildRoot {
val settings = findNearestGradleProjectSettings(scriptPath) ?: return detectUnlinkedGradleBuildRoot(scriptPath)
return loadLinkedRoot(settings)
}
private fun loadLinkedRoot(settings: GradleProjectSettings) =
tryLoadFromFsCache(settings) ?: createOtherLinkedRoot(settings)
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when {
supported -> GradleBuildRoot.New(settings.externalProjectPath)
else -> GradleBuildRoot.Legacy(settings.externalProjectPath)
}
}
private fun getGradleProjectSettings(workingDir: String): GradleProjectSettings? {
return (ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings)
.getLinkedProjectSettings(workingDir)
}
private fun findNearestGradleProjectSettings(localPath: String): GradleProjectSettings? {
return getGradleProjectSettings(project)
.filter { localPath.startsWith(it.externalProjectPath) }
.maxBy { it.externalProjectPath.length }
}
private fun detectUnlinkedGradleBuildRoot(file: String): GradleBuildRoot.Unlinked =
GradleBuildRoot.Unlinked(file)
init {
getGradleProjectSettings(project).forEach {
roots[it.externalProjectPath] = loadLinkedRoot(it)
}
// subscribe to gradle build unlink
val listener = object : GradleSettingsListenerAdapter() {
override fun onProjectsLinked(settings: MutableCollection<GradleProjectSettings>) {
settings.forEach {
loadLinkedRoot(it)
}
}
override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) {
linkedProjectPaths.forEach {
val buildRoot = VfsUtil.findFile(Paths.get(it), false)
if (buildRoot != null) {
if (roots.remove(buildRoot.localPath) != null) {
KotlinDslScriptModels.remove(buildRoot)
}
}
}
}
override fun onGradleHomeChange(oldPath: String?, newPath: String?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
override fun onGradleDistributionTypeChange(currentValue: DistributionType?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
}
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
}
private fun tryLoadFromFsCache(settings: GradleProjectSettings) =
tryCreateImportedRoot(settings.externalProjectPath) {
KotlinDslScriptModels.read(it)
}
fun markImportingInProgress(workingDir: String) {
val linked = roots.getBuildRoot(workingDir) as? GradleBuildRoot.Linked
if (linked != null) {
linked.importing = true
} else {
val settings = getGradleProjectSettings(workingDir)
if (settings != null) {
val root = createOtherLinkedRoot(settings)
root.importing = true
roots[workingDir] = root
}
}
}
fun update(build: KotlinDslGradleBuildSync) {
val settings = getGradleProjectSettings(build.workingDir) ?: return
(roots.getBuildRoot(build.workingDir) as? GradleBuildRoot.Linked)?.importing = false
if (!kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)) {
roots[build.workingDir] = GradleBuildRoot.Legacy(build.workingDir)
return
}
// fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) {
val root = roots.findNearestRoot(build.workingDir) ?: return
if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return
}
val templateClasspath = findTemplateClasspath(build) ?: return
val data = GradleImportedBuildRootData(templateClasspath, build.models)
val newSupport = tryCreateImportedRoot(build.workingDir) { data } ?: return
KotlinDslScriptModels.write(newSupport.dir, data)
roots[build.workingDir] = newSupport
manager.updater.ensureUpdateScheduled()
}
private fun tryCreateImportedRoot(
externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> GradleImportedBuildRootData?
): GradleBuildRoot.Imported? {
val gradleExeSettings =
ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>(
project,
externalProjectPath,
GradleConstants.SYSTEM_ID
)
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null
val newSupport = GradleBuildRoot.Imported(
project,
buildRoot,
GradleKtsContext(gradleExeSettings.javaHome?.let { File(it) }),
data
)
val oldSupport = roots.findNearestRoot(externalProjectPath)
if (oldSupport != null) {
hideNotificationForProjectImport(project)
}
return newSupport
}
private fun findTemplateClasspath(build: KotlinDslGradleBuildSync): List<String>? {
val anyScript = VfsUtil.findFile(Paths.get(build.models.first().file), true)!!
// todo: find definition according to build.workingDir
val definition = anyScript.findScriptDefinition(project) ?: return null
return definition.asLegacyOrNull<KotlinScriptDefinitionFromAnnotatedTemplate>()
?.templateClasspath?.map { it.path }
}
// used in 201
@Suppress("UNUSED")
fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val script = getScriptInfo(file) ?: return false
return script.model.inputs.isUpToDate(project, file)
}
companion object {
fun getInstance(project: Project): GradleScriptingSupportProvider =
EPN.getPoint(project).extensionList.firstIsInstance()
}
}
@@ -17,6 +17,8 @@ import com.intellij.ui.EditorNotifications
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
@@ -28,8 +30,10 @@ class MissingGradleScriptConfigurationNotificationProvider(private val project:
if (!isGradleKotlinScript(file)) return null if (!isGradleKotlinScript(file)) return null
if (file.fileType != KotlinFileType.INSTANCE) return null if (file.fileType != KotlinFileType.INSTANCE) return null
return when (GradleScriptingSupportProvider.getInstance(project).findScriptBuildRoot(file)) { val scriptUnderRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)
is GradleBuildRoot.Unlinked -> EditorNotificationPanel().apply { val root = scriptUnderRoot?.root
return when {
root is GradleBuildRoot.Unlinked -> EditorNotificationPanel().apply {
text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported")) text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported"))
val linkProjectText = KotlinIdeaGradleBundle.message("action.label.text.load.script.configuration") val linkProjectText = KotlinIdeaGradleBundle.message("action.label.text.load.script.configuration")
@@ -55,7 +59,7 @@ class MissingGradleScriptConfigurationNotificationProvider(private val project:
linkProjectText linkProjectText
) )
} }
is GradleBuildRoot.Linked -> EditorNotificationPanel().apply { root is GradleBuildRoot.New && !root.importing -> EditorNotificationPanel().apply {
text(getMissingConfigurationNotificationText()) text(getMissingConfigurationNotificationText())
createActionLabel(getMissingConfigurationActionText()) { createActionLabel(getMissingConfigurationActionText()) {
runPartialGradleImport(project) runPartialGradleImport(project)
@@ -16,7 +16,7 @@ import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel
import org.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel import org.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptInputsWatcher import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptInputsWatcher
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
@@ -103,6 +103,7 @@ private fun KotlinDslScriptsModel.toListOfScriptModels(project: Project): List<K
} }
class KotlinDslGradleBuildSync(val workingDir: String, val taskId: ExternalSystemTaskId) { class KotlinDslGradleBuildSync(val workingDir: String, val taskId: ExternalSystemTaskId) {
val projectRoots = mutableSetOf<String>()
val models = mutableListOf<KotlinDslScriptModel>() val models = mutableListOf<KotlinDslScriptModel>()
} }
@@ -117,7 +118,10 @@ fun saveScriptModels(project: Project, build: KotlinDslGradleBuildSync) {
build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }.toSet() build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }.toSet()
) )
GradleScriptingSupportProvider.getInstance(project).update(build) // todo: use real info about projects
build.projectRoots.addAll(build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) })
GradleBuildRootsManager.getInstance(project).update(build)
project.service<GradleScriptInputsWatcher>().clearState() project.service<GradleScriptInputsWatcher>().clearState()
} }
@@ -9,7 +9,7 @@ import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType
import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() { class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
override fun onStart(id: ExternalSystemTaskId, workingDir: String?) { override fun onStart(id: ExternalSystemTaskId, workingDir: String?) {
@@ -19,7 +19,7 @@ class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
if (workingDir == null) return if (workingDir == null) return
val project = id.findProject() ?: return val project = id.findProject() ?: return
GradleScriptingSupportProvider.getInstance(project).markImportingInProgress(workingDir) GradleBuildRootsManager.getInstance(project).markImportingInProgress(workingDir)
project.kotlinGradleDslSync[id] = KotlinDslGradleBuildSync(workingDir, id) project.kotlinGradleDslSync[id] = KotlinDslGradleBuildSync(workingDir, id)
} }
@@ -0,0 +1,98 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptDefinitionsContributor
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import java.io.File
/**
* See [GradleBuildRootsManager]
*/
sealed class GradleBuildRoot {
/**
* Add Gradle Project
* for other scripts too
*
* precompiled script
* may be also included scripts not returned by gradle: todo proper notification
*/
class Unlinked() : GradleBuildRoot()
abstract class Linked : GradleBuildRoot() {
@Volatile
var importing = false
abstract val pathPrefix: String
open val projectRoots: Collection<String> get() = listOf()
}
/**
* Notification: please update to Gradle 6.0
* default loader, cases:
* - not loaded: Notification: Load configration to get code insights
* - loaded, not up-to-date: Notifaction: Reload configuraiton
* - loaded, up-to-date: Nothing
*/
class Legacy(override val pathPrefix: String) : Linked()
/**
* not imported:
* Notification: Import Gradle project to get code insights
* during import:
* - disable action on importing. todo: don't miss failed import
* - pause analyzing, todo: change status text to: importing gradle project
*/
class New(override val pathPrefix: String) : Linked()
// precompiled scripts not detected by gradle
class Imported(
val project: Project,
val dir: VirtualFile,
val javaHome: File?,
val data: GradleBuildRootData
) : Linked() {
override val pathPrefix: String = dir.path
override val projectRoots: Collection<String>
get() = data.projectRoots
fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
if (javaHome != null) {
builder.addSdk(javaHome)
}
val definitions = GradleScriptDefinitionsContributor.getDefinitions(project)
builder.classes.addAll(data.templateClasspath)
data.models.forEach { script ->
val definition = selectScriptDefinition(script, definitions)
builder.scripts[script.file] = GradleScriptInfo(this, definition, script)
builder.classes.addAll(script.classPath)
builder.sources.addAll(script.sourcePath)
}
}
private fun selectScriptDefinition(
script: KotlinDslScriptModel,
definitions: List<ScriptDefinition>
): ScriptDefinition? {
val file = LocalFileSystem.getInstance().findFileByPath(script.file) ?: return null
val scriptSource = VirtualFileScriptSource(file)
return definitions.firstOrNull { it.isScript(scriptSource) }
}
}
}
@@ -3,11 +3,12 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle.roots
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
data class GradleImportedBuildRootData( data class GradleBuildRootData(
val templateClasspath: List<String>, val projectRoots: Collection<String>,
val models: List<KotlinDslScriptModel> val templateClasspath: Collection<String>,
val models: Collection<KotlinDslScriptModel>
) )
@@ -3,7 +3,7 @@
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.FileAttribute import com.intellij.openapi.vfs.newvfs.FileAttribute
@@ -11,15 +11,16 @@ import org.jetbrains.kotlin.idea.core.util.readNullable
import org.jetbrains.kotlin.idea.core.util.readString import org.jetbrains.kotlin.idea.core.util.readString
import org.jetbrains.kotlin.idea.core.util.writeNullable import org.jetbrains.kotlin.idea.core.util.writeNullable
import org.jetbrains.kotlin.idea.core.util.writeString import org.jetbrains.kotlin.idea.core.util.writeString
import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import java.io.DataInput import java.io.DataInput
import java.io.DataInputStream import java.io.DataInputStream
import java.io.DataOutput import java.io.DataOutput
internal object KotlinDslScriptModels { internal object GradleBuildRootDataSerializer {
private val attribute = FileAttribute("kotlin-dsl-script-models", 3, false) private val attribute = FileAttribute("kotlin-dsl-script-models", 4, false)
fun read(buildRoot: VirtualFile): GradleImportedBuildRootData? { fun read(buildRoot: VirtualFile): GradleBuildRootData? {
return attribute.readAttribute(buildRoot)?.use { return attribute.readAttribute(buildRoot)?.use {
it.readNullable { it.readNullable {
readKotlinDslScriptModels(it) readKotlinDslScriptModels(it)
@@ -27,7 +28,7 @@ internal object KotlinDslScriptModels {
} }
} }
fun write(buildRoot: VirtualFile, data: GradleImportedBuildRootData?) { fun write(buildRoot: VirtualFile, data: GradleBuildRootData?) {
attribute.writeAttribute(buildRoot).use { attribute.writeAttribute(buildRoot).use {
it.writeNullable(data) { it.writeNullable(data) {
writeKotlinDslScriptModels(this, it) writeKotlinDslScriptModels(this, it)
@@ -40,8 +41,9 @@ internal object KotlinDslScriptModels {
} }
} }
internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleImportedBuildRootData) { internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleBuildRootData) {
val strings = StringsPool.writer(output) val strings = StringsPool.writer(output)
strings.addStrings(data.projectRoots)
strings.addStrings(data.templateClasspath) strings.addStrings(data.templateClasspath)
data.models.forEach { data.models.forEach {
strings.addString(it.file) strings.addString(it.file)
@@ -50,6 +52,7 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleImported
strings.addStrings(it.imports) strings.addStrings(it.imports)
} }
strings.writeHeader() strings.writeHeader()
strings.writeStringIds(data.projectRoots)
strings.writeStringIds(data.templateClasspath) strings.writeStringIds(data.templateClasspath)
output.writeList(data.models) { output.writeList(data.models) {
strings.writeStringId(it.file) strings.writeStringId(it.file)
@@ -61,9 +64,10 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleImported
} }
} }
internal fun readKotlinDslScriptModels(input: DataInputStream): GradleImportedBuildRootData { internal fun readKotlinDslScriptModels(input: DataInputStream): GradleBuildRootData {
val strings = StringsPool.reader(input) val strings = StringsPool.reader(input)
val projectRoots = strings.readStrings()
val templateClasspath = strings.readStrings() val templateClasspath = strings.readStrings()
val models = input.readList { val models = input.readList {
@@ -77,7 +81,7 @@ internal fun readKotlinDslScriptModels(input: DataInputStream): GradleImportedBu
) )
} }
return GradleImportedBuildRootData(templateClasspath, models) return GradleBuildRootData(projectRoots, templateClasspath, models)
} }
private object StringsPool { private object StringsPool {
@@ -0,0 +1,58 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.diagnostic.logger
class GradleBuildRootIndex {
private val log = logger<GradleBuildRootIndex>()
private val byWorkingDir = HashMap<String, GradleBuildRoot.Linked>()
val byProjectDir = HashMap<String, GradleBuildRoot.Linked>()
val values: Collection<GradleBuildRoot>
get() = byWorkingDir.values
@Synchronized
fun rebuildProjectRoots() {
byProjectDir.clear()
byWorkingDir.values.forEach { buildRoot ->
buildRoot.projectRoots.forEach {
byProjectDir[it] = buildRoot
}
}
}
@Synchronized
fun getBuildRoot(dir: String) = byWorkingDir[dir]
@Synchronized
fun findNearestRoot(path: String): GradleBuildRoot.Linked? {
var max: Pair<String, GradleBuildRoot.Linked>? = null
byWorkingDir.entries.forEach {
if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) {
max = it.key to it.value
}
}
return max?.second
}
@Synchronized
fun getBuildByProjectDir(projectDir: String) = byProjectDir[projectDir]
@Synchronized
operator fun set(prefix: String, value: GradleBuildRoot.Linked) {
val old = byWorkingDir.put(prefix, value)
rebuildProjectRoots()
log.info("$prefix: $old -> $value")
}
@Synchronized
fun remove(prefix: String) = byWorkingDir.remove(prefix)?.also {
rebuildProjectRoots()
log.info("$prefix: removed")
}
}
@@ -0,0 +1,280 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.*
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
import org.jetbrains.plugins.gradle.settings.*
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import java.nio.file.Paths
/**
* [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build).
* Each [GradleBuildRoot] may have it's own Gradle version, java home and other settings.
*
* This manager allows to find related Gradle build by the Gradle Kotlin script file path.
* Each imported build have info about all of it's Kotlin Build Scripts.
* It is populated by calling [update], stored in FS and will be loaded from FS on next project opening
*
* [CompositeScriptConfigurationManager] may ask about known scripts by calling [collectConfigurations].
*
* It also used to show related notification and floating actions depending on root kind, state and script state itself.
*
* Roots may be:
* - [GradleBuildRoot.Unlinked] - The script not related to any Gradle build that is linked to IntelliJ Project,
* or we cannot known what is it
* - [GradleBuildRoot.Linked] - Linked project, that may be itself:
* - [GradleBuildRoot.Legacy] - Gradle build with old Gradle version (<6.0)
* - [GradleBuildRoot.New] - not yet imported
* - [GradleBuildRoot.Imported] - imported
*/
class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider() {
val manager: CompositeScriptConfigurationManager
get() = ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager
private val updater
get() = manager.updater
val roots = GradleBuildRootIndex()
////////////
/// ScriptingSupport.Provider implementation:
override fun updateScriptDefinitions() {
// nothing related to script definition and project roots are cached
}
override fun isApplicable(file: VirtualFile): Boolean {
val scriptUnderRoot = findScriptBuildRoot(file) ?: return false
if (scriptUnderRoot.root is GradleBuildRoot.Legacy) return false
return true
}
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)?.root) {
is GradleBuildRoot.Linked -> root.importing
else -> false
}
}
// used in 201
@Suppress("UNUSED")
fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val script = getScriptInfo(file) ?: return false
return !script.model.inputs.isUpToDate(project, file)
}
override fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
roots.values.forEach { root ->
if (root is GradleBuildRoot.Imported) {
root.collectConfigurations(builder)
}
}
}
//////////////////
private val VirtualFile.localPath
get() = path
fun getScriptInfo(file: VirtualFile): GradleScriptInfo? =
manager.getLightScriptInfo(file.localPath) as? GradleScriptInfo
class ScriptUnderRoot(
val root: GradleBuildRoot?,
val script: GradleScriptInfo? = null
)
fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? {
if (!isGradleKotlinScript(gradleKtsFile)) return null
val filePath = gradleKtsFile.path
val scriptInfo = getScriptInfo(gradleKtsFile)
val imported = scriptInfo?.buildRoot
if (imported != null) return ScriptUnderRoot(imported, scriptInfo)
if (gradleKtsFile.name == "build.gradle.kts" ||
gradleKtsFile.name == "settings.gradle.kts" ||
gradleKtsFile.name == "init.gradle.kts"
) {
// build|settings|init.gradle.kts scripts should be located near gradle project root only
val gradleBuild = roots.getBuildByProjectDir(gradleKtsFile.parent.localPath)
if (gradleBuild != null) return ScriptUnderRoot(gradleBuild)
}
// other scripts: "included", "precompiled" scripts, scripts in unlinked projects,
// or just random files with ".gradle.kts" ending
// try find nearest linked legacy gradle project settings first
val found = roots.findNearestRoot(filePath)
if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found)
return ScriptUnderRoot(GradleBuildRoot.Unlinked())
}
fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) {
actualizeBuildRoot(workingDir)?.importing = inProgress
}
fun update(build: KotlinDslGradleBuildSync) {
// fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) {
val root = roots.getBuildRoot(build.workingDir) ?: return
if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return
}
val old = actualizeBuildRoot(build.workingDir) ?: return
old.importing = false
if (old is GradleBuildRoot.Legacy) return
val templateClasspath = GradleScriptDefinitionsContributor.getDefinitionsTemplateClasspath(project)
val data = GradleBuildRootData(build.projectRoots, templateClasspath, build.models)
val newSupport = tryCreateImportedRoot(build.workingDir) { data } ?: return
GradleBuildRootDataSerializer.write(newSupport.dir, data)
roots[build.workingDir] = newSupport
manager.updater.ensureUpdateScheduled()
}
init {
getGradleProjectSettings(project).forEach {
roots[it.externalProjectPath] = loadLinkedRoot(it)
}
// subscribe to linked gradle project modification
val listener = object : GradleSettingsListenerAdapter() {
override fun onProjectsLinked(settings: MutableCollection<GradleProjectSettings>) {
settings.forEach {
loadLinkedRoot(it)
}
}
override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) {
linkedProjectPaths.forEach {
val buildRoot = VfsUtil.findFile(Paths.get(it), false)
if (buildRoot != null) {
if (roots.remove(buildRoot.localPath) != null) {
GradleBuildRootDataSerializer.remove(buildRoot)
}
}
}
}
override fun onGradleHomeChange(oldPath: String?, newPath: String?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
override fun onGradleDistributionTypeChange(currentValue: DistributionType?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
}
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
}
private fun getGradleProjectSettings(workingDir: String): GradleProjectSettings? {
return (ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings)
.getLinkedProjectSettings(workingDir)
}
/**
* Check that root under [workingDir] in sync with it's [GradleProjectSettings].
* Actually this should be true, but we may miss some change events.
* For that cases we are rechecking this on each Gradle Project sync (importing/reimporting)
*/
private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot.Linked? {
val actualSettings = getGradleProjectSettings(workingDir)
val buildRoot = roots.getBuildRoot(workingDir)
return when {
buildRoot != null -> when {
!buildRoot.checkActual(actualSettings) -> updateBuildRoot(workingDir)
else -> buildRoot
}
actualSettings != null -> loadLinkedRoot(actualSettings)
else -> null
}
}
private fun GradleBuildRoot.Linked.checkActual(actualSettings: GradleProjectSettings?): Boolean {
if (actualSettings == null) return false
val knownAsSupported = this !is GradleBuildRoot.Legacy
val shouldBeSupported = kotlinDslScriptsModelImportSupported(actualSettings.resolveGradleVersion().version)
return knownAsSupported == shouldBeSupported
}
private fun updateBuildRoot(rootPath: String): GradleBuildRoot.Linked? {
val settings = getGradleProjectSettings(rootPath)
if (settings == null) {
val removed = roots.remove(rootPath)
if (removed is GradleBuildRoot.Imported) {
updater.ensureUpdateScheduled()
}
return null
} else {
val newRoot = loadLinkedRoot(settings)
roots[newRoot.pathPrefix] = newRoot
updater.ensureUpdateScheduled()
return newRoot
}
}
private fun loadLinkedRoot(settings: GradleProjectSettings) =
tryLoadFromFsCache(settings) ?: createOtherLinkedRoot(settings)
private fun tryLoadFromFsCache(settings: GradleProjectSettings) =
tryCreateImportedRoot(settings.externalProjectPath) {
GradleBuildRootDataSerializer.read(it)
}
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when {
supported -> GradleBuildRoot.New(settings.externalProjectPath)
else -> GradleBuildRoot.Legacy(settings.externalProjectPath)
}
}
private fun tryCreateImportedRoot(
externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> GradleBuildRootData?
): GradleBuildRoot.Imported? {
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null
val javaHome = ExternalSystemApiUtil
.getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID)
.javaHome?.let { File(it) }
val newSupport = GradleBuildRoot.Imported(project, buildRoot, javaHome, data)
val oldSupport = roots.getBuildRoot(externalProjectPath)
if (oldSupport != null) {
hideNotificationForProjectImport(project)
}
return newSupport
}
companion object {
fun getInstance(project: Project): GradleBuildRootsManager =
EPN.getPoint(project).extensionList.firstIsInstance()
}
}
@@ -0,0 +1,66 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.vfs.VfsUtil
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.adjustByDefinition
import java.io.File
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.jdkHome
import kotlin.script.experimental.jvm.jvm
class GradleScriptInfo(
val buildRoot: GradleBuildRoot.Imported,
scriptDefinition: ScriptDefinition?,
val model: KotlinDslScriptModel
) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) {
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper? {
val javaHome = buildRoot.javaHome
val scriptFile = File(model.file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
if (definition == null) return null
return ScriptCompilationConfigurationWrapper.FromCompilationConfiguration(
VirtualFileScriptSource(virtualFile),
definition.compilationConfiguration.with {
if (javaHome != null) {
jvm.jdkHome(javaHome)
}
defaultImports(model.imports)
dependencies(JvmDependency(model.classPath.map { File(it) }))
ide.dependenciesSources(JvmDependency(model.sourcePath.map { File(it) }))
}.adjustByDefinition(definition)
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradleScriptInfo
if (buildRoot.pathPrefix != other.buildRoot.pathPrefix) return false
if (model != other.model) return false
if (definition != other.definition) return false
return true
}
override fun hashCode(): Int {
var result = buildRoot.pathPrefix.hashCode()
result = 31 * result + model.hashCode()
result = 31 * result + definition.hashCode()
return result
}
}
@@ -19,12 +19,9 @@ import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtScriptInitializer import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.plugins.gradle.settings.GradleLocalSettings
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.settings.GradleSettings import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
import org.slf4j.LoggerFactory
import kotlin.collections.HashMap
private val sections = arrayListOf("buildscript", "plugins", "initscript", "pluginManagement") private val sections = arrayListOf("buildscript", "plugins", "initscript", "pluginManagement")
@@ -96,57 +93,5 @@ fun useScriptConfigurationFromImportOnly(): Boolean {
return Registry.`is`("kotlin.gradle.scripts.useIdeaProjectImport", false) return Registry.`is`("kotlin.gradle.scripts.useIdeaProjectImport", false)
} }
fun getGradleVersion(project: Project, settings: GradleProjectSettings): String { fun getGradleProjectSettings(project: Project): Collection<GradleProjectSettings> =
val localVersion = GradleLocalSettings.getInstance(project).getGradleVersion(settings.externalProjectPath) (ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings).linkedProjectsSettings
if (localVersion != null) return localVersion
return settings.resolveGradleVersion().version
}
fun getGradleProjectSettings(project: Project): Collection<GradleProjectSettings> {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings
return gradleSettings.getLinkedProjectsSettings()
}
class RootsIndex {
private val log = LoggerFactory.getLogger(RootsIndex::class.java)
private val buildRoots = HashMap<String, GradleBuildRoot>()
private val scripts = HashMap<String, GradleBuildRoot>()
val values: Collection<GradleBuildRoot>
get() = buildRoots.values
@Synchronized
fun rebuildProjectRoots() {
// values.forEach {
// it.
// }
}
@Synchronized
fun getBuildRoot(dir: String) = buildRoots[dir]
@Synchronized
fun findNearestRoot(path: String): GradleBuildRoot? {
var max: Pair<String, GradleBuildRoot>? = null
buildRoots.entries.forEach {
if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) {
max = it.key to it.value
}
}
return max?.second
}
@Synchronized
operator fun set(prefix: String, value: GradleBuildRoot) {
val old = buildRoots.put(prefix, value)
rebuildProjectRoots()
log.info("{}: {} -> {}", prefix, old, value)
}
@Synchronized
fun remove(prefix: String) = buildRoots.remove(prefix)?.also {
rebuildProjectRoots()
log.info("{}: removed", prefix)
}
}
@@ -10,6 +10,7 @@ import junit.framework.AssertionFailedError
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.applySuggestedScriptConfiguration import org.jetbrains.kotlin.idea.core.script.applySuggestedScriptConfiguration
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.script.configuration.utils.areSimilar import org.jetbrains.kotlin.idea.core.script.configuration.utils.areSimilar
@@ -37,7 +38,7 @@ class GradleKtsImportTest : GradleImportingTestCase() {
fun data(): Collection<Array<Any?>> = listOf(arrayOf<Any?>("6.0.1")) fun data(): Collection<Array<Any?>> = listOf(arrayOf<Any?>("6.0.1"))
} }
val scriptConfigurationManager get() = ScriptConfigurationManager.getInstance(myProject) val scriptConfigurationManager get() = ScriptConfigurationManager.getInstance(myProject) as CompositeScriptConfigurationManager
val projectDir get() = File(GradleSettings.getInstance(myProject).linkedProjectsSettings.first().externalProjectPath) val projectDir get() = File(GradleSettings.getInstance(myProject).linkedProjectsSettings.first().externalProjectPath)
override fun testDataDirName(): String { override fun testDataDirName(): String {
@@ -98,8 +99,8 @@ class GradleKtsImportTest : GradleImportingTestCase() {
// reload configuration and check this it is not changed // reload configuration and check this it is not changed
scripts.forEach { scripts.forEach {
val reloadedConfiguration = scriptConfigurationManager.forceReloadConfiguration( val reloadedConfiguration = scriptConfigurationManager.default.forceReloadConfiguration(
it.virtualFile, it.psiFile,
object : DefaultScriptConfigurationLoader(it.psiFile.project) { object : DefaultScriptConfigurationLoader(it.psiFile.project) {
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition) = false override fun shouldRunInBackground(scriptDefinition: ScriptDefinition) = false
override fun loadDependencies( override fun loadDependencies(
@@ -6,6 +6,9 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootData
import org.jetbrains.kotlin.idea.scripting.gradle.roots.readKotlinDslScriptModels
import org.jetbrains.kotlin.idea.scripting.gradle.roots.writeKotlinDslScriptModels
import org.junit.Test import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -13,11 +16,12 @@ import java.io.DataInputStream
import java.io.DataOutputStream import java.io.DataOutputStream
import kotlin.test.assertEquals import kotlin.test.assertEquals
class KotlinDslScriptModelsTest { class GradleBuildRootDataSerializerTest {
@Test @Test
fun write() { fun write() {
val data = GradleImportedBuildRootData( val data = GradleBuildRootData(
listOf("a", "b", "c"), listOf("a", "b", "c"),
listOf("a"),
listOf( listOf(
KotlinDslScriptModel( KotlinDslScriptModel(
"a", "a",
@@ -233,13 +233,13 @@ open class GradleScriptInputsWatcherTest : AbstractScriptConfigurationLoadingTes
} }
private fun assertConfigurationUpToDate(file: KtFile) { private fun assertConfigurationUpToDate(file: KtFile) {
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file) scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
assertNoBackgroundTasks() assertNoBackgroundTasks()
assertNoLoading() assertNoLoading()
} }
private fun assertConfigurationUpdateWasDone(file: KtFile) { private fun assertConfigurationUpdateWasDone(file: KtFile) {
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file) scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
assertAndDoAllBackgroundTasks() assertAndDoAllBackgroundTasks()
assertSingleLoading() assertSingleLoading()
} }
@@ -43,7 +43,7 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
fun testManualLoadingForUpToDate() { fun testManualLoadingForUpToDate() {
assertConfigurationShouldBeLoadedManually() assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts) scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded() assertConfigurationWasLoaded()
} }
@@ -56,7 +56,7 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
assertNoBackgroundTasks() assertNoBackgroundTasks()
assertNoLoading() assertNoLoading()
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts) scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded() assertConfigurationWasLoaded()
} }
@@ -64,7 +64,7 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
fun testFileAttributesForManualLoading() { fun testFileAttributesForManualLoading() {
assertConfigurationShouldBeLoadedManually() assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts) scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded() assertConfigurationWasLoaded()
@@ -76,12 +76,12 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
fun testNoNotificationAfterForReload() { fun testNoNotificationAfterForReload() {
assertConfigurationShouldBeLoadedManually() assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts) scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded() assertConfigurationWasLoaded()
makeChangesInsideSections() makeChangesInsideSections()
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts) scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded() assertConfigurationWasLoaded()
} }
@@ -1,33 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
class RootsIndexTest {
@Test
fun findRoot() {
val roots = RootsIndex<Int>()
roots["a/b/c"] = 1
assertFails { roots["a/b/c/d"] = 2 }
roots["a/c"] = 3
roots["d"] = 4
assertEquals(1, roots.findNearestRoot("a/b/c/x"))
assertEquals(1, roots.findNearestRoot("a/b/c"))
assertEquals(3, roots.findNearestRoot("a/c/x"))
assertEquals(3, roots.findNearestRoot("a/c"))
assertEquals(4, roots.findNearestRoot("d"))
assertEquals(4, roots.findNearestRoot("d/x"))
assertEquals(null, roots.findNearestRoot("e"))
assertEquals(null, roots.findNearestRoot("e/f"))
roots["d"] = 5
assertEquals(5, roots.findNearestRoot("d"))
}
}
+2 -1
View File
@@ -17,7 +17,8 @@
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptListener"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/> <loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/>
<scriptingSupportProvider implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider"/>
<scriptingSupportProvider implementation="org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager"/>
</extensions> </extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle"> <extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
+5 -4
View File
@@ -14,10 +14,11 @@
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea"> <extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptListener"/>
<scriptingSupportProvider
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider" <loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/>
/>
<scriptingSupportProvider implementation="org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager"/>
</extensions> </extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle"> <extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
@@ -13,6 +13,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.core.script.* import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions
import org.jetbrains.kotlin.idea.core.script.configuration.loader.FileContentsDependentConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.loader.FileContentsDependentConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.testingBackgroundExecutor import org.jetbrains.kotlin.idea.core.script.configuration.testingBackgroundExecutor
@@ -21,7 +22,7 @@ import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurationTest() { abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurationTest() {
lateinit var scriptConfigurationManager: ScriptConfigurationManager lateinit var scriptConfigurationManager: CompositeScriptConfigurationManager
companion object { companion object {
private var occurredLoadings = 0 private var occurredLoadings = 0
@@ -41,7 +42,7 @@ abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurat
testScriptConfigurationNotification = true testScriptConfigurationNotification = true
ApplicationManager.getApplication().isScriptChangesNotifierDisabled = false ApplicationManager.getApplication().isScriptChangesNotifierDisabled = false
scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java) scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java) as CompositeScriptConfigurationManager
} }
override fun tearDown() { override fun tearDown() {
@@ -103,7 +104,7 @@ abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurat
protected fun makeChanges(contents: String, file: KtFile = myFile as KtFile) { protected fun makeChanges(contents: String, file: KtFile = myFile as KtFile) {
changeContents(contents) changeContents(contents)
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file) scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
} }
protected fun changeContents(contents: String, file: PsiFile = myFile) { protected fun changeContents(contents: String, file: PsiFile = myFile) {
@@ -163,10 +163,10 @@ class ScriptConfigurationLoadingTest : AbstractScriptConfigurationLoadingTest()
fun testLoadingForUsagesSearch() { fun testLoadingForUsagesSearch() {
assertAndLoadInitialConfiguration() assertAndLoadInitialConfiguration()
assertTrue(scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(myFile as KtFile))) assertTrue(scriptConfigurationManager.default.ensureConfigurationUpToDate(listOf(myFile as KtFile)))
changeContents("A") changeContents("A")
assertFalse(scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(myFile as KtFile))) assertFalse(scriptConfigurationManager.default.ensureConfigurationUpToDate(listOf(myFile as KtFile)))
} }
fun testReportsOnAutoApply() { fun testReportsOnAutoApply() {