diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinition.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinition.kt index d6f1ebe089a..ca0d446d522 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinition.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptDefinition.kt @@ -69,6 +69,13 @@ class KotlinScriptExternalDependenciesUnion(val dependencies: Iterable get() = dependencies.flatMap { it.sources } } +fun Iterable.isSameClasspathAs(other: Iterable): Boolean { + val c1 = map { it.canonicalPath }.toHashSet() + val c2 = other.map { it.canonicalPath }.toHashSet() + return c1.size == c2.size && + c1.zip(c2).all { it.first == it.second } +} + data class ScriptParameter(val name: Name, val type: KotlinType) fun getFileName(file: TF): String = when (file) { diff --git a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptExternalImportsProvider.kt b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptExternalImportsProvider.kt index 587acb6bd33..6ac2ef260fc 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptExternalImportsProvider.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/script/KotlinScriptExternalImportsProvider.kt @@ -49,6 +49,7 @@ class KotlinScriptExternalImportsProvider(val project: Project, private val scri } } + // optimized for initial caching, additional handling of possible duplicates to save a call to distinct fun cacheExternalImports(files: Iterable): Unit = cacheLock.write { val uncached = hashSetOf() files.forEach { file -> @@ -71,6 +72,36 @@ class KotlinScriptExternalImportsProvider(val project: Project, private val scri } } + // optimized for update, no special duplicates handling + fun updateExternalImportsCache(files: Iterable): Iterable = cacheLock.write { + files.mapNotNull { file -> + val path = getFilePath(file) + val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) + if (scriptDef != null) { + val deps = scriptDef.getDependenciesFor(file, project) + val oldDeps = cache[path] + when { + deps != null && (oldDeps == null || + !deps.classpath.isSameClasspathAs(oldDeps.classpath) || !deps.sources.isSameClasspathAs(oldDeps.sources)) -> { + // changed or new + cache.put(path, deps) + cacheOfNulls.remove(path) + file + } + deps != null -> { + // same as before + null + } + else -> { + if (cache.remove(path) != null || cacheOfNulls.remove(path)) file // cleared + else null // same as before + } + } + } + else null // not a script + } + } + fun invalidateCaches() { cacheLock.write { cache.keys.toList().apply { diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptConfigurationManager.kt index 429048b2c1d..10e62bd0731 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptConfigurationManager.kt @@ -17,24 +17,33 @@ package org.jetbrains.kotlin.idea.core.script import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ex.ProjectRootManagerEx +import com.intellij.openapi.util.EmptyRunnable import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.psi.search.FileTypeIndex import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.indexing.IndexableSetContributor import com.intellij.util.io.URLUtil import org.jetbrains.kotlin.idea.caches.resolve.FileLibraryScope import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.script.* import org.jetbrains.kotlin.utils.PathUtil import java.io.File import java.io.FileNotFoundException import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read +import kotlin.concurrent.write class KotlinScriptConfigurationManager( private val project: Project, + private val dumbService: DumbService, private val scriptDefinitionProvider: KotlinScriptDefinitionProvider, private val scriptExternalImportsProvider: KotlinScriptExternalImportsProvider? ) { @@ -47,11 +56,28 @@ class KotlinScriptConfigurationManager( reloadScriptDefinitions() // TODO: sort out read/write action business and if possible make it lazy (e.g. move to getAllScriptsClasspath) runReadAction { cacheAllScriptsExtraImports() } + + project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() { + override fun after(events: List) { + val isChanged = scriptExternalImportsProvider?.updateExternalImportsCache( events.mapNotNull { it.file })?.any() ?: false + if (isChanged) { + // TODO: consider more fine-grained update + cacheLock.write { + allScriptsClasspathCache = null + } + notifyRootsChanged() + } + } + }) } private var allScriptsClasspathCache: List? = null private val cacheLock = ReentrantReadWriteLock() + private fun notifyRootsChanged() { + runWriteAction { ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true) } + } + fun getScriptClasspath(file: VirtualFile): List = scriptExternalImportsProvider ?.getExternalImports(file) @@ -61,12 +87,17 @@ class KotlinScriptConfigurationManager( fun getAllScriptsClasspath(): List = cacheLock.read { if (allScriptsClasspathCache == null) { - allScriptsClasspathCache = - (scriptExternalImportsProvider?.getKnownCombinedClasspath() ?: emptyList()) - .distinct() - .mapNotNull { it.classpathEntryToVfs() } + dumbService.runWhenSmart { + cacheLock.write { + allScriptsClasspathCache = + (scriptExternalImportsProvider?.getKnownCombinedClasspath() ?: emptyList()) + .distinct() + .mapNotNull { it.classpathEntryToVfs() } + } + notifyRootsChanged() + } } - return allScriptsClasspathCache!! + return allScriptsClasspathCache ?: emptyList() } private fun File.classpathEntryToVfs(): VirtualFile =