GradleBuildRoot: remove classes nesting

This commit is contained in:
Sergey Rostov
2020-05-29 12:21:44 +03:00
parent d8892ced9d
commit 2ed68643cb
11 changed files with 306 additions and 212 deletions
@@ -10,14 +10,14 @@ 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.cache.CachedConfigurationInputs import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot import org.jetbrains.kotlin.idea.scripting.gradle.roots.*
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
/** /**
* Up to date of gradle script depends on following factors: * Up to date of gradle script depends on following factors:
* 1. It is out of date when essential [sections] are changed. See [getGradleScriptInputsStamp]. * 1. It is out of date when essential [sections] are changed. See [getGradleScriptInputsStamp].
* 2. When some related file is changed (other gradle script, gradle.properties file) * 2. When some related file is changed (other gradle script, gradle.properties file)
* See [GradleBuildRoot.Linked.areRelatedFilesChangedBefore]. * See [GradleBuildRoot.areRelatedFilesChangedBefore].
* *
* [lastModifiedTs] is needed to check if some related file was changed since last update * [lastModifiedTs] is needed to check if some related file was changed since last update
*/ */
@@ -14,6 +14,7 @@ import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications 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.roots.GradleBuildRootsLocator.NotificationKind.*
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
class MissingGradleScriptConfigurationNotificationProvider(private val project: Project) : class MissingGradleScriptConfigurationNotificationProvider(private val project: Project) :
@@ -24,32 +25,63 @@ 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
val scriptUnderRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file) ?: return null val rootsManager = GradleBuildRootsManager.getInstance(project)
return when { val scriptUnderRoot = rootsManager.findScriptBuildRoot(file) ?: return null
scriptUnderRoot.isUnrelatedScript -> EditorNotificationPanel().apply { return when (scriptUnderRoot.notificationKind) {
text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported")) dontCare -> null
outsideAnyting -> EditorNotificationPanel().apply {
createActionLabel(KotlinIdeaGradleBundle.message("action.text.standalone")) { text("Code insight unavailable (related Gradle project not linked)")
GradleBuildRootsManager.getInstance(project).addStandaloneScript(file) // todo: Code insight unavailable (cannot find related Gradle project)
createActionLabel("Link related Gradle project") {
runPartialGradleImport(project)
} }
val helpIcon = createActionLabel("") {}
helpIcon.setIcon(AllIcons.General.ContextHelp)
helpIcon.setUseIconAsLink(true)
helpIcon.toolTipText = KotlinIdeaGradleBundle.message(
"tool.tip.text.the.external.gradle.project.needs.to.be.imported.to.get.this.script.analyzed"
)
} }
scriptUnderRoot.importRequired -> EditorNotificationPanel().apply { wasNotImportedAfterCreation -> EditorNotificationPanel().apply {
text(getMissingConfigurationNotificationText()) text("Code insight unavailable (Gradle project Sync required)")
createActionLabel(getMissingConfigurationActionText()) { createActionLabel(getMissingConfigurationActionText()) {
runPartialGradleImport(project) runPartialGradleImport(project)
} }
} }
else -> null notEvaluatedInLastImport -> EditorNotificationPanel().apply {
text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported"))
createActionLabel(KotlinIdeaGradleBundle.message("action.text.standalone")) {
rootsManager.updateStandaloneScripts {
removeStandaloneScript(file.path)
}
}
contextHelp(
KotlinIdeaGradleBundle.message("tool.tip.text.the.external.gradle.project.needs.to.be.imported.to.get.this.script.analyzed")
)
}
standalone -> EditorNotificationPanel().apply {
text("Standalone script")
createActionLabel("Disable script") {
rootsManager.updateStandaloneScripts {
addStandaloneScript(file.path)
}
}
contextHelp(
"""
<div width=400>\
<p>Configuration for this script will be loaded separately from Gradle project Sync. \
<br/>
<p>This can be expensive for large Gradle projects.</p>\
</div>
"""
)
}
} }
} }
private fun EditorNotificationPanel.contextHelp(text: String) {
val helpIcon = createActionLabel("") {}
helpIcon.setIcon(AllIcons.General.ContextHelp)
helpIcon.setUseIconAsLink(true)
helpIcon.toolTipText = text
}
companion object { companion object {
private val KEY = Key.create<EditorNotificationPanel>("GradleScriptOutOfSourceNotification") private val KEY = Key.create<EditorNotificationPanel>("GradleScriptOutOfSourceNotification")
} }
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.idea.scripting.gradle.roots 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.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
@@ -18,6 +17,10 @@ import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import java.io.File import java.io.File
@set:TestOnly
@get:TestOnly
internal var skipLastModifiedFilesLoading = false
/** /**
* [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build). * [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. * Each [GradleBuildRoot] may have it's own Gradle version, Java home and other settings.
@@ -27,132 +30,114 @@ import java.io.File
* See [GradleBuildRootsManager] for more details. * See [GradleBuildRootsManager] for more details.
*/ */
sealed class GradleBuildRoot { sealed class GradleBuildRoot {
/** @Volatile
* The script not related to any Gradle build that is linked to IntelliJ Project, var importing = false
* or we cannot known what is it
*/
@Suppress("CanSealedSubClassBeObject")
class Unlinked : GradleBuildRoot()
/** abstract val pathPrefix: String
* Linked project, that may be itself: [Legacy], [New] or [Imported].
*/
abstract class Linked : GradleBuildRoot() {
@Volatile
var importing = false
abstract val pathPrefix: String abstract val projectRoots: Collection<String>
abstract val projectRoots: Collection<String> val dir: VirtualFile?
get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix)
val dir: VirtualFile? private lateinit var lastModifiedFiles: LastModifiedFiles
get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix)
private lateinit var lastModifiedFiles: LastModifiedFiles fun loadLastModifiedFiles() {
val loaded = if (!skipLastModifiedFilesLoading) {
val dir = dir
if (dir != null) LastModifiedFiles.read(dir)
else null
} else null
fun loadLastModifiedFiles() { lastModifiedFiles = loaded ?: LastModifiedFiles()
val loaded = if (!skipLastModifiedFilesLoading) {
val dir = dir
if (dir != null) LastModifiedFiles.read(dir)
else null
} else null
lastModifiedFiles = loaded ?: LastModifiedFiles()
}
fun saveLastModifiedFiles() {
LastModifiedFiles.write(dir ?: return, lastModifiedFiles)
}
fun areRelatedFilesChangedBefore(file: VirtualFile, lastModified: Long): Boolean =
lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified
fun fileChanged(filePath: String, ts: Long) {
lastModifiedFiles.fileChanged(ts, filePath)
}
} }
abstract class WithoutScriptModels(settings: GradleProjectSettings) : Linked() { fun saveLastModifiedFiles() {
final override val pathPrefix = settings.externalProjectPath!! LastModifiedFiles.write(dir ?: return, lastModifiedFiles)
final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix)
init {
loadLastModifiedFiles()
}
} }
/** fun areRelatedFilesChangedBefore(file: VirtualFile, lastModified: Long): Boolean =
* Gradle build with old Gradle version (<6.0) lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified
*/
class Legacy(
settings: GradleProjectSettings
) : WithoutScriptModels(settings) {
init {
loadLastModifiedFiles()
}
}
/** fun fileChanged(filePath: String, ts: Long) {
* Linked but not yet imported Gradle build. lastModifiedFiles.fileChanged(ts, filePath)
*/ }
class New( }
settings: GradleProjectSettings
) : WithoutScriptModels(settings) { sealed class WithoutScriptModels(settings: GradleProjectSettings) : GradleBuildRoot() {
init { final override val pathPrefix = settings.externalProjectPath!!
loadLastModifiedFiles() final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix)
}
} init {
loadLastModifiedFiles()
/** }
* Imported Gradle build. }
* Each imported build have info about all of it's Kotlin Build Scripts.
*/ /**
class Imported( * Gradle build with old Gradle version (<6.0)
override val pathPrefix: String, */
val javaHome: File?, class Legacy(
val data: GradleBuildRootData settings: GradleProjectSettings
) : Linked() { ) : WithoutScriptModels(settings) {
override val projectRoots: Collection<String> init {
get() = data.projectRoots loadLastModifiedFiles()
}
init { }
loadLastModifiedFiles()
} /**
* Linked but not yet imported Gradle build.
fun collectConfigurations(builder: ScriptClassRootsBuilder) { */
if (javaHome != null) { class New(
builder.sdks.addSdk(javaHome) settings: GradleProjectSettings
} ) : WithoutScriptModels(settings) {
init {
val definitions = GradleScriptDefinitionsContributor.getDefinitions(builder.project) loadLastModifiedFiles()
}
builder.addTemplateClassesRoots(data.templateClasspath) }
data.models.forEach { script -> /**
val definition = selectScriptDefinition(script, definitions) * Imported Gradle build.
* Each imported build have info about all of it's Kotlin Build Scripts.
builder.addCustom( */
script.file, class Imported(
script.classPath, override val pathPrefix: String,
script.sourcePath, val javaHome: File?,
GradleScriptInfo(this, definition, script) val data: GradleBuildRootData
) ) : GradleBuildRoot() {
} override val projectRoots: Collection<String>
} get() = data.projectRoots
private fun selectScriptDefinition( init {
script: KotlinDslScriptModel, loadLastModifiedFiles()
definitions: List<ScriptDefinition> }
): ScriptDefinition? {
val file = LocalFileSystem.getInstance().findFileByPath(script.file) ?: return null fun collectConfigurations(builder: ScriptClassRootsBuilder) {
val scriptSource = VirtualFileScriptSource(file) if (javaHome != null) {
return definitions.firstOrNull { it.isScript(scriptSource) } builder.sdks.addSdk(javaHome)
} }
}
val definitions = GradleScriptDefinitionsContributor.getDefinitions(builder.project)
companion object {
@set:TestOnly builder.addTemplateClassesRoots(data.templateClasspath)
@get:TestOnly
internal var skipLastModifiedFilesLoading = false data.models.forEach { script ->
val definition = selectScriptDefinition(script, definitions)
builder.addCustom(
script.file,
script.classPath,
script.sourcePath,
GradleScriptInfo(this, definition, script)
)
}
}
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) }
} }
} }
@@ -7,15 +7,15 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.diagnostic.logger
class GradleBuildRootIndex { class GradleBuildRootIndex : StandaloneScriptsUpdater {
private val log = logger<GradleBuildRootIndex>() private val log = logger<GradleBuildRootIndex>()
private val standaloneScriptRoots = mutableMapOf<String, GradleBuildRoot.Linked?>() private val standaloneScriptRoots = mutableMapOf<String, GradleBuildRoot?>()
private val byWorkingDir = HashMap<String, GradleBuildRoot.Linked>() private val byWorkingDir = HashMap<String, GradleBuildRoot>()
private val byProjectDir = HashMap<String, GradleBuildRoot.Linked>() private val byProjectDir = HashMap<String, GradleBuildRoot>()
val list: Collection<GradleBuildRoot.Linked> val list: Collection<GradleBuildRoot>
get() = byWorkingDir.values get() = byWorkingDir.values
@Synchronized @Synchronized
@@ -34,8 +34,8 @@ class GradleBuildRootIndex {
fun getBuildByRootDir(dir: String) = byWorkingDir[dir] fun getBuildByRootDir(dir: String) = byWorkingDir[dir]
@Synchronized @Synchronized
fun findNearestRoot(path: String): GradleBuildRoot.Linked? { fun findNearestRoot(path: String): GradleBuildRoot? {
var max: Pair<String, GradleBuildRoot.Linked>? = null var max: Pair<String, GradleBuildRoot>? = null
byWorkingDir.entries.forEach { byWorkingDir.entries.forEach {
if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) { if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) {
max = it.key to it.value max = it.key to it.value
@@ -54,7 +54,7 @@ class GradleBuildRootIndex {
fun getStandaloneScriptRoot(path: String) = standaloneScriptRoots[path] fun getStandaloneScriptRoot(path: String) = standaloneScriptRoots[path]
@Synchronized @Synchronized
fun add(value: GradleBuildRoot.Linked): GradleBuildRoot.Linked? { fun add(value: GradleBuildRoot): GradleBuildRoot? {
val prefix = value.pathPrefix val prefix = value.pathPrefix
val old = byWorkingDir.put(prefix, value) val old = byWorkingDir.put(prefix, value)
rebuildProjectRoots() rebuildProjectRoots()
@@ -69,11 +69,15 @@ class GradleBuildRootIndex {
} }
@Synchronized @Synchronized
fun addStandaloneScript(path: String) { override fun addStandaloneScript(path: String) {
computeStandaloneScriptRoot(path) computeStandaloneScriptRoot(path)
} }
var standaloneScripts: Collection<String> @Synchronized
override fun removeStandaloneScript(path: String) =
standaloneScriptRoots.remove(path)
override var standaloneScripts: Collection<String>
@Synchronized get() = standaloneScriptRoots.keys @Synchronized get() = standaloneScriptRoots.keys
@Synchronized set(value) { @Synchronized set(value) {
standaloneScriptRoots.clear() standaloneScriptRoots.clear()
@@ -38,7 +38,7 @@ abstract class GradleBuildRootsLocator {
findAffectedFileRoot(filePath) != null || findAffectedFileRoot(filePath) != null ||
roots.isStandaloneScript(filePath) roots.isStandaloneScript(filePath)
fun findAffectedFileRoot(filePath: String): GradleBuildRoot.Linked? { fun findAffectedFileRoot(filePath: String): GradleBuildRoot? {
if (filePath.endsWith("/gradle.properties") || if (filePath.endsWith("/gradle.properties") ||
filePath.endsWith("/gradle.local") filePath.endsWith("/gradle.local")
) { ) {
@@ -50,25 +50,42 @@ abstract class GradleBuildRootsLocator {
return roots.getBuildByRootDir(filePath.substring(0, buildDirIndex)) return roots.getBuildByRootDir(filePath.substring(0, buildDirIndex))
} }
return findScriptBuildRoot(filePath, searchNearestLegacy = false)?.root as? GradleBuildRoot.Linked return findScriptBuildRoot(filePath, searchNearestLegacy = false)?.root as? GradleBuildRoot
}
enum class NotificationKind {
dontCare, // one of: imported, inside linked legacy gradle build
outsideAnyting, // suggest link related gradle build or just say that there is no one
wasNotImportedAfterCreation, // project not yet imported after this file was created
notEvaluatedInLastImport, // all other scripts, suggest to sync or mark as standalone
standalone
} }
class ScriptUnderRoot( class ScriptUnderRoot(
val root: GradleBuildRoot?, val root: GradleBuildRoot?,
val script: GradleScriptInfo? = null, val script: GradleScriptInfo? = null,
val standalone: Boolean = false val standalone: Boolean = false,
val nearest: GradleBuildRoot? = null
) { ) {
val isUnrelatedScript: Boolean val notificationKind: NotificationKind
get() = root is GradleBuildRoot.Unlinked get() = when {
isImported -> NotificationKind.dontCare
val importRequired: Boolean standalone -> NotificationKind.standalone
get() = root is GradleBuildRoot.Linked && nearest == null -> NotificationKind.outsideAnyting
!isImported && nearest.importing -> NotificationKind.dontCare
!root.importing && else -> when (nearest) {
!standalone is Legacy -> NotificationKind.dontCare
is New -> NotificationKind.wasNotImportedAfterCreation
is Imported -> NotificationKind.notEvaluatedInLastImport // todo: wasNotImportedAfterCreation
}
}
private val isImported: Boolean private val isImported: Boolean
get() = script != null get() = script != null
override fun toString(): String {
return "ScriptUnderRoot(root=$root, script=$script, standalone=$standalone, nearest=$nearest)"
}
} }
fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? = fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? =
@@ -98,12 +115,14 @@ abstract class GradleBuildRootsLocator {
return ScriptUnderRoot(standaloneScriptRoot, standalone = true) return ScriptUnderRoot(standaloneScriptRoot, standalone = true)
} }
// todo(gradle6): remove, it is required only for projects with old gradle
if (searchNearestLegacy) { if (searchNearestLegacy) {
val found = roots.findNearestRoot(filePath) val nearest = roots.findNearestRoot(filePath)
if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found) return when (nearest) {
is Legacy -> ScriptUnderRoot(nearest)
else -> ScriptUnderRoot(null, nearest = nearest)
}
} else {
return ScriptUnderRoot(null)
} }
return ScriptUnderRoot(GradleBuildRoot.Unlinked())
} }
} }
@@ -21,11 +21,11 @@ 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.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport.Companion.EPN import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport.Companion.EPN
import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile
import org.jetbrains.kotlin.idea.core.script.ucache.ScriptClassRootsBuilder import org.jetbrains.kotlin.idea.core.script.ucache.ScriptClassRootsBuilder
import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.scripting.gradle.* import org.jetbrains.kotlin.idea.scripting.gradle.*
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
@@ -50,12 +50,10 @@ import java.util.concurrent.atomic.AtomicBoolean
* It also used to show related notification and floating actions depending on root kind, state and script state itself. * It also used to show related notification and floating actions depending on root kind, state and script state itself.
* *
* Roots may be: * Roots may be:
* - [GradleBuildRoot.Unlinked] - The script not related to any Gradle build that is linked to IntelliJ Project, * - [GradleBuildRoot] - Linked project, that may be itself:
* or we cannot known what is it * - [Legacy] - Gradle build with old Gradle version (<6.0)
* - [GradleBuildRoot.Linked] - Linked project, that may be itself: * - [New] - not yet imported
* - [GradleBuildRoot.Legacy] - Gradle build with old Gradle version (<6.0) * - [Imported] - imported
* - [GradleBuildRoot.New] - not yet imported
* - [GradleBuildRoot.Imported] - imported
*/ */
class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), ScriptingSupport { class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), ScriptingSupport {
private val manager: CompositeScriptConfigurationManager private val manager: CompositeScriptConfigurationManager
@@ -69,14 +67,14 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
override fun isApplicable(file: VirtualFile): Boolean { override fun isApplicable(file: VirtualFile): Boolean {
val scriptUnderRoot = findScriptBuildRoot(file) ?: return false val scriptUnderRoot = findScriptBuildRoot(file) ?: return false
if (scriptUnderRoot.root is GradleBuildRoot.Legacy) return false if (scriptUnderRoot.root is Legacy) return false
if (roots.isStandaloneScript(file.path)) return false if (roots.isStandaloneScript(file.path)) return false
return true return true
} }
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean { override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)?.root) { return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)?.root) {
is GradleBuildRoot.Linked -> root.importing is GradleBuildRoot -> root.importing
else -> false else -> false
} }
} }
@@ -98,7 +96,7 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
override fun collectConfigurations(builder: ScriptClassRootsBuilder) { override fun collectConfigurations(builder: ScriptClassRootsBuilder) {
roots.list.forEach { root -> roots.list.forEach { root ->
if (root is GradleBuildRoot.Imported) { if (root is Imported) {
root.collectConfigurations(builder) root.collectConfigurations(builder)
} }
} }
@@ -116,32 +114,32 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) { fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) {
actualizeBuildRoot(workingDir)?.importing = inProgress actualizeBuildRoot(workingDir)?.importing = inProgress
updateNotifications(workingDir) updateNotifications { it.startsWith(workingDir) }
scriptConfigurationsAreUpToDate(project) hideNotificationForProjectImport(project)
} }
fun update(build: KotlinDslGradleBuildSync) { fun update(build: KotlinDslGradleBuildSync) {
// fast path for linked gradle builds without .gradle.kts support // fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) { if (build.models.isEmpty()) {
val root = getBuildRootByWorkingDir(build.workingDir) ?: return val root = getBuildRootByWorkingDir(build.workingDir) ?: return
if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return if (root is Imported && root.data.models.isEmpty()) return
} }
val root = actualizeBuildRoot(build.workingDir) ?: return val root = actualizeBuildRoot(build.workingDir) ?: return
root.importing = false root.importing = false
if (root is GradleBuildRoot.Legacy) return if (root is Legacy) return
val templateClasspath = GradleScriptDefinitionsContributor.getDefinitionsTemplateClasspath(project) val templateClasspath = GradleScriptDefinitionsContributor.getDefinitionsTemplateClasspath(project)
val newData = GradleBuildRootData(build.projectRoots, templateClasspath, build.models) val newData = GradleBuildRootData(build.projectRoots, templateClasspath, build.models)
val mergedData = if (build.failed && root is GradleBuildRoot.Imported) merge(root.data, newData) else newData val mergedData = if (build.failed && root is Imported) merge(root.data, newData) else newData
val newSupport = tryCreateImportedRoot(build.workingDir) { mergedData } ?: return val newSupport = tryCreateImportedRoot(build.workingDir) { mergedData } ?: return
GradleBuildRootDataSerializer.write(newSupport.dir ?: return, mergedData) GradleBuildRootDataSerializer.write(newSupport.dir ?: return, mergedData)
add(newSupport) add(newSupport)
scriptConfigurationsAreUpToDate(project) hideNotificationForProjectImport(project)
} }
private fun merge(old: GradleBuildRootData, new: GradleBuildRootData): GradleBuildRootData { private fun merge(old: GradleBuildRootData, new: GradleBuildRootData): GradleBuildRootData {
@@ -168,12 +166,22 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
} }
} }
fun addStandaloneScript(file: VirtualFile) { fun updateStandaloneScripts(update: StandaloneScriptsUpdater.() -> Unit) {
roots.addStandaloneScript(file.path) val changes = StandaloneScriptsUpdater.collectChanges(delegate = roots, update)
roots.rebuildProjectRoots()
updateNotifications(file.path) updateNotifications { it in changes.new || it in changes.removed }
val ktFile = project.getKtFile(file) ?: return
ScriptConfigurationManager.getInstance(project).getConfiguration(ktFile) runReadAction {
changes.new.forEach {
val virtualFile = LocalFileSystem.getInstance().findFileByPath(it)
if (virtualFile != null) {
val ktFile = PsiManager.getInstance(project).findFile(virtualFile) as? KtFile
if (ktFile != null) {
ScriptConfigurationManager.getInstance(project).getConfiguration(ktFile)
}
}
}
}
} }
init { init {
@@ -218,7 +226,7 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
* Actually this should be true, but we may miss some change events. * 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) * For that cases we are rechecking this on each Gradle Project sync (importing/reimporting)
*/ */
private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot.Linked? { private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot? {
val actualSettings = getGradleProjectSettings(workingDir) val actualSettings = getGradleProjectSettings(workingDir)
val buildRoot = getBuildRootByWorkingDir(workingDir) val buildRoot = getBuildRootByWorkingDir(workingDir)
@@ -232,15 +240,15 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
} }
} }
private fun GradleBuildRoot.Linked.checkActual(actualSettings: GradleProjectSettings?): Boolean { private fun GradleBuildRoot.checkActual(actualSettings: GradleProjectSettings?): Boolean {
if (actualSettings == null) return false if (actualSettings == null) return false
val knownAsSupported = this !is GradleBuildRoot.Legacy val knownAsSupported = this !is Legacy
val shouldBeSupported = kotlinDslScriptsModelImportSupported(actualSettings.resolveGradleVersion().version) val shouldBeSupported = kotlinDslScriptsModelImportSupported(actualSettings.resolveGradleVersion().version)
return knownAsSupported == shouldBeSupported return knownAsSupported == shouldBeSupported
} }
private fun reloadBuildRoot(rootPath: String): GradleBuildRoot.Linked? { private fun reloadBuildRoot(rootPath: String): GradleBuildRoot? {
val settings = getGradleProjectSettings(rootPath) val settings = getGradleProjectSettings(rootPath)
if (settings == null) { if (settings == null) {
remove(rootPath) remove(rootPath)
@@ -267,45 +275,45 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
settings: GradleProjectSettings settings: GradleProjectSettings
) = data.copy(projectRoots = data.projectRoots.toSet() + settings.modules) ) = data.copy(projectRoots = data.projectRoots.toSet() + settings.modules)
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked { private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version) val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when { return when {
supported -> GradleBuildRoot.New(settings) supported -> New(settings)
else -> GradleBuildRoot.Legacy(settings) else -> Legacy(settings)
} }
} }
private fun tryCreateImportedRoot( private fun tryCreateImportedRoot(
externalProjectPath: String, externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> GradleBuildRootData? dataProvider: (buildRoot: VirtualFile) -> GradleBuildRootData?
): GradleBuildRoot.Imported? { ): Imported? {
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null val data = dataProvider(buildRoot) ?: return null
val javaHome = ExternalSystemApiUtil val javaHome = ExternalSystemApiUtil
.getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID) .getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID)
.javaHome?.let { File(it) } .javaHome?.let { File(it) }
return GradleBuildRoot.Imported(externalProjectPath, javaHome, data) return Imported(externalProjectPath, javaHome, data)
} }
private fun add(newRoot: GradleBuildRoot.Linked) { private fun add(newRoot: GradleBuildRoot) {
val old = roots.add(newRoot) val old = roots.add(newRoot)
if (old is GradleBuildRoot.Imported && newRoot !is GradleBuildRoot.Imported) removeData(old.pathPrefix) if (old is Imported && newRoot !is Imported) removeData(old.pathPrefix)
if (old is GradleBuildRoot.Imported || newRoot is GradleBuildRoot.Imported) { if (old is Imported || newRoot is Imported) {
updater.invalidateAndCommit() updater.invalidateAndCommit()
} }
updateNotifications(newRoot.pathPrefix) updateNotifications { it.startsWith(newRoot.pathPrefix) }
} }
private fun remove(rootPath: String) { private fun remove(rootPath: String) {
val removed = roots.remove(rootPath) val removed = roots.remove(rootPath)
if (removed is GradleBuildRoot.Imported) { if (removed is Imported) {
removeData(rootPath) removeData(rootPath)
updater.invalidateAndCommit() updater.invalidateAndCommit()
} }
updateNotifications(rootPath) updateNotifications { it.startsWith(rootPath) }
} }
private fun removeData(rootPath: String) { private fun removeData(rootPath: String) {
@@ -317,11 +325,11 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
} }
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
private fun updateNotifications(dir: String) { private fun updateNotifications(shouldUpdatePath: (String) -> Boolean) {
if (!project.isOpen) return if (!project.isOpen) return
val openedScripts = FileEditorManager.getInstance(project).openFiles.filter { val openedScripts = FileEditorManager.getInstance(project).openFiles.filter {
it.path.startsWith(dir) && maybeAffectedGradleProjectFile(it.path) shouldUpdatePath(it.path) && maybeAffectedGradleProjectFile(it.path)
} }
if (openedScripts.isEmpty()) return if (openedScripts.isEmpty()) return
@@ -19,7 +19,7 @@ import kotlin.script.experimental.jvm.jdkHome
import kotlin.script.experimental.jvm.jvm import kotlin.script.experimental.jvm.jvm
class GradleScriptInfo( class GradleScriptInfo(
val buildRoot: GradleBuildRoot.Imported, val buildRoot: Imported,
scriptDefinition: ScriptDefinition?, scriptDefinition: ScriptDefinition?,
val model: KotlinDslScriptModel val model: KotlinDslScriptModel
) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) { ) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) {
@@ -0,0 +1,43 @@
/*
* 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
interface StandaloneScriptsUpdater {
var standaloneScripts: Collection<String>
fun addStandaloneScript(path: String)
fun removeStandaloneScript(path: String): GradleBuildRoot?
class Changes {
val new = mutableSetOf<String>()
val removed = mutableSetOf<String>()
}
companion object {
fun collectChanges(
delegate: StandaloneScriptsUpdater,
update: StandaloneScriptsUpdater.() -> Unit
): Changes {
synchronized(delegate) {
val old = delegate.standaloneScripts.toSet()
delegate.update()
val changes = Changes()
val new = delegate.standaloneScripts
new.forEach {
if (it !in old) changes.new.add(it)
}
old.forEach {
if (it !in new) changes.removed.add(it)
}
return changes
}
}
}
}
@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.* import com.intellij.psi.*
import com.intellij.psi.impl.source.tree.LeafPsiElement import com.intellij.psi.impl.source.tree.LeafPsiElement
@@ -64,7 +63,7 @@ fun getGradleScriptInputsStamp(
} }
} }
val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot.Linked val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot
GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp, buildRoot?.pathPrefix) GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp, buildRoot?.pathPrefix)
} else null } else null
} }
@@ -7,10 +7,11 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots
import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs 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 kotlin.test.assertEquals
open class AbstractGradleBuildRootsLocatorTest { open class AbstractGradleBuildRootsLocatorTest {
init { init {
GradleBuildRoot.skipLastModifiedFilesLoading = true skipLastModifiedFilesLoading = true
} }
private val scripts = mutableMapOf<String, GradleScriptInfo>() private val scripts = mutableMapOf<String, GradleScriptInfo>()
@@ -21,7 +22,7 @@ open class AbstractGradleBuildRootsLocatorTest {
fun accessRoots() = roots fun accessRoots() = roots
} }
private fun add(root: GradleBuildRoot.Linked) { private fun add(root: GradleBuildRoot) {
locator.accessRoots().add(root) locator.accessRoots().add(root)
} }
@@ -31,7 +32,7 @@ open class AbstractGradleBuildRootsLocatorTest {
relativeScripts: List<String> = listOf("build.gradle.kts") relativeScripts: List<String> = listOf("build.gradle.kts")
) { ) {
val pathPrefix = "$dir/" val pathPrefix = "$dir/"
val root = GradleBuildRoot.Imported( val root = Imported(
dir, dir,
null, null,
GradleBuildRootData( GradleBuildRootData(
@@ -62,4 +63,8 @@ open class AbstractGradleBuildRootsLocatorTest {
fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true) = fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true) =
locator.findScriptBuildRoot(filePath, searchNearestLegacy) locator.findScriptBuildRoot(filePath, searchNearestLegacy)
fun assertNotificationKind(filePath: String, notificationKind: GradleBuildRootsLocator.NotificationKind) {
assertEquals(notificationKind, findScriptBuildRoot(filePath)?.notificationKind)
}
} }
@@ -5,7 +5,10 @@
package org.jetbrains.kotlin.idea.scripting.gradle.roots package org.jetbrains.kotlin.idea.scripting.gradle.roots
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.dontCare
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.wasNotImportedAfterCreation
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertNotNull import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
@@ -16,18 +19,14 @@ class GradleBuildRootsLocatorTest : AbstractGradleBuildRootsLocatorTest() {
// the build.gradle.kts under the project root will be definitive import at next import // the build.gradle.kts under the project root will be definitive import at next import
// so, we should not treat it as unlinked // so, we should not treat it as unlinked
newImportedGradleProject("imported", relativeScripts = listOf()) newImportedGradleProject("imported", relativeScripts = listOf())
val scriptUnderRoot = findScriptBuildRoot("imported/build.gradle.kts")
assertNotNull(scriptUnderRoot) assertNotificationKind("imported/build.gradle.kts", wasNotImportedAfterCreation)
assertTrue(scriptUnderRoot.importRequired)
assertFalse(scriptUnderRoot.isUnrelatedScript)
} }
@Test @Test
fun testBuildGradleKtsNearProjectRoot() { fun testBuildGradleKtsNearProjectRoot() {
newImportedGradleProject("imported", relativeScripts = listOf("build.gradle.kts")) newImportedGradleProject("imported", relativeScripts = listOf("build.gradle.kts"))
val scriptUnderRoot = findScriptBuildRoot("imported/build.gradle.kts")
assertNotNull(scriptUnderRoot) assertNotificationKind("imported/build.gradle.kts", dontCare)
assertFalse(scriptUnderRoot.importRequired)
assertFalse(scriptUnderRoot.isUnrelatedScript)
} }
} }