diff --git a/idea/src/org/jetbrains/kotlin/idea/script/scriptsTemplatesFromDependencies.kt b/idea/src/org/jetbrains/kotlin/idea/script/scriptsTemplatesFromDependencies.kt index 561f9c4408f..58b4bceea7c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/script/scriptsTemplatesFromDependencies.kt +++ b/idea/src/org/jetbrains/kotlin/idea/script/scriptsTemplatesFromDependencies.kt @@ -6,6 +6,8 @@ package org.jetbrains.kotlin.idea.script import com.intellij.ProjectTopics +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootListener @@ -24,100 +26,136 @@ import kotlin.concurrent.write class ScriptTemplatesFromDependenciesProvider(private val project: Project) : ScriptDefinitionContributor { - data class TemplatesWithCp( - val templates: List, - val classpath: List - ) - private var templates: TemplatesWithCp? = null - private val lock = ReentrantReadWriteLock() + private val templatesLock = ReentrantReadWriteLock() init { val connection = project.messageBus.connect() connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { override fun rootsChanged(event: ModuleRootEvent) { if (project.isInitialized) { - var templatesChanged = false - lock.read { - val newTemplates = getScriptTemplates() - if (newTemplates != templates) lock.write { - templates = newTemplates - templatesChanged = true - } - } - if (templatesChanged) { - ScriptDefinitionsManager.getInstance(project).reloadDefinitionsBy(this@ScriptTemplatesFromDependenciesProvider) - } + asyncRunUpdateScriptTemplates() } } }) } - private fun getScriptTemplates(): TemplatesWithCp { - val templates = LinkedHashSet() - val classpath = LinkedHashSet() - val templatesPath = "META-INF/kotlin/script/templates/" - val processedRoots = hashSetOf() - - fun addTemplatesFromRoot(vfile: VirtualFile): Boolean { - var templatesFound = false - val root = JarFileSystem.getInstance().getJarRootForLocalFile(vfile) ?: vfile - if (root.isValid) { - root.findFileByRelativePath(templatesPath)?.takeIf { it.isDirectory }?.children?.forEach { - if (it.isValid && !it.isDirectory) { - templates.add(it.name) - templatesFound = true - } - } - } - return templatesFound - } - - // processing source roots from the same project first since the resources are copied to the classes roots only on compilation - project.allModules().forEach { module -> - OrderEnumerator.orderEntries(module).withoutDepModules().withoutLibraries().withoutSdk().sourceRoots.forEach { - if (addTemplatesFromRoot(it)) { - classpath.addAll(OrderEnumerator.orderEntries(module).withoutSdk().classesRoots.mapNotNull { - it.canonicalPath?.let { File(it.removeSuffix("!/")) } - }) - } - } - } - - project.allModules().forEach { module -> - // assuming that all libraries are placed into classes roots - // TODO: extract exact library dependencies instead of putting all module dependencies into classpath - OrderEnumerator.orderEntries(module).withoutDepModules().withoutSdk().classesRoots.forEach { - if (addTemplatesFromRoot(it)) { - // minimizing the classpath needed to use the template by taking cp only from modules with new templates found - // on the other hand the approach may fail if some module contains a template without proper classpath, while - // the other has properly configured classpath, so assuming that the dependencies are set correctly everywhere - classpath.addAll(OrderEnumerator.orderEntries(module).withoutSdk().classesRoots.mapNotNull { - it.canonicalPath?.let { File(it.removeSuffix("!/")) } - }) - } - } - } - return TemplatesWithCp(templates.toList(), classpath.toList()) - } - override val id = "ScriptTemplatesFromDependenciesProvider" override fun getDefinitions(): List { - lock.read { + val templatesCopy = templatesLock.read { if (templates == null) { - val newTemplates = getScriptTemplates() - lock.write { - templates = newTemplates + syncUpdateScriptTemplates(false) + } + templates ?: return emptyList() + } + return loadDefinitionsFromTemplates( + templateClassNames = templatesCopy.templates, + templateClasspath = templatesCopy.classpath, + environment = mapOf( + "projectRoot" to (project.basePath ?: project.baseDir.canonicalPath)?.let(::File) + ) + ) + } + + private var areRootsChanged = false + private var backgroundTask: TemplatesCollectorBackgroundTask? = null + private val backgroundTaskLock = ReentrantReadWriteLock() + + private inner class TemplatesCollectorBackgroundTask + : Task.Backgroundable(project, "Kotlin: scanning dependencies for script definitions...", true) { + + override fun run(indicator: ProgressIndicator) { + while (true) { + backgroundTaskLock.read { + if (indicator.isCanceled || !areRootsChanged) { + backgroundTaskLock.write { + backgroundTask = null + } + return + } + areRootsChanged = false + } + if (syncUpdateScriptTemplates(true)) { + ScriptDefinitionsManager.getInstance(project).reloadDefinitionsBy(this@ScriptTemplatesFromDependenciesProvider) } } } - return loadDefinitionsFromTemplates( - templateClassNames = templates!!.templates, - templateClasspath = templates!!.classpath, - environment = mapOf( - "projectRoot" to (project.basePath ?: project.baseDir.canonicalPath)?.let(::File)) - ) + } + + private fun asyncRunUpdateScriptTemplates() { + backgroundTaskLock.write { + areRootsChanged = true + if (backgroundTask == null) { + backgroundTask = TemplatesCollectorBackgroundTask() + backgroundTask!!.queue() + } + } + } + + private fun syncUpdateScriptTemplates(force: Boolean): Boolean { + val wasRunning = templatesLock.isWriteLocked + templatesLock.write { + if (wasRunning && !force && templates != null) return true + val newTemplates = scriptDefinitionsFromDependencies(project) + if (newTemplates != templates) { + templates = newTemplates + return true + } + return false + } } } +private data class TemplatesWithCp( + val templates: List, + val classpath: List +) + +private fun scriptDefinitionsFromDependencies(project: Project): TemplatesWithCp { + val templates = LinkedHashSet() + val classpath = LinkedHashSet() + val templatesPath = "META-INF/kotlin/script/templates/" + + fun addTemplatesFromRoot(vfile: VirtualFile): Boolean { + var templatesFound = false + val root = JarFileSystem.getInstance().getJarRootForLocalFile(vfile) ?: vfile + if (root.isValid) { + root.findFileByRelativePath(templatesPath)?.takeIf { it.isDirectory }?.children?.forEach { + if (it.isValid && !it.isDirectory) { + templates.add(it.name) + templatesFound = true + } + } + } + return templatesFound + } + + // processing source roots from the same project first since the resources are copied to the classes roots only on compilation + project.allModules().forEach { module -> + OrderEnumerator.orderEntries(module).withoutDepModules().withoutLibraries().withoutSdk().sourceRoots.forEach { root -> + if (addTemplatesFromRoot(root)) { + classpath.addAll(OrderEnumerator.orderEntries(module).withoutSdk().classesRoots.mapNotNull { + it.canonicalPath?.removeSuffix("!/").let(::File) + }) + } + } + } + + project.allModules().forEach { module -> + // assuming that all libraries are placed into classes roots + // TODO: extract exact library dependencies instead of putting all module dependencies into classpath + OrderEnumerator.orderEntries(module).withoutDepModules().withoutSdk().classesRoots.forEach { root -> + if (addTemplatesFromRoot(root)) { + // minimizing the classpath needed to use the template by taking cp only from modules with new templates found + // on the other hand the approach may fail if some module contains a template without proper classpath, while + // the other has properly configured classpath, so assuming that the dependencies are set correctly everywhere + classpath.addAll(OrderEnumerator.orderEntries(module).withoutSdk().classesRoots.mapNotNull { + it.canonicalPath?.removeSuffix("!/").let(::File) + }) + } + } + } + return TemplatesWithCp(templates.toList(), classpath.toList()) +} +