Refactor KotlinScriptConfigurationManager: extract parts
Introduce ScriptDependenciesUpdater, DependenciesCache and ScriptContentLoader
This commit is contained in:
+5
-4
@@ -21,7 +21,7 @@ import com.intellij.openapi.project.Project
|
|||||||
import com.intellij.openapi.vfs.VirtualFile
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
import org.jetbrains.kotlin.script.KotlinScriptDefinitionProvider
|
import org.jetbrains.kotlin.script.KotlinScriptDefinitionProvider
|
||||||
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProvider
|
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProvider
|
||||||
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProviderBase
|
import org.jetbrains.kotlin.script.ScriptContentLoader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||||
import kotlin.concurrent.read
|
import kotlin.concurrent.read
|
||||||
@@ -29,12 +29,13 @@ import kotlin.concurrent.write
|
|||||||
import kotlin.script.dependencies.ScriptDependencies
|
import kotlin.script.dependencies.ScriptDependencies
|
||||||
|
|
||||||
class KotlinScriptExternalImportsProviderImpl(
|
class KotlinScriptExternalImportsProviderImpl(
|
||||||
val project: Project,
|
project: Project,
|
||||||
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
|
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
|
||||||
) : KotlinScriptExternalImportsProviderBase(project) {
|
) : KotlinScriptExternalImportsProvider {
|
||||||
|
|
||||||
private val cacheLock = ReentrantReadWriteLock()
|
private val cacheLock = ReentrantReadWriteLock()
|
||||||
private val cache = hashMapOf<String, ScriptDependencies?>()
|
private val cache = hashMapOf<String, ScriptDependencies?>()
|
||||||
|
private val scriptContentLoader = ScriptContentLoader(project)
|
||||||
|
|
||||||
override fun getScriptDependencies(file: VirtualFile): ScriptDependencies? = cacheLock.read {
|
override fun getScriptDependencies(file: VirtualFile): ScriptDependencies? = cacheLock.read {
|
||||||
calculateExternalDependencies(file)
|
calculateExternalDependencies(file)
|
||||||
@@ -47,7 +48,7 @@ class KotlinScriptExternalImportsProviderImpl(
|
|||||||
else {
|
else {
|
||||||
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file)
|
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file)
|
||||||
if (scriptDef != null) {
|
if (scriptDef != null) {
|
||||||
val deps = resolveDependencies(scriptDef, file)
|
val deps = scriptContentLoader.loadContentsAndResolveDependencies(scriptDef, file)
|
||||||
|
|
||||||
if (deps != null) {
|
if (deps != null) {
|
||||||
log.info("[kts] new cached deps for $path: ${deps.classpath.joinToString(File.pathSeparator)}")
|
log.info("[kts] new cached deps for $path: ${deps.classpath.joinToString(File.pathSeparator)}")
|
||||||
|
|||||||
+2
-2
@@ -28,7 +28,7 @@ import kotlin.reflect.KClass
|
|||||||
import kotlin.script.dependencies.ScriptContents
|
import kotlin.script.dependencies.ScriptContents
|
||||||
import kotlin.script.dependencies.ScriptDependencies
|
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)
|
fun getScriptContents(scriptDefinition: KotlinScriptDefinition, file: VirtualFile)
|
||||||
= BasicScriptContents(file, getAnnotations = { loadAnnotations(scriptDefinition, file) })
|
= 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() }
|
override val text: CharSequence? by lazy { virtualFile.inputStream.reader(charset = virtualFile.charset).readText() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveDependencies(
|
fun loadContentsAndResolveDependencies(
|
||||||
scriptDef: KotlinScriptDefinition,
|
scriptDef: KotlinScriptDefinition,
|
||||||
file: VirtualFile
|
file: VirtualFile
|
||||||
): ScriptDependencies? {
|
): 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-308
@@ -16,37 +16,18 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.idea.core.script
|
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.components.ServiceManager
|
||||||
import com.intellij.openapi.diagnostic.Logger
|
import com.intellij.openapi.diagnostic.Logger
|
||||||
import com.intellij.openapi.extensions.Extensions
|
|
||||||
import com.intellij.openapi.project.Project
|
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.StandardFileSystems
|
||||||
import com.intellij.openapi.vfs.VirtualFile
|
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 com.intellij.util.io.URLUtil
|
||||||
import kotlinx.coroutines.experimental.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.experimental.future.future
|
|
||||||
import org.jetbrains.annotations.TestOnly
|
import org.jetbrains.annotations.TestOnly
|
||||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
import org.jetbrains.kotlin.script.KotlinScriptDefinitionProvider
|
||||||
import org.jetbrains.kotlin.script.*
|
import org.jetbrains.kotlin.script.KotlinScriptExternalImportsProvider
|
||||||
|
import org.jetbrains.kotlin.script.makeScriptDefsFromTemplatesProviderExtensions
|
||||||
import java.io.File
|
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.ScriptDependencies
|
||||||
import kotlin.script.dependencies.experimental.AsyncDependenciesResolver
|
|
||||||
|
|
||||||
|
|
||||||
// NOTE: this service exists exclusively because KotlinScriptConfigurationManager
|
// NOTE: this service exists exclusively because KotlinScriptConfigurationManager
|
||||||
@@ -62,253 +43,26 @@ class IdeScriptExternalImportsProvider(
|
|||||||
class KotlinScriptConfigurationManager(
|
class KotlinScriptConfigurationManager(
|
||||||
private val project: Project,
|
private val project: Project,
|
||||||
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
|
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider
|
||||||
) : KotlinScriptExternalImportsProviderBase(project) {
|
) {
|
||||||
private val cacheLock = ReentrantReadWriteLock()
|
private val cache = DependenciesCache(project)
|
||||||
private val scriptDependencyUpdatesDispatcher = newFixedThreadPool(1).asCoroutineDispatcher()
|
private val cacheUpdater = ScriptDependenciesUpdater(project, cache, scriptDefinitionProvider)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
reloadScriptDefinitions()
|
reloadScriptDefinitions()
|
||||||
listenToVfsChanges()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenToVfsChanges() {
|
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> = toVfsRoots(cacheUpdater.getCurrentDependencies(file).classpath)
|
||||||
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() {
|
fun getScriptDependencies(file: VirtualFile) = cacheUpdater.getCurrentDependencies(file)
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
private fun reloadScriptDefinitions() {
|
private fun reloadScriptDefinitions() {
|
||||||
val def = makeScriptDefsFromTemplatesProviderExtensions(project, { ep, ex -> log.warn("[kts] Error loading definition from ${ep.id}", ex) })
|
val def = makeScriptDefsFromTemplatesProviderExtensions(project, { ep, ex -> log.warn("[kts] Error loading definition from ${ep.id}", ex) })
|
||||||
scriptDefinitionProvider.setScriptDefinitions(def)
|
scriptDefinitionProvider.setScriptDefinitions(def)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TimeStampedRequest(val future: CompletableFuture<*>, val timeStamp: TimeStamp)
|
fun getAllScriptsClasspathScope() = cache.allScriptsClasspathScope.get()
|
||||||
|
fun getAllLibrarySourcesScope() = cache.allLibrarySourcesScope.get()
|
||||||
private class DataAndRequest(
|
fun getAllLibrarySources() = cache.allLibrarySourcesCache.get()
|
||||||
val dependencies: ScriptDependencies?,
|
fun getAllScriptsClasspath() = cache.allScriptsClasspathCache.get()
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@@ -336,10 +90,9 @@ class KotlinScriptConfigurationManager(
|
|||||||
fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) {
|
fun updateScriptDependenciesSynchronously(virtualFile: VirtualFile, project: Project) {
|
||||||
with(getInstance(project)) {
|
with(getInstance(project)) {
|
||||||
val scriptDefinition = KotlinScriptDefinitionProvider.getInstance(project)!!.findScriptDefinition(virtualFile)!!
|
val scriptDefinition = KotlinScriptDefinitionProvider.getInstance(project)!!.findScriptDefinition(virtualFile)!!
|
||||||
val updated = updateSync(virtualFile, scriptDefinition)
|
val updated = cacheUpdater.updateSync(virtualFile, scriptDefinition)
|
||||||
assert(updated)
|
assert(updated)
|
||||||
invalidateLocalCaches()
|
cacheUpdater.onChange()
|
||||||
notifyRootsChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,54 +100,8 @@ class KotlinScriptConfigurationManager(
|
|||||||
fun reloadScriptDefinitions(project: Project) {
|
fun reloadScriptDefinitions(project: Project) {
|
||||||
with(getInstance(project)) {
|
with(getInstance(project)) {
|
||||||
reloadScriptDefinitions()
|
reloadScriptDefinitions()
|
||||||
cacheLock.write(cache::clear)
|
cacheUpdater.clear()
|
||||||
invalidateLocalCaches()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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++)
|
|
||||||
}
|
|
||||||
+234
@@ -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++)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user