Compare commits

...

6 Commits

Author SHA1 Message Date
Natalia Selezneva
b7c416b245 Try to fix flacky scratch test 2020-05-14 17:18:05 +03:00
Natalia Selezneva
07382a337a tmp log 2020-05-14 16:05:53 +03:00
Natalia Selezneva
f8aed6a823 Minor: do not associate with the same extensions multiple times in ScriptDefinitionsManager 2020-05-13 15:13:18 +03:00
Natalia Selezneva
c6db5d2251 Do not show notification before all definitions are loaded 2020-05-13 15:13:18 +03:00
Natalia Selezneva
4802016831 Load script definitions in IDE on project opening
This will speed up first script opening,
will avoid unexpected indexing because of new definitions
Script definitions are also needed for Kotlin scripting settings
2020-05-13 15:13:18 +03:00
Natalia Selezneva
86a397edf9 Introduce index for folders with script definitions templates
Replace it with nonBlockingReadAction meaning that if update was canceled then loading will be restarted after all write events happen
Also scanning is now started after indexing

^KT-36378 Fixed
^KT-34138 Fixed
^KT-37863 Fixed
2020-05-13 15:13:17 +03:00
10 changed files with 329 additions and 250 deletions

View File

@@ -16,10 +16,12 @@ import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.progress.runBackgroundableTask
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.ex.PathUtilEx
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.startup.StartupActivity
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
@@ -28,7 +30,9 @@ import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.project.SdkInfo
import org.jetbrains.kotlin.idea.caches.project.getScriptRelatedModuleInfo
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.util.application.executeOnPooledThread
import org.jetbrains.kotlin.idea.util.application.getServiceSafe
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
import org.jetbrains.kotlin.script.ScriptTemplatesProvider
import org.jetbrains.kotlin.scripting.definitions.*
@@ -55,6 +59,20 @@ import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContextOrStdlib
import kotlin.script.templates.standard.ScriptTemplateWithArgs
class LoadScriptDefinitionsStartupActivity : StartupActivity {
override fun runActivity(project: Project) {
if (isUnitTestMode()) {
// In tests definitions are loaded synchronously because they are needed to analyze script
// In IDE script won't be highlighted before all definitions are loaded, then the highlighting will be restarted
ScriptDefinitionsManager.getInstance(project).reloadScriptDefinitionsIfNeeded()
} else {
executeOnPooledThread {
ScriptDefinitionsManager.getInstance(project).reloadScriptDefinitionsIfNeeded()
}
}
}
}
class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinitionProvider() {
private var definitionsBySource = mutableMapOf<ScriptDefinitionsSource, List<ScriptDefinition>>()
private var definitions: List<ScriptDefinition>? = null
@@ -124,7 +142,17 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
return fromNewEp.dropLast(1) + fromDeprecatedEP + fromNewEp.last()
}
fun reloadScriptDefinitionsIfNeeded() = lock.write {
if (definitions == null) {
loadScriptDefinitions()
}
}
fun reloadScriptDefinitions() = lock.write {
loadScriptDefinitions()
}
private fun loadScriptDefinitions() {
for (source in getSources()) {
val definitions = source.safeGetDefinitions()
definitionsBySource[source] = definitions
@@ -147,9 +175,6 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
}
fun isReady(): Boolean {
if (definitions == null) {
reloadScriptDefinitions()
}
return definitions != null && definitionsBySource.keys.all { source ->
// TODO: implement another API for readiness checking
(source as? ScriptDefinitionContributor)?.isReady() != false
@@ -173,7 +198,7 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
val newExtensions = getKnownFilenameExtensions().filter {
fileTypeManager.getFileTypeByExtension(it) != KotlinFileType.INSTANCE
}.toList()
}.toSet()
if (newExtensions.any()) {
// Register new file extensions
@@ -319,7 +344,7 @@ fun ScriptDefinitionContributor.asSource(): ScriptDefinitionsSource =
if (this is ScriptDefinitionsSource) this
else ScriptDefinitionSourceFromContributor(this)
class StandardScriptDefinitionContributor(project: Project) : ScriptDefinitionContributor {
class StandardScriptDefinitionContributor(val project: Project) : ScriptDefinitionContributor {
private val standardIdeScriptDefinition = StandardIdeScriptDefinition(project)
override fun getDefinitions() = listOf(standardIdeScriptDefinition)

View File

@@ -22,6 +22,7 @@ import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File
import java.util.logging.Logger
abstract class ScriptClassRootsCache(
private val project: Project,
@@ -60,10 +61,13 @@ abstract class ScriptClassRootsCache(
private val scriptsDependenciesCache: MutableMap<VirtualFile, Fat> =
ConcurrentFactoryMap.createWeakMap { file ->
val configuration = getConfiguration(file) ?: return@createWeakMap null
logger.info("configuration for ${file.path} - $configuration")
val roots = configuration.dependenciesClassPath
val sdk = getScriptSdk(file)
logger.info("sdk for ${file.path} - ${sdk}")
@Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) {
return@createWeakMap Fat(
@@ -84,7 +88,10 @@ abstract class ScriptClassRootsCache(
return scriptsDependenciesCache[file]?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE
}
private val logger = Logger.getLogger("ScriptClassRoots")
fun getScriptConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
logger.info("getScriptConfiguration for ${file.path}")
return scriptsDependenciesCache[file]?.scriptConfiguration
}

View File

@@ -28,6 +28,7 @@ import org.jetbrains.plugins.gradle.settings.GradleSettingsListener
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import java.nio.file.Paths
import java.util.logging.Logger
/**
* Creates [GradleScriptingSupport] per each linked Gradle build.
@@ -46,10 +47,14 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
override fun getSupport(file: VirtualFile): ScriptingSupport? {
if (isGradleKotlinScript(file)) {
findRoot(file)?.let { return it }
findRoot(file)?.let {
logger.info("support for ${file.path} - $it")
return it
}
val externalProjectSettings = findExternalProjectSettings(file) ?: return null
if (kotlinDslScriptsModelImportSupported(getGradleVersion(project, externalProjectSettings))) {
logger.info("support for ${file.path} - unlinked")
return unlinkedFilesSupport
}
}
@@ -96,7 +101,10 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
}
private val logger = Logger.getLogger("#org.jetbrains.kotlin.idea.scripting.gradle")
fun update(build: KotlinDslGradleBuildSync) {
logger.info("after sync ${build.workingDir} ${build.models.joinToString() }")
// fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) {
val root = roots.findRoot(build.workingDir) ?: return
@@ -104,6 +112,7 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
}
val templateClasspath = findTemplateClasspath(build) ?: return
logger.info("templateClasspath ${templateClasspath.joinToString()}")
val data = ConfigurationData(templateClasspath, build.models)
val newSupport = createSupport(build.workingDir) { data } ?: return
KotlinDslScriptModels.write(newSupport.buildRoot, data)

View File

@@ -9,5 +9,7 @@
<projectService serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage"/>
<trafficLightRendererContributor implementation="org.jetbrains.kotlin.idea.core.script.ScriptTrafficLightRendererContributor"/>
<fileBasedIndex implementation="org.jetbrains.kotlin.idea.script.ScriptTemplatesClassRootsIndex"/>
<postStartupActivity implementation="org.jetbrains.kotlin.idea.core.script.LoadScriptDefinitionsStartupActivity"/>
</extensions>
</idea-plugin>

View File

@@ -24,6 +24,7 @@ import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.actions.KOTLIN_WORKSHEET_EXTENSION
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.debugger.coroutine.util.logger
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingUtil
import org.jetbrains.kotlin.idea.scratch.actions.ClearScratchAction
@@ -37,12 +38,15 @@ import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.parsing.KotlinParserDefinition.Companion.STD_SCRIPT_SUFFIX
import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.MockLibraryUtil
import org.jetbrains.kotlin.utils.PathUtil
import org.junit.Assert
import org.junit.runner.RunWith
import java.io.File
@RunWith(JUnit3WithIdeaConfigurationRunner::class)
abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
fun doRightPreviewPanelOutputTest(fileName: String) {
@@ -151,12 +155,26 @@ abstract class AbstractScratchRunActionTest : FileEditorManagerTestCase() {
configureScratchByText(sourceFile.name, fileText)
}
waitForDefinitions()
if (!KotlinHighlightingUtil.shouldHighlight(myFixture.file)) error("Highlighting for scratch file is switched off")
launchScratch()
waitUntilScratchFinishes(isRepl)
}
private fun waitForDefinitions() {
val timeout = 10000
val start = System.currentTimeMillis()
while (!ScriptDefinitionsManager.getInstance(project).isReady()) {
if ((System.currentTimeMillis() - start) > timeout) {
LOG.warn("Waiting timeout $timeout ms is exceed")
break
}
Thread.sleep(100)
}
}
private fun getExpectedFile(fileName: String, isRepl: Boolean, suffix: String): File {
val expectedFileName = if (isRepl) {
fileName.replace(".kts", ".repl.$suffix")

View File

@@ -1,113 +0,0 @@
/*
* Copyright 2010-2019 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.script
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionSourceAsContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.util.application.executeOnPooledThread
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
abstract class AsyncScriptDefinitionsContributor(protected val project: Project) : ScriptDefinitionSourceAsContributor {
abstract val progressMessage: String
override fun isReady(): Boolean = _definitions != null
override val definitions: Sequence<ScriptDefinition>
get() {
definitionsLock.read {
if (_definitions != null) {
return _definitions!!.asSequence()
}
}
forceStartUpdate = false
asyncRunUpdateScriptTemplates()
return emptySequence()
}
protected fun asyncRunUpdateScriptTemplates() {
val backgroundTask = inProgressLock.write {
shouldStartNewUpdate = true
if (!inProgress) {
inProgress = true
return@write DefinitionsCollectorBackgroundTask()
}
return@write null
}
// TODO: resolve actual reason for the exception below
try {
backgroundTask?.queue()
} catch (e: IllegalStateException) {
if (e.message?.contains("Calling invokeAndWait from read-action leads to possible deadlock") == false) throw e
}
}
protected abstract fun loadScriptDefinitions(previous: List<ScriptDefinition>?): List<ScriptDefinition>
private var _definitions: List<ScriptDefinition>? = null
private val definitionsLock = ReentrantReadWriteLock()
@Volatile
protected var forceStartUpdate = false
@Volatile
protected var shouldStartNewUpdate = false
private var inProgress = false
private val inProgressLock = ReentrantReadWriteLock()
private inner class DefinitionsCollectorBackgroundTask : Task.Backgroundable(project, progressMessage, true) {
override fun onFinished() {
inProgressLock.write {
inProgress = false
}
}
override fun run(indicator: ProgressIndicator) {
while (true) {
if (indicator.isCanceled || !shouldStartNewUpdate) {
return
}
shouldStartNewUpdate = false
val previousDefinitions = definitionsLock.read {
if (!forceStartUpdate && _definitions != null) return
_definitions
}
val newDefinitions = loadScriptDefinitions(previousDefinitions)
val needReload = definitionsLock.write {
if (newDefinitions != _definitions) {
_definitions = newDefinitions
return@write true
}
return@write false
}
if (needReload) {
if (isHeadless) {
// If new script definitions found, then ScriptDefinitionsManager.reloadDefinitionsBy should be called
// This may cause deadlock because Task.Backgroundable.queue executes task synchronously in headless mode
executeOnPooledThread {
ScriptDefinitionsManager.getInstance(project).reloadDefinitionsBy(this@AsyncScriptDefinitionsContributor)
}
} else {
ScriptDefinitionsManager.getInstance(project).reloadDefinitionsBy(this@AsyncScriptDefinitionsContributor)
}
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.script
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.indexing.*
import com.intellij.util.io.IOUtil
import com.intellij.util.io.KeyDescriptor
import org.jetbrains.kotlin.scripting.definitions.SCRIPT_DEFINITION_MARKERS_PATH
import java.io.DataInput
import java.io.DataOutput
import java.util.*
class ScriptTemplatesClassRootsIndex :
ScalarIndexExtension<String>(),
FileBasedIndex.InputFilter, KeyDescriptor<String>,
DataIndexer<String, Void, FileContent> {
companion object {
val KEY = ID.create<String, Void>(ScriptTemplatesClassRootsIndex::class.java.canonicalName)
private val suffix = SCRIPT_DEFINITION_MARKERS_PATH.removeSuffix("/")
}
override fun getName(): ID<String, Void> = KEY
override fun getIndexer(): DataIndexer<String, Void, FileContent> = this
override fun getKeyDescriptor(): KeyDescriptor<String> = this
override fun getInputFilter(): FileBasedIndex.InputFilter = this
override fun dependsOnFileContent() = false
override fun getVersion(): Int = 1
override fun indexDirectories(): Boolean = true
override fun acceptInput(file: VirtualFile): Boolean {
return file.isDirectory && file.path.endsWith(suffix)
}
override fun save(out: DataOutput, value: String) {
IOUtil.writeUTF(out, value)
}
override fun read(input: DataInput): String? {
return IOUtil.readUTF(input)
}
override fun getHashCode(value: String): Int {
return value.hashCode()
}
override fun isEqual(val1: String?, val2: String?): Boolean {
return val1 == val2
}
override fun map(inputData: FileContent): Map<String?, Void?> {
return Collections.singletonMap(inputData.file.url, null)
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2010-2019 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.script
import com.intellij.ProjectTopics
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.roots.OrderEnumerator
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.vfs.*
import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.indexing.FileBasedIndex
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionSourceAsContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.core.script.loadDefinitionsFromTemplates
import org.jetbrains.kotlin.scripting.definitions.SCRIPT_DEFINITION_MARKERS_EXTENSION_WITH_DOT
import org.jetbrains.kotlin.scripting.definitions.SCRIPT_DEFINITION_MARKERS_PATH
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.getEnvironment
import java.io.File
import java.net.URL
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
class ScriptTemplatesFromDependenciesProvider(private val project: Project) : ScriptDefinitionSourceAsContributor {
override val id = "ScriptTemplatesFromDependenciesProvider"
override fun isReady(): Boolean = _definitions != null
override val definitions: Sequence<ScriptDefinition>
get() {
definitionsLock.withLock {
if (_definitions != null) {
return _definitions!!.asSequence()
}
}
forceStartUpdate = false
asyncRunUpdateScriptTemplates()
return emptySequence()
}
init {
val connection = project.messageBus.connect()
connection.subscribe(
ProjectTopics.PROJECT_ROOTS,
object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
if (project.isInitialized) {
forceStartUpdate = true
asyncRunUpdateScriptTemplates()
}
}
},
)
}
private fun asyncRunUpdateScriptTemplates() {
definitionsLock.withLock {
if (!forceStartUpdate && _definitions != null) return
}
inProgressLock.withLock {
if (!inProgress) {
inProgress = true
loadScriptDefinitions()
}
}
}
private var _definitions: List<ScriptDefinition>? = null
private val definitionsLock = ReentrantLock()
private var oldTemplates: TemplatesWithCp? = null
private data class TemplatesWithCp(
val templates: List<String>,
val classpath: List<File>,
)
private var inProgress = false
private val inProgressLock = ReentrantLock()
@Volatile
private var forceStartUpdate = false
private fun loadScriptDefinitions() {
if (ApplicationManager.getApplication().isUnitTestMode || project.isDefault) {
return onEarlyEnd()
}
val templates = LinkedHashSet<String>()
val classpath = LinkedHashSet<File>()
ReadAction
.nonBlocking<List<VirtualFile>> {
val fileManager = VirtualFileManager.getInstance()
FileBasedIndex.getInstance().getAllKeys(ScriptTemplatesClassRootsIndex.KEY, project).mapNotNull {
val vFile = fileManager.findFileByUrl(it)
// see SCRIPT_DEFINITION_MARKERS_PATH
vFile?.parent?.parent?.parent?.parent
}
}
.inSmartMode(project)
.expireWith(project)
.submit(AppExecutorUtil.getAppExecutorService())
.onSuccess { roots ->
val jarFS = JarFileSystem.getInstance()
roots.forEach { root ->
root.findFileByRelativePath(SCRIPT_DEFINITION_MARKERS_PATH)?.children?.forEach { resourceFile ->
if (resourceFile.isValid && !resourceFile.isDirectory) {
templates.add(resourceFile.name.removeSuffix(SCRIPT_DEFINITION_MARKERS_EXTENSION_WITH_DOT))
}
}
val templateSource = jarFS.getVirtualFileForJar(root) ?: root
val module = ProjectFileIndex.getInstance(project).getModuleForFile(templateSource) ?: return@forEach
// assuming that all libraries are placed into classes roots
// TODO: extract exact library dependencies instead of putting all module dependencies into classpath
// 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)
}
)
}
}
.onProcessed {
if (templates.isEmpty()) return@onProcessed onEarlyEnd()
val newTemplates = TemplatesWithCp(templates.toList(), classpath.toList())
if (newTemplates == oldTemplates) {
inProgressLock.withLock {
inProgress = false
}
return@onProcessed
}
oldTemplates = newTemplates
val hostConfiguration = ScriptingHostConfiguration(defaultJvmScriptingHostConfiguration) {
getEnvironment {
mapOf(
"projectRoot" to (project.basePath ?: project.baseDir.canonicalPath)?.let(::File),
)
}
}
val newDefinitions = loadDefinitionsFromTemplates(
templateClassNames = newTemplates.templates,
templateClasspath = newTemplates.classpath,
baseHostConfiguration = hostConfiguration,
)
val needReload = definitionsLock.withLock {
if (newDefinitions != _definitions) {
_definitions = newDefinitions
return@withLock true
}
return@withLock false
}
if (needReload) {
ScriptDefinitionsManager.getInstance(project).reloadDefinitionsBy(this@ScriptTemplatesFromDependenciesProvider)
}
inProgressLock.withLock {
inProgress = false
}
}
}
private fun onEarlyEnd() {
definitionsLock.withLock {
_definitions = emptyList()
}
inProgressLock.withLock {
inProgress = false
}
}
}

View File

@@ -39,6 +39,7 @@ class MultipleScriptDefinitionsChecker(private val project: Project) : EditorNot
if (!ktFile.isScript()) return null
if (KotlinScriptingSettings.getInstance(ktFile.project).suppressDefinitionsCheck) return null
if (!ScriptDefinitionsManager.getInstance(ktFile.project).isReady()) return null
val allApplicableDefinitions = ScriptDefinitionsManager.getInstance(project)
.getAllDefinitions()

View File

@@ -1,131 +0,0 @@
/*
* Copyright 2000-2018 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.script
import com.intellij.ProjectTopics
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.roots.OrderEnumerator
import com.intellij.openapi.vfs.JarFileSystem
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.core.script.loadDefinitionsFromTemplates
import org.jetbrains.kotlin.idea.util.projectStructure.allModules
import org.jetbrains.kotlin.scripting.definitions.SCRIPT_DEFINITION_MARKERS_EXTENSION_WITH_DOT
import org.jetbrains.kotlin.scripting.definitions.SCRIPT_DEFINITION_MARKERS_PATH
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.getEnvironment
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.jvm.defaultJvmScriptingHostConfiguration
class ScriptTemplatesFromDependenciesProvider(project: Project) : AsyncScriptDefinitionsContributor(project) {
private var templates: TemplatesWithCp? = null
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) {
forceStartUpdate = true
asyncRunUpdateScriptTemplates()
}
}
})
}
override val id = "ScriptTemplatesFromDependenciesProvider"
override val progressMessage = KotlinBundle.message("script.progress.text.kotlin.scanning.dependencies.for.script.definitions")
override fun loadScriptDefinitions(previous: List<ScriptDefinition>?): List<ScriptDefinition> {
if (project.isDefault) return emptyList()
val templatesCopy = templatesLock.write {
val newTemplates = scriptDefinitionsFromDependencies(project)
if (newTemplates != templates) {
templates = newTemplates
return@write newTemplates
}
return@write null
}
if (templatesCopy != null) {
val hostConfiguration = ScriptingHostConfiguration(defaultJvmScriptingHostConfiguration) {
getEnvironment {
mapOf(
"projectRoot" to (project.basePath ?: project.baseDir.canonicalPath)?.let(::File)
)
}
}
return loadDefinitionsFromTemplates(
templateClassNames = templatesCopy.templates,
templateClasspath = templatesCopy.classpath,
baseHostConfiguration = hostConfiguration
)
}
return previous ?: emptyList()
}
}
private data class TemplatesWithCp(
val templates: List<String>,
val classpath: List<File>
)
private fun scriptDefinitionsFromDependencies(project: Project): TemplatesWithCp {
val templates = LinkedHashSet<String>()
val classpath = LinkedHashSet<File>()
fun addTemplatesFromRoot(vfile: VirtualFile): Boolean {
var templatesFound = false
val root = JarFileSystem.getInstance().getJarRootForLocalFile(vfile) ?: vfile
if (root.isValid) {
root.findFileByRelativePath(SCRIPT_DEFINITION_MARKERS_PATH)?.takeIf { it.isDirectory }?.children?.forEach {
if (it.isValid && !it.isDirectory) {
templates.add(it.name.removeSuffix(SCRIPT_DEFINITION_MARKERS_EXTENSION_WITH_DOT))
templatesFound = true
}
}
}
return templatesFound
}
runReadAction {
if (project.isDisposed) return@runReadAction
// 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())
}