Refactor KotlinScriptConfigurationManager: extract parts

Introduce ScriptDependenciesUpdater, DependenciesCache and ScriptContentLoader
This commit is contained in:
Pavel V. Talanov
2017-07-13 23:51:11 +03:00
parent 1b42095dc1
commit 96d8f685e9
5 changed files with 376 additions and 314 deletions
@@ -21,7 +21,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.script.KotlinScriptDefinitionProvider
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProvider
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProviderBase
import org.jetbrains.kotlin.script.ScriptContentLoader
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
@@ -29,12 +29,13 @@ import kotlin.concurrent.write
import kotlin.script.dependencies.ScriptDependencies
class KotlinScriptExternalImportsProviderImpl(
val project: Project,
project: Project,
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
) : KotlinScriptExternalImportsProviderBase(project) {
) : KotlinScriptExternalImportsProvider {
private val cacheLock = ReentrantReadWriteLock()
private val cache = hashMapOf<String, ScriptDependencies?>()
private val scriptContentLoader = ScriptContentLoader(project)
override fun getScriptDependencies(file: VirtualFile): ScriptDependencies? = cacheLock.read {
calculateExternalDependencies(file)
@@ -47,7 +48,7 @@ class KotlinScriptExternalImportsProviderImpl(
else {
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file)
if (scriptDef != null) {
val deps = resolveDependencies(scriptDef, file)
val deps = scriptContentLoader.loadContentsAndResolveDependencies(scriptDef, file)
if (deps != null) {
log.info("[kts] new cached deps for $path: ${deps.classpath.joinToString(File.pathSeparator)}")
@@ -28,7 +28,7 @@ import kotlin.reflect.KClass
import kotlin.script.dependencies.ScriptContents
import kotlin.script.dependencies.ScriptDependencies
abstract class KotlinScriptExternalImportsProviderBase(private val project: Project) : KotlinScriptExternalImportsProvider {
class ScriptContentLoader(private val project: Project) {
fun getScriptContents(scriptDefinition: KotlinScriptDefinition, file: VirtualFile)
= BasicScriptContents(file, getAnnotations = { loadAnnotations(scriptDefinition, file) })
@@ -57,7 +57,7 @@ abstract class KotlinScriptExternalImportsProviderBase(private val project: Proj
override val text: CharSequence? by lazy { virtualFile.inputStream.reader(charset = virtualFile.charset).readText() }
}
fun resolveDependencies(
fun loadContentsAndResolveDependencies(
scriptDef: KotlinScriptDefinition,
file: VirtualFile
): ScriptDependencies? {
@@ -0,0 +1,120 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.search.NonClasspathDirectoriesScope
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import kotlin.script.dependencies.ScriptDependencies
internal class DependenciesCache(private val project: Project) {
private val cacheLock = ReentrantReadWriteLock()
private val cache = hashMapOf<String, ScriptDependencies>()
operator fun get(virtualFile: VirtualFile): ScriptDependencies? = cacheLock.read { cache[virtualFile.path] }
val allScriptsClasspathCache = ClearableLazyValue(cacheLock) {
val files = cache.values.flatMap { it.classpath }.distinct()
KotlinScriptConfigurationManager.toVfsRoots(files)
}
val allScriptsClasspathScope = ClearableLazyValue(cacheLock) {
NonClasspathDirectoriesScope(allScriptsClasspathCache.get())
}
val allLibrarySourcesCache = ClearableLazyValue(cacheLock) {
KotlinScriptConfigurationManager.toVfsRoots(cache.values.flatMap { it.sources }.distinct())
}
val allLibrarySourcesScope = ClearableLazyValue(cacheLock) {
NonClasspathDirectoriesScope(allLibrarySourcesCache.get())
}
fun onChange() {
allScriptsClasspathCache.clear()
allScriptsClasspathScope.clear()
allLibrarySourcesCache.clear()
allLibrarySourcesScope.clear()
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project).getExtensionPoint(PsiElementFinder.EP_NAME).extensions
.filterIsInstance<KotlinScriptDependenciesClassFinder>()
.single()
kotlinScriptDependenciesClassFinder.clearCache()
}
fun clear() {
cacheLock.write(cache::clear)
onChange()
}
fun save(virtualFile: VirtualFile, new: ScriptDependencies): Boolean {
val path = virtualFile.path
val old = cacheLock.write {
val old = cache[path]
cache[path] = new
old
}
return old == null || !new.match(old)
}
fun delete(virtualFile: VirtualFile): Boolean = cacheLock.write {
cache.remove(virtualFile.path) != null
}
}
// TODO: relying on this to compare dependencies seems wrong, doesn't take javaHome and other stuff into account
private fun ScriptDependencies.match(other: ScriptDependencies)
= classpath.isSamePathListAs(other.classpath) &&
sources.toSet().isSamePathListAs(other.sources.toSet()) // TODO: gradle returns stdlib and reflect sources in unstable order for some reason
private fun Iterable<File>.isSamePathListAs(other: Iterable<File>): Boolean =
with(Pair(iterator(), other.iterator())) {
while (first.hasNext() && second.hasNext()) {
if (first.next().canonicalPath != second.next().canonicalPath) return false
}
!(first.hasNext() || second.hasNext())
}
internal class ClearableLazyValue<out T : Any>(private val lock: ReentrantReadWriteLock, private val compute: () -> T) {
private var value: T? = null
fun get(): T {
lock.read {
if (value == null) {
lock.write {
value = compute()
}
}
return value!!
}
}
fun clear() {
lock.write {
value = null
}
}
}
@@ -16,37 +16,18 @@
package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectUtil
import com.intellij.openapi.roots.ProjectRootManager
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.PsiElementFinder
import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.io.URLUtil
import kotlinx.coroutines.experimental.asCoroutineDispatcher
import kotlinx.coroutines.experimental.future.future
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.script.*
import org.jetbrains.kotlin.script.KotlinScriptDefinitionProvider
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProvider
import org.jetbrains.kotlin.script.makeScriptDefsFromTemplatesProviderExtensions
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors.newFixedThreadPool
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
import kotlin.script.dependencies.ScriptDependencies
import kotlin.script.dependencies.experimental.AsyncDependenciesResolver
// NOTE: this service exists exclusively because KotlinScriptConfigurationManager
@@ -62,253 +43,26 @@ class IdeScriptExternalImportsProvider(
class KotlinScriptConfigurationManager(
private val project: Project,
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
) : KotlinScriptExternalImportsProviderBase(project) {
private val cacheLock = ReentrantReadWriteLock()
private val scriptDependencyUpdatesDispatcher = newFixedThreadPool(1).asCoroutineDispatcher()
) {
private val cache = DependenciesCache(project)
private val cacheUpdater = ScriptDependenciesUpdater(project, cache, scriptDefinitionProvider)
init {
reloadScriptDefinitions()
listenToVfsChanges()
}
private fun listenToVfsChanges() {
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() {
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val application = ApplicationManager.getApplication()
override fun after(events: List<VFileEvent>) {
if (updateExternalImportsCache(events.mapNotNull {
// The check is partly taken from the BuildManager.java
it.file?.takeIf {
// the isUnitTestMode check fixes ScriptConfigurationHighlighting & Navigation tests, since they are not trigger proper update mechanims
// TODO: find out the reason, then consider to fix tests and remove this check
(application.isUnitTestMode || projectFileIndex.isInContent(it)) && !ProjectUtil.isProjectOrWorkspaceFile(it)
}
})) {
invalidateLocalCaches()
notifyRootsChanged()
}
}
})
}
private val allScriptsClasspathCache = ClearableLazyValue(cacheLock) {
val files = cache.values.flatMap { it?.dependencies?.classpath ?: emptyList() }.distinct()
toVfsRoots(files)
}
private val allScriptsClasspathScope = ClearableLazyValue(cacheLock) {
NonClasspathDirectoriesScope(getAllScriptsClasspath())
}
private val allLibrarySourcesCache = ClearableLazyValue(cacheLock) {
toVfsRoots(cache.values.flatMap { it?.dependencies?.sources ?: emptyList() }.distinct())
}
private val allLibrarySourcesScope = ClearableLazyValue(cacheLock) {
NonClasspathDirectoriesScope(getAllLibrarySources())
}
private fun notifyRootsChanged() {
val rootsChangesRunnable = {
runWriteAction {
if (project.isDisposed) return@runWriteAction
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) {
rootsChangesRunnable.invoke()
}
else {
application.invokeLater(rootsChangesRunnable, ModalityState.defaultModalityState())
}
}
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> = toVfsRoots(getScriptDependencies(file).classpath)
fun getAllScriptsClasspath(): List<VirtualFile> = allScriptsClasspathCache.get()
fun getAllLibrarySources(): List<VirtualFile> = allLibrarySourcesCache.get()
fun getAllScriptsClasspathScope() = allScriptsClasspathScope.get()
fun getAllLibrarySourcesScope() = allLibrarySourcesScope.get()
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> = toVfsRoots(cacheUpdater.getCurrentDependencies(file).classpath)
fun getScriptDependencies(file: VirtualFile) = cacheUpdater.getCurrentDependencies(file)
private fun reloadScriptDefinitions() {
val def = makeScriptDefsFromTemplatesProviderExtensions(project, { ep, ex -> log.warn("[kts] Error loading definition from ${ep.id}", ex) })
scriptDefinitionProvider.setScriptDefinitions(def)
}
private class TimeStampedRequest(val future: CompletableFuture<*>, val timeStamp: TimeStamp)
private class DataAndRequest(
val dependencies: ScriptDependencies?,
val modificationStamp: Long?,
val requestInProgress: TimeStampedRequest? = null
)
private val cache = hashMapOf<String, DataAndRequest?>()
override fun getScriptDependencies(file: VirtualFile): ScriptDependencies = cacheLock.read {
val path = file.path
val cached = cache[path]
cached?.dependencies?.let { return it }
tryLoadingFromDisk(cached, file, path)
updateExternalImportsCache(listOf(file))
return cache[path]?.dependencies ?: ScriptDependencies.Empty
}
private fun tryLoadingFromDisk(cached: DataAndRequest?, file: VirtualFile, path: String) {
if (cached != null) return
ScriptDependenciesFileAttribute.read(file)?.let { deserialized ->
save(path, deserialized, file, persist = false)
invalidateLocalCaches()
notifyRootsChanged()
}
}
private fun updateExternalImportsCache(files: Iterable<VirtualFile>) = cacheLock.write {
files.mapNotNull { file ->
scriptDefinitionProvider.findScriptDefinition(file)?.let {
updateForFile(file, it)
}
}
}.contains(true)
private fun updateForFile(file: VirtualFile, scriptDef: KotlinScriptDefinition): Boolean {
if (!file.isValid) {
return cache.remove(file.path) != null
}
val dependencyResolver = scriptDef.dependencyResolver
if (dependencyResolver is AsyncDependenciesResolver) {
return updateAsync(file, scriptDef, dependencyResolver)
}
return updateSync(file, scriptDef)
}
private fun updateAsync(
file: VirtualFile,
scriptDefinition: KotlinScriptDefinition,
dependencyResolver: AsyncDependenciesResolver
): Boolean {
val path = file.path
val oldDataAndRequest = cache[path]
if (!shouldSendNewRequest(file, oldDataAndRequest)) {
return false
}
oldDataAndRequest?.requestInProgress?.future?.cancel(true)
val (currentTimeStamp, newFuture) = sendRequest(path, scriptDefinition, dependencyResolver, file, oldDataAndRequest)
cache[path] = DataAndRequest(
oldDataAndRequest?.dependencies,
file.modificationStamp,
TimeStampedRequest(newFuture, currentTimeStamp)
)
return false // not changed immediately
}
private fun sendRequest(
path: String,
scriptDef: KotlinScriptDefinition,
dependenciesResolver: AsyncDependenciesResolver,
file: VirtualFile,
oldDataAndRequest: DataAndRequest?
): Pair<TimeStamp, CompletableFuture<*>> {
val currentTimeStamp = TimeStamps.next()
val newFuture = future(scriptDependencyUpdatesDispatcher) {
dependenciesResolver.resolveAsync(
getScriptContents(scriptDef, file),
(scriptDef as? KotlinScriptDefinitionFromAnnotatedTemplate)?.environment.orEmpty()
)
}.thenAccept { result ->
cacheLock.read {
val lastTimeStamp = cache[path]?.requestInProgress?.timeStamp
if (lastTimeStamp == currentTimeStamp) {
ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports)
if (cacheSync(result.dependencies ?: ScriptDependencies.Empty, oldDataAndRequest?.dependencies, path, file)) {
invalidateLocalCaches()
notifyRootsChanged()
}
}
}
}
return Pair(currentTimeStamp, newFuture)
}
private fun shouldSendNewRequest(file: VirtualFile, oldDataAndRequest: DataAndRequest?): Boolean {
val currentStamp = file.modificationStamp
val previousStamp = oldDataAndRequest?.modificationStamp
if (currentStamp != previousStamp) {
return true
}
return oldDataAndRequest.requestInProgress == null
}
private fun updateSync(file: VirtualFile, scriptDef: KotlinScriptDefinition): Boolean {
val path = file.path
val oldDeps = cache[path]?.dependencies
val newDeps = resolveDependencies(scriptDef, file) ?: ScriptDependencies.Empty
return cacheSync(newDeps, oldDeps, path, file)
}
private fun cacheSync(
new: ScriptDependencies,
old: ScriptDependencies?,
path: String,
file: VirtualFile
): Boolean {
return when {
old == null || !(new.match(old)) -> {
// changed or new
save(path, new, file, persist = true)
true
}
else -> {
save(path, new, file, persist = false)
// same
false
}
}
}
private fun save(path: String, new: ScriptDependencies?, virtualFile: VirtualFile, persist: Boolean) {
cacheLock.write {
cache.put(path, DataAndRequest(new, virtualFile.modificationStamp))
}
if (persist && new != null) {
ScriptDependenciesFileAttribute.write(virtualFile, new)
}
}
private fun invalidateLocalCaches() {
allScriptsClasspathCache.clear()
allScriptsClasspathScope.clear()
allLibrarySourcesCache.clear()
allLibrarySourcesScope.clear()
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project).getExtensionPoint(PsiElementFinder.EP_NAME).extensions
.filterIsInstance<KotlinScriptDependenciesClassFinder>()
.single()
kotlinScriptDependenciesClassFinder.clearCache()
}
fun getAllScriptsClasspathScope() = cache.allScriptsClasspathScope.get()
fun getAllLibrarySourcesScope() = cache.allLibrarySourcesScope.get()
fun getAllLibrarySources() = cache.allLibrarySourcesCache.get()
fun getAllScriptsClasspath() = cache.allScriptsClasspathCache.get()
companion object {
@JvmStatic
@@ -336,10 +90,9 @@ class KotlinScriptConfigurationManager(
fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) {
with(getInstance(project)) {
val scriptDefinition = KotlinScriptDefinitionProvider.getInstance(project)!!.findScriptDefinition(virtualFile)!!
val updated = updateSync(virtualFile, scriptDefinition)
val updated = cacheUpdater.updateSync(virtualFile, scriptDefinition)
assert(updated)
invalidateLocalCaches()
notifyRootsChanged()
cacheUpdater.onChange()
}
}
@@ -347,54 +100,8 @@ class KotlinScriptConfigurationManager(
fun reloadScriptDefinitions(project: Project) {
with(getInstance(project)) {
reloadScriptDefinitions()
cacheLock.write(cache::clear)
invalidateLocalCaches()
cacheUpdater.clear()
}
}
}
}
private class ClearableLazyValue<out T : Any>(private val lock: ReentrantReadWriteLock, private val compute: () -> T) {
private var value: T? = null
fun get(): T {
lock.read {
if (value == null) {
lock.write {
value = compute()
}
}
return value!!
}
}
fun clear() {
lock.write {
value = null
}
}
}
// TODO: relying on this to compare dependencies seems wrong, doesn't take javaHome and other stuff into account
private fun ScriptDependencies.match(other: ScriptDependencies)
= classpath.isSamePathListAs(other.classpath) &&
sources.toSet().isSamePathListAs(other.sources.toSet()) // TODO: gradle returns stdlib and reflect sources in unstable order for some reason
private fun Iterable<File>.isSamePathListAs(other: Iterable<File>): Boolean =
with(Pair(iterator(), other.iterator())) {
while (first.hasNext() && second.hasNext()) {
if (first.next().canonicalPath != second.next().canonicalPath) return false
}
!(first.hasNext() || second.hasNext())
}
data class TimeStamp(private val stamp: Long) {
operator fun compareTo(other: TimeStamp) = this.stamp.compareTo(other.stamp)
}
object TimeStamps {
private var current: Long = 0
fun next() = TimeStamp(current++)
}
@@ -0,0 +1,234 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectUtil
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
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 kotlinx.coroutines.experimental.asCoroutineDispatcher
import kotlinx.coroutines.experimental.future.future
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.script.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import kotlin.script.dependencies.ScriptDependencies
import kotlin.script.dependencies.experimental.AsyncDependenciesResolver
internal class ScriptDependenciesUpdater(
private val project: Project,
private val cache: DependenciesCache,
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
) {
private val requests = ConcurrentHashMap<String, ModStampedRequest>()
private val scriptDependencyUpdatesDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
private val contentLoader = ScriptContentLoader(project)
init {
listenToVfsChanges()
}
private class TimeStampedRequest(val future: CompletableFuture<*>, val timeStamp: TimeStamp)
private class ModStampedRequest(
val modificationStamp: Long,
val request: TimeStampedRequest? = null
)
fun getCurrentDependencies(file: VirtualFile): ScriptDependencies {
cache[file]?.let { return it }
tryLoadingFromDisk(file)
updateCache(listOf(file))
return cache[file] ?: ScriptDependencies.Empty
}
private fun tryLoadingFromDisk(file: VirtualFile) {
ScriptDependenciesFileAttribute.read(file)?.let { deserialized ->
cache.save(file, deserialized)
onChange()
}
}
private fun updateCache(files: Iterable<VirtualFile>) =
files.map { file ->
if (!file.isValid) {
return cache.delete(file)
}
else {
updateForFile(file)
}
}.contains(true)
private fun updateForFile(file: VirtualFile): Boolean {
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file) ?: return false
return when (scriptDef.dependencyResolver) {
is AsyncDependenciesResolver -> {
updateAsync(file, scriptDef)
return false
}
else -> updateSync(file, scriptDef)
}
}
private fun updateAsync(
file: VirtualFile,
scriptDefinition: KotlinScriptDefinition
) {
val path = file.path
val lastRequest = requests[path]
if (!shouldSendNewRequest(file, lastRequest)) {
return
}
lastRequest?.request?.future?.cancel(true)
val (currentTimeStamp, newFuture) = sendRequest(file, scriptDefinition)
requests[path] = ModStampedRequest(
file.modificationStamp,
TimeStampedRequest(newFuture, currentTimeStamp)
)
return
}
private fun shouldSendNewRequest(file: VirtualFile, previousRequest: ModStampedRequest?): Boolean {
val currentStamp = file.modificationStamp
val previousStamp = previousRequest?.modificationStamp
if (currentStamp != previousStamp) {
return true
}
return previousRequest.request == null
}
private fun sendRequest(
file: VirtualFile,
scriptDef: KotlinScriptDefinition
): Pair<TimeStamp, CompletableFuture<*>> {
val currentTimeStamp = TimeStamps.next()
val dependenciesResolver = scriptDef.dependencyResolver as AsyncDependenciesResolver
val path = file.path
val newFuture = future(scriptDependencyUpdatesDispatcher) {
dependenciesResolver.resolveAsync(
contentLoader.getScriptContents(scriptDef, file),
(scriptDef as? KotlinScriptDefinitionFromAnnotatedTemplate)?.environment.orEmpty()
)
}.thenAccept { result ->
val lastTimeStamp = requests[path]?.request?.timeStamp
if (lastTimeStamp == currentTimeStamp) {
ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports)
if (cache(result.dependencies ?: ScriptDependencies.Empty, file)) {
onChange()
}
}
}
return Pair(currentTimeStamp, newFuture)
}
fun updateSync(file: VirtualFile, scriptDef: KotlinScriptDefinition): Boolean {
val newDeps = contentLoader.loadContentsAndResolveDependencies(scriptDef, file) ?: ScriptDependencies.Empty
return cache(newDeps, file)
}
private fun cache(
new: ScriptDependencies,
file: VirtualFile
): Boolean {
val updated = cache.save(file, new)
if (updated) {
ScriptDependenciesFileAttribute.write(file, new)
}
return updated
}
fun onChange() {
cache.onChange()
notifyRootsChanged()
}
private fun notifyRootsChanged() {
val rootsChangesRunnable = {
runWriteAction {
if (project.isDisposed) return@runWriteAction
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) {
rootsChangesRunnable.invoke()
}
else {
application.invokeLater(rootsChangesRunnable, ModalityState.defaultModalityState())
}
}
private fun listenToVfsChanges() {
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() {
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val application = ApplicationManager.getApplication()
override fun after(events: List<VFileEvent>) {
if (updateCache(events.mapNotNull {
// The check is partly taken from the BuildManager.java
it.file?.takeIf {
// the isUnitTestMode check fixes ScriptConfigurationHighlighting & Navigation tests, since they are not trigger proper update mechanims
// TODO: find out the reason, then consider to fix tests and remove this check
(application.isUnitTestMode || projectFileIndex.isInContent(it)) && !ProjectUtil.isProjectOrWorkspaceFile(it)
}
})) {
onChange()
}
}
})
}
fun clear() {
cache.clear()
requests.clear()
}
}
private data class TimeStamp(private val stamp: Long) {
operator fun compareTo(other: TimeStamp) = this.stamp.compareTo(other.stamp)
}
private object TimeStamps {
private var current: Long = 0
fun next() = TimeStamp(current++)
}