Refactor the mechanism how script configurations are updated

Introduce file attributes cache, which is only applicable when no dependencies are cached in memory cache
Refactor ScriptDependenciesCache to memory cache that is able to get configuration from cache and checks if it is up to date
Move all public methods to ScriptDependenciesManager
Introduce ScriptClassRootsManager that checks if root change event should be called after script configuration update
This commit is contained in:
Natalia Selezneva
2019-08-22 11:59:32 +03:00
parent dc46f73ecf
commit 961e8c2c74
37 changed files with 1025 additions and 898 deletions
@@ -431,7 +431,7 @@ public abstract class CodegenTestCase extends KtUsefulTestCase {
if (externalImportsProvider != null) {
myEnvironment.getSourceFiles().forEach(
file -> {
ScriptCompilationConfigurationWrapper refinedConfiguration = ErrorHandlingKt.valueOrNull(externalImportsProvider.getScriptConfigurationResult(file));
ScriptCompilationConfigurationWrapper refinedConfiguration = externalImportsProvider.getScriptConfiguration(file);
if (refinedConfiguration != null) {
files.addAll(refinedConfiguration.getDependenciesClassPath());
}
@@ -105,7 +105,7 @@ sealed class ScriptDependenciesInfo(val project: Project) : IdeaModuleInfo, Bina
class ForProject(project: Project) : ScriptDependenciesInfo(project) {
override val sdk: Sdk?
get() {
return ScriptDependenciesManager.getInstance(project).getAllScriptsSdks().firstOrNull()
return ScriptDependenciesManager.getInstance(project).getFirstScriptsSdk()
?: ScriptDependenciesManager.getScriptDefaultSdk(project)
}
@@ -22,7 +22,7 @@ import org.jetbrains.kotlin.idea.caches.project.NotUnderContentRootModuleInfo
import org.jetbrains.kotlin.idea.caches.project.getModuleInfo
import org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.isRunningInCidrIde
import org.jetbrains.kotlin.psi.KtCodeFragment
@@ -70,7 +70,7 @@ object KotlinHighlightingUtil {
@Suppress("DEPRECATION")
private fun shouldHighlightScript(ktFile: KtFile): Boolean {
if (isRunningInCidrIde) return false // There is no Java support in CIDR. So do not highlight errors in KTS if running in CIDR.
if (!ScriptsCompilationConfigurationUpdater.areDependenciesCached(ktFile)) return false
if (!ScriptDependenciesManager.getInstance(ktFile.project).isConfigurationCached(ktFile)) return false
if (IdeScriptReportSink.getReports(ktFile).any { it.severity == ScriptDiagnostic.Severity.FATAL }) {
return false
}
@@ -16,13 +16,9 @@
package org.jetbrains.kotlin.idea.core.script
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.ui.EditorNotifications
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink
@@ -34,15 +30,6 @@ class IdeScriptReportSink(val project: Project) : ScriptReportSink {
// TODO: persist errors between launches?
scriptFile.scriptDiagnostics = reports
ApplicationManager.getApplication().invokeLater {
if (scriptFile.isValid && !project.isDisposed) {
PsiManager.getInstance(project).findFile(scriptFile)?.let { psiFile ->
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
EditorNotifications.getInstance(project).updateNotifications(scriptFile)
}
}
}
}
companion object {
@@ -0,0 +1,87 @@
/*
* 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.core.script
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.atomic.AtomicInteger
/**
* Utility for postponing indexing of new roots at the end of some bulk operation.
*/
internal class ScriptClassRootsManager(val project: Project) {
private var newRootsPresent: Boolean = false
private val concurrentTransactions = AtomicInteger()
fun checkNonCachedRoots(
cache: ScriptConfigurationMemoryCache,
file: VirtualFile,
configuration: ScriptCompilationConfigurationWrapper
) {
checkInTransaction()
if (cache.hasNotCachedRoots(configuration)) {
synchronized(this) {
debug(file) { "new class roots found: $configuration" }
checkInTransaction()
newRootsPresent = true
}
}
}
private fun checkInTransaction() {
check(concurrentTransactions.get() > 0)
}
inline fun transaction(body: () -> Unit) {
startTransaction()
try {
body()
} finally {
commit()
}
}
fun startTransaction() {
concurrentTransactions.incrementAndGet()
}
fun commit() {
concurrentTransactions.decrementAndGet()
// run indexing even in inner transaction
// (outer transaction may be async, so it would be better to not wait it)
startIndexingIfNeeded()
}
@Synchronized
private fun startIndexingIfNeeded() {
if (!newRootsPresent) return
newRootsPresent = false
val doNotifyRootsChanged = Runnable {
runWriteAction {
if (project.isDisposed) return@runWriteAction
debug { "roots change event" }
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
TransactionGuard.submitTransaction(project, doNotifyRootsChanged)
} else {
TransactionGuard.getInstance().submitTransactionLater(project, doNotifyRootsChanged)
}
}
}
@@ -0,0 +1,335 @@
/*
* 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.core.script
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager.Companion.toVfsRoots
import org.jetbrains.kotlin.idea.core.script.dependencies.*
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.valueOrNull
class ScriptConfigurationManagerImpl internal constructor(
private val project: Project
) : ScriptConfigurationManager, ScriptDependenciesManager {
private val rootsManager = ScriptClassRootsManager(project)
private val memoryCache = ScriptConfigurationMemoryCache(project)
private val fileAttributesCache = ScriptConfigurationFileAttributeCache()
private val fromRefinedLoader = FromRefinedConfigurationLoader()
private val loaders = arrayListOf(
OutsiderFileDependenciesLoader(this),
fromRefinedLoader,
fileAttributesCache
)
private val backgroundLoader = BackgroundLoader(project, rootsManager, ::reloadConfigurationAsync)
private val listener = ScriptsListener(project, this)
/**
* Save configurations into cache.
* Start indexing for new class/source roots.
* Re-highlight opened scripts with changed configuration.
*/
override fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, ScriptCompilationConfigurationResult>>) {
rootsManager.transaction {
for ((file, result) in files) {
saveConfiguration(file, result, skipNotification = true)
}
}
}
/**
* Start configuration update for files if configuration isn't up to date.
* Start indexing for new class/source roots.
*
* @return true if update was started for any file, false if all configurations are cached
*/
override fun updateConfigurationsIfNotCached(files: List<KtFile>): Boolean {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
val notCached = files.filterNot { isConfigurationUpToDate(it.originalFile.virtualFile) }
if (notCached.isNotEmpty()) {
rootsManager.transaction {
for (file in notCached) {
reloadConfiguration(file)
}
}
return true
}
return false
}
/**
* Check if configuration is already cached for [file] (in cache or FileAttributes).
* Don't check if file was changed after the last update.
* Supposed to be used to switch highlighting off for scripts without configuration.
* to avoid all file being highlighted in red.
*/
override fun isConfigurationCached(file: KtFile): Boolean {
return isConfigurationCached(file.originalFile.virtualFile)
}
/**
* Clear configuration caches
* Start re-highlighting for opened scripts
*/
override fun clearConfigurationCachesAndRehighlight() {
clearAndRehighlight()
}
@Deprecated("Use getScriptClasspath(KtFile) instead")
override fun getScriptClasspath(file: VirtualFile): List<VirtualFile> {
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return emptyList()
return getScriptClasspath(ktFile)
}
override fun getScriptClasspath(file: KtFile): List<VirtualFile> =
toVfsRoots(getConfiguration(file)?.dependenciesClassPath.orEmpty())
override fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper? {
val virtualFile = file.originalFile.virtualFile
val cached = getCachedConfiguration(virtualFile)
if (cached != null) {
return cached
}
if (ScriptDefinitionsManager.getInstance(project).isReady() && !isConfigurationUpToDate(virtualFile)) {
rootsManager.transaction {
reloadConfiguration(file)
}
}
return getCachedConfiguration(virtualFile)
}
override fun getScriptDependenciesClassFilesScope(file: VirtualFile) = scriptDependenciesClassFilesScope(file)
override fun getScriptSdk(file: VirtualFile) = scriptSdk(file)
override fun getFirstScriptsSdk() = memoryCache.firstScriptSdk
override fun getAllScriptsDependenciesClassFilesScope() = memoryCache.allDependenciesClassFilesScope
override fun getAllScriptDependenciesSourcesScope() = memoryCache.allDependenciesSourcesScope
override fun getAllScriptsDependenciesClassFiles() = memoryCache.allDependenciesClassFiles
override fun getAllScriptDependenciesSources() = memoryCache.allDependenciesSources
@TestOnly
fun updateScriptDependenciesSynchronously(file: PsiFile) {
val scriptDefinition = file.findScriptDefinition() ?: return
assert(file is KtFile) {
"PsiFile should be a KtFile, otherwise script dependencies cannot be loaded"
}
if (isConfigurationUpToDate(file.virtualFile)) return
rootsManager.transaction {
val result = fromRefinedLoader.loadDependencies(true, file as KtFile, scriptDefinition)
if (result != null) {
saveConfiguration(file.originalFile.virtualFile, result, skipNotification = true, skipSaveToAttributes = false)
}
}
}
private fun reloadConfiguration(file: KtFile) {
val virtualFile = file.originalFile.virtualFile
memoryCache.setUpToDate(virtualFile)
val scriptDefinition = file.findScriptDefinition() ?: return
val syncLoaders = loaders.filterNot { it.isAsync(file, scriptDefinition) }
reloadConfigurationBy(file, scriptDefinition, syncLoaders)
val asyncLoaders = loaders - syncLoaders
if (asyncLoaders.isNotEmpty()) {
backgroundLoader.scheduleAsync(file)
}
}
private fun reloadConfigurationAsync(file: KtFile) {
val scriptDefinition = file.findScriptDefinition() ?: return
val asyncLoaders = loaders.filter { it.isAsync(file, scriptDefinition) }
if (asyncLoaders.size > 1) {
LOG.warn("There are more than one async compilation configuration loader. " +
"This mean that the last one will overwrite the results of the previous ones: " +
asyncLoaders.joinToString { it.javaClass.name })
}
reloadConfigurationBy(file, scriptDefinition, asyncLoaders)
}
private fun reloadConfigurationBy(file: KtFile, scriptDefinition: ScriptDefinition, loaders: List<ScriptDependenciesLoader>) {
val firstLoad = memoryCache.getCachedConfiguration(file.originalFile.virtualFile) == null
loaders.forEach { loader ->
val result = loader.loadDependencies(firstLoad, file, scriptDefinition)
if (result != null) {
return saveConfiguration(file.originalFile.virtualFile, result, loader.skipNotification, loader.skipSaveToAttributes)
}
}
}
/**
* Save [newResult] for [file] into caches and update highlih.
* Should be called inside `rootsManager.transaction { ... }`.
*
* @param skipNotification forces loading new configuration even if auto reload is disabled.
* @param skipSaveToAttributes skips saving to FileAttributes (used in [ScriptConfigurationFileAttributeCache] only).
*
* @sample ScriptDependenciesManager.getConfiguration
*/
private fun saveConfiguration(
file: VirtualFile,
newResult: ScriptCompilationConfigurationResult,
skipNotification: Boolean = false,
skipSaveToAttributes: Boolean = false
) {
debug(file) { "configuration received = $newResult" }
saveReports(file, newResult.reports)
val newConfiguration = newResult.valueOrNull()
if (newConfiguration != null) {
val oldConfiguration = getCachedConfiguration(file)
if (oldConfiguration == newConfiguration) {
file.removeScriptDependenciesNotificationPanel(project)
} else {
val autoReload = skipNotification
|| oldConfiguration == null
|| KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
|| ApplicationManager.getApplication().isUnitTestMode
if (autoReload) {
if (oldConfiguration != null) {
file.removeScriptDependenciesNotificationPanel(project)
}
saveChangedConfiguration(file, newConfiguration, skipSaveToAttributes)
} else {
debug(file) {
"configuration changed, notification is shown: old = $oldConfiguration, new = $newConfiguration"
}
file.addScriptDependenciesNotificationPanel(
newConfiguration, project,
onClick = {
file.removeScriptDependenciesNotificationPanel(project)
rootsManager.transaction {
saveChangedConfiguration(file, it, skipSaveToAttributes)
}
}
)
}
}
}
}
private fun saveChangedConfiguration(
file: VirtualFile,
newConfiguration: ScriptCompilationConfigurationWrapper?,
skipSaveToAttributes: Boolean
) {
debug(file) { "configuration changed = $newConfiguration" }
if (newConfiguration != null) {
rootsManager.checkNonCachedRoots(memoryCache, file, newConfiguration)
if (!skipSaveToAttributes) {
debug(file) { "configuration saved to file attributes: $newConfiguration" }
fileAttributesCache.save(file, newConfiguration)
}
memoryCache.replaceConfiguration(file, newConfiguration)
memoryCache.clearClassRootsCaches()
}
updateHighlighting(listOf(file))
}
private fun saveReports(
file: VirtualFile,
newReports: List<ScriptDiagnostic>
) {
val oldReports = IdeScriptReportSink.getReports(file)
if (oldReports != newReports) {
debug(file) { "new script reports = $newReports" }
ServiceManager.getService(project, ScriptReportSink::class.java).attachReports(file, newReports)
GlobalScope.launch(EDT(project)) {
if (project.isDisposed) return@launch
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
}
private fun updateHighlighting(files: List<VirtualFile>) {
if (files.isEmpty()) return
GlobalScope.launch(EDT(project)) {
if (project.isDisposed) return@launch
val openFiles = FileEditorManager.getInstance(project).openFiles
val openScripts = files.filter { it.isValid && openFiles.contains(it) }
openScripts.forEach {
PsiManager.getInstance(project).findFile(it)?.let { psiFile ->
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
}
}
}
}
private fun getCachedConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? =
memoryCache.getCachedConfiguration(file)
private fun isConfigurationCached(file: VirtualFile): Boolean {
return getCachedConfiguration(file) != null || file in fileAttributesCache
}
private fun isConfigurationUpToDate(file: VirtualFile): Boolean {
return isConfigurationCached(file) && memoryCache.isConfigurationUpToDate(file)
}
private fun clearAndRehighlight() {
updateHighlighting(memoryCache.clearConfigurationCaches())
}
private fun scriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return memoryCache.scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE
}
private fun scriptSdk(file: VirtualFile): Sdk? {
return memoryCache.scriptsSdksCache[file]
}
}
@@ -6,7 +6,6 @@
package org.jetbrains.kotlin.idea.core.script
import com.intellij.ProjectTopics
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
@@ -23,12 +22,10 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.ConcurrentFactoryMap
import com.intellij.util.containers.SLRUMap
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationMemoryCache.Companion.MAX_SCRIPTS_CACHED
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
@@ -36,9 +33,8 @@ import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.isAccessible
import kotlin.script.experimental.api.valueOrNull
class ScriptsCompilationConfigurationCache(private val project: Project) {
class ScriptConfigurationMemoryCache internal constructor(private val project: Project) {
companion object {
const val MAX_SCRIPTS_CACHED = 50
@@ -48,29 +44,37 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
val connection = project.messageBus.connect()
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
clearCaches()
clearClassRootsCaches()
}
})
}
private val cacheLock = ReentrantReadWriteLock()
private val scriptDependenciesCache = SLRUCacheWithLock<VirtualFile, ScriptCompilationConfigurationResult>()
private val scriptsModificationStampsCache = SLRUCacheWithLock<KtFile, Long>()
private val scriptDependenciesCache = SLRUCacheWithLock<VirtualFile, ScriptCompilationConfigurationWrapper>()
private val scriptsModificationStampsCache = SLRUCacheWithLock<VirtualFile, Long>()
operator fun get(file: VirtualFile): ScriptCompilationConfigurationResult? = scriptDependenciesCache.get(file)
fun getCachedConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? = scriptDependenciesCache.get(file)
fun shouldRunDependenciesUpdate(file: KtFile): Boolean {
return scriptsModificationStampsCache.replace(file, file.modificationStamp) != file.modificationStamp
fun isConfigurationUpToDate(file: VirtualFile): Boolean {
return scriptsModificationStampsCache.get(file) == file.modificationStamp
}
private val scriptsDependenciesClasspathScopeCache: MutableMap<VirtualFile, GlobalSearchScope> =
fun setUpToDate(file: VirtualFile) {
scriptsModificationStampsCache.replace(file, file.modificationStamp)
}
fun replaceConfiguration(file: VirtualFile, new: ScriptCompilationConfigurationWrapper) {
scriptDependenciesCache.replace(file, new)
}
val scriptsDependenciesClasspathScopeCache: MutableMap<VirtualFile, GlobalSearchScope> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = getCachedConfigurationOrLoad(file)
?: return@createWeakMap null
val compilationConfiguration = getConfiguration(file)
?: return@createWeakMap GlobalSearchScope.EMPTY_SCOPE
val roots = compilationConfiguration.dependenciesClassPath
val sdk = getScriptSdk(file)
val sdk = scriptsSdksCache[file]
@Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) {
@@ -83,29 +87,19 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
)
}
fun scriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE
}
private val scriptsSdksCache: MutableMap<VirtualFile, Sdk?> =
val scriptsSdksCache: MutableMap<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = getCachedConfigurationOrLoad(file)
val compilationConfiguration = getConfiguration(file)
return@createWeakMap getScriptSdk(compilationConfiguration) ?: ScriptDependenciesManager.getScriptDefaultSdk(project)
}
fun getScriptSdk(file: VirtualFile): Sdk? {
return scriptsSdksCache[file]
}
private fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
val configuration = getCachedConfiguration(file)
private fun getCachedConfigurationOrLoad(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
val compilationConfiguration = scriptDependenciesCache.get(file)?.valueOrNull()
if (configuration != null) return configuration
if (compilationConfiguration != null) {
return compilationConfiguration
}
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null
return ScriptDependenciesManager.getInstance(project).getRefinedCompilationConfiguration(ktFile)?.valueOrNull()
val ktFile = runReadAction { PsiManager.getInstance(project).findFile(file) as? KtFile } ?: return null
return ScriptDependenciesManager.getInstance(project).getConfiguration(ktFile)
}
private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? {
@@ -119,15 +113,21 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
return getAllProjectSdks().find { it.homeDirectory == javaHome }
}
val allSdks by ClearableLazyValue(cacheLock) {
val firstScriptSdk: Sdk?
get() {
val firstCachedScript = scriptDependenciesCache.getAll().firstOrNull()?.key ?: return null
return scriptsSdksCache[firstCachedScript]
}
private val allSdks by ClearableLazyValue(cacheLock) {
scriptDependenciesCache.getAll()
.mapNotNull { getScriptSdk(it.key) }
.mapNotNull { scriptsSdksCache[it.key] }
.distinct()
}
private val allNonIndexedSdks by ClearableLazyValue(cacheLock) {
scriptDependenciesCache.getAll()
.mapNotNull { getScriptSdk(it.key) }
.mapNotNull { scriptsSdksCache[it.key] }
.filterNonModuleSdk()
.distinct()
}
@@ -137,7 +137,7 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
.flatMap { it.rootProvider.getFiles(OrderRootType.CLASSES).toList() }
val scriptDependenciesClasspath = scriptDependenciesCache.getAll()
.flatMap { it.value.valueOrNull()?.dependenciesClassPath ?: emptyList() }.distinct()
.flatMap { it.value.dependenciesClassPath }.distinct()
sdkFiles + ScriptDependenciesManager.toVfsRoots(scriptDependenciesClasspath)
}
@@ -147,7 +147,7 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
.flatMap { it.rootProvider.getFiles(OrderRootType.SOURCES).toList() }
val scriptDependenciesSources = scriptDependenciesCache.getAll()
.flatMap { it.value.valueOrNull()?.dependenciesSources ?: emptyList() }.distinct()
.flatMap { it.value.dependenciesSources }.distinct()
sdkSources + ScriptDependenciesManager.toVfsRoots(scriptDependenciesSources)
}
@@ -164,12 +164,20 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
return filterNot { moduleSdks.contains(it) }
}
private fun onChange(files: List<VirtualFile>) {
clearCaches()
updateHighlighting(files)
fun clearConfigurationCaches(): List<VirtualFile> {
debug { "configuration caches cleared" }
val keys = scriptDependenciesCache.getAll().map { it.key }.toList()
scriptDependenciesCache.clear()
scriptsModificationStampsCache.clear()
return keys
}
private fun clearCaches() {
fun clearClassRootsCaches() {
debug { "class roots caches cleared" }
this::allSdks.clearValue()
this::allNonIndexedSdks.clearValue()
@@ -180,6 +188,7 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
this::allDependenciesSourcesScope.clearValue()
scriptsDependenciesClasspathScopeCache.clear()
scriptsSdksCache.clear()
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project).getExtensionPoint(PsiElementFinder.EP_NAME).extensions
@@ -187,53 +196,36 @@ class ScriptsCompilationConfigurationCache(private val project: Project) {
.single()
kotlinScriptDependenciesClassFinder.clearCache()
}
private fun updateHighlighting(files: List<VirtualFile>) {
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
GlobalScope.launch(EDT(project)) {
files.filter { it.isValid }.forEach {
PsiManager.getInstance(project).findFile(it)?.let { psiFile ->
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
}
}
}
}
fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean {
val scriptSdk = getScriptSdk(compilationConfiguration) ?: ScriptDependenciesManager.getScriptDefaultSdk(project)
return (scriptSdk != null && !allSdks.contains(scriptSdk)) ||
!allDependenciesClassFiles.containsAll(ScriptDependenciesManager.toVfsRoots(compilationConfiguration.dependenciesClassPath)) ||
!allDependenciesSources.containsAll(ScriptDependenciesManager.toVfsRoots(compilationConfiguration.dependenciesSources))
}
fun clear() {
val keys = scriptDependenciesCache.getAll().map { it.key }.toList()
scriptDependenciesCache.clear()
scriptsModificationStampsCache.clear()
updateHighlighting(keys)
}
fun save(file: VirtualFile, new: ScriptCompilationConfigurationResult): Boolean {
val old = scriptDependenciesCache.replace(file, new)
val changed = new != old
if (changed) {
onChange(listOf(file))
val wasSdkChanged = scriptSdk != null && !allSdks.contains(scriptSdk)
if (wasSdkChanged) {
debug { "sdk was changed: $compilationConfiguration" }
return true
}
return changed
val newClassRoots = ScriptDependenciesManager.toVfsRoots(compilationConfiguration.dependenciesClassPath)
for (newClassRoot in newClassRoots) {
if (!allDependenciesClassFiles.contains(newClassRoot)) {
debug { "class root was changed: $newClassRoot" }
return true
}
}
val newSourceRoots = ScriptDependenciesManager.toVfsRoots(compilationConfiguration.dependenciesSources)
for (newSourceRoot in newSourceRoots) {
if (!allDependenciesSources.contains(newSourceRoot)) {
debug { "source root was changed: $newSourceRoot" }
return true
}
}
return false
}
fun delete(file: VirtualFile): Boolean {
val changed = scriptDependenciesCache.remove(file)
if (changed) {
onChange(listOf(file))
}
return changed
}
}
private fun <R> KProperty0<R>.clearValue() {
@@ -268,8 +260,8 @@ private class SLRUCacheWithLock<K, V> {
private val lock = ReentrantReadWriteLock()
val cache = SLRUMap<K, V>(
ScriptsCompilationConfigurationCache.MAX_SCRIPTS_CACHED,
ScriptsCompilationConfigurationCache.MAX_SCRIPTS_CACHED
MAX_SCRIPTS_CACHED,
MAX_SCRIPTS_CACHED
)
fun get(value: K): V? = lock.write {
@@ -36,7 +36,6 @@ import com.intellij.openapi.projectRoots.ex.PathUtilEx
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotifications
import com.intellij.util.containers.SLRUMap
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.project.SdkInfo
@@ -191,13 +190,7 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
scriptDefinitionsCacheLock.write { scriptDefinitionsCache.clear() }
// TODO: clear by script type/definition
ServiceManager.getService(project, ScriptsCompilationConfigurationCache::class.java).clear()
ApplicationManager.getApplication().invokeLater {
if (!project.isDisposed) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
ScriptDependenciesManager.getInstance(project).clearConfigurationCachesAndRehighlight()
}
private fun ScriptDefinitionsSource.safeGetDefinitions(): List<ScriptDefinition> {
@@ -218,9 +211,6 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
}
}
private val LOG = Logger.getInstance("ScriptTemplatesProviders")
// TODO: consider rewriting to return sequence
fun loadDefinitionsFromTemplates(
templateClassNames: List<String>,
@@ -1,83 +0,0 @@
/*
* 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.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.util.*
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.getScriptOriginalFile
import java.io.*
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.dependencies.ScriptDependencies
var KtFile.scriptDependencies: ScriptDependencies?
get() = this.getScriptOriginalFile()?.virtualFile?.scriptDependencies
set(value) {
this.getScriptOriginalFile()?.virtualFile?.scriptDependencies = value
}
var VirtualFile.scriptDependencies: ScriptDependencies? by cachedFileAttribute(
name = "kotlin-script-dependencies",
version = 3,
read = {
ScriptDependencies(
classpath = readFileList(),
imports = readStringList(),
javaHome = readNullable(DataInput::readFile),
scripts = readFileList(),
sources = readFileList()
)
},
write = {
with(it) {
writeFileList(classpath)
writeStringList(imports)
writeNullable(javaHome, DataOutput::writeFile)
writeFileList(scripts)
writeFileList(sources)
}
}
)
var KtFile.scriptCompilationConfiguration: ScriptCompilationConfiguration?
get() = this.getScriptOriginalFile()?.virtualFile?.scriptCompilationConfiguration
set(value) {
this.getScriptOriginalFile()?.virtualFile?.scriptCompilationConfiguration = value
}
var VirtualFile.scriptCompilationConfiguration: ScriptCompilationConfiguration? by cachedFileAttribute(
name = "kotlin-script-compilation-configuration",
version = 1,
read = {
val size = readInt()
val bytes = ByteArray(size)
read(bytes, 0, size)
val bis = ByteArrayInputStream(bytes)
ObjectInputStream(bis).use { ois ->
ois.readObject() as ScriptCompilationConfiguration
}
},
write = {
val os = ByteArrayOutputStream()
ObjectOutputStream(os).use { oos ->
oos.writeObject(it)
}
val bytes = os.toByteArray()
writeInt(bytes.size)
write(bytes)
}
)
@@ -17,7 +17,6 @@
package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.JavaSdkType
import com.intellij.openapi.projectRoots.Sdk
@@ -25,18 +24,17 @@ import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.io.URLUtil
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.dependencies.SyncScriptDependenciesLoader
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.makeFailureResult
// NOTE: this service exists exclusively because ScriptDependencyManager
// cannot be registered as implementing two services (state would be duplicated)
@@ -45,40 +43,73 @@ class IdeScriptDependenciesProvider(
project: Project
) : ScriptDependenciesProvider(project) {
override fun getScriptConfigurationResult(file: KtFile): ScriptCompilationConfigurationResult? {
return scriptDependenciesManager.getRefinedCompilationConfiguration(file)
val configuration = getScriptConfiguration(file)
val reports = IdeScriptReportSink.getReports(file)
if (configuration == null && reports.isNotEmpty()) {
return makeFailureResult(reports)
}
return configuration?.asSuccess(reports)
}
override fun getScriptConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper? {
return scriptDependenciesManager.getConfiguration(file)
}
}
// TODO: rename and provide alias for compatibility - this is not only about dependencies anymore
class ScriptDependenciesManager internal constructor(
private val cacheUpdater: ScriptsCompilationConfigurationUpdater,
private val cache: ScriptsCompilationConfigurationCache,
private val project: Project
) {
/**
* Facade for loading and caching Kotlin script files configuration.
*
* Loaded configuration will be cached in [memoryCache] and [fileAttributesCache].
* This service also starts indexing of new dependency roots and runs highlighting
* of opened files.
*/
interface ScriptConfigurationManager {
/**
* Save configurations into cache.
* Start indexing for new class/source roots.
* Re-highlight opened scripts with changed configuration.
*/
fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, ScriptCompilationConfigurationResult>>)
/**
* Start configuration update for files if configuration isn't up to date.
* Start indexing for new class/source roots.
*
* @return true if update was started for any file, false if all configurations are cached
*/
fun updateConfigurationsIfNotCached(files: List<KtFile>): Boolean
/**
* Check if configuration is already cached for [file] (in cache or FileAttributes).
* Don't check if file was changed after the last update.
* Supposed to be used to switch highlighting off for scripts without configuration.
* to avoid all file being highlighted in red.
*/
fun isConfigurationCached(file: KtFile): Boolean
/**
* Clear configuration caches
* Start re-highlighting for opened scripts
*/
fun clearConfigurationCachesAndRehighlight()
@Deprecated("Use getScriptClasspath(KtFile) instead")
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> {
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return emptyList()
return getScriptClasspath(ktFile)
}
fun getScriptClasspath(file: VirtualFile): List<VirtualFile>
fun getScriptClasspath(file: KtFile): List<VirtualFile> =
toVfsRoots(cacheUpdater.getCurrentCompilationConfiguration(file)?.valueOrNull()?.dependenciesClassPath.orEmpty())
fun getRefinedCompilationConfiguration(file: KtFile): ScriptCompilationConfigurationResult? =
cacheUpdater.getCurrentCompilationConfiguration(file)
fun getScriptDependenciesClassFilesScope(file: VirtualFile) = cache.scriptDependenciesClassFilesScope(file)
fun getScriptSdk(file: VirtualFile) = cache.getScriptSdk(file)
fun getAllScriptsSdks() = cache.allSdks
fun getAllScriptsDependenciesClassFilesScope() = cache.allDependenciesClassFilesScope
fun getAllScriptDependenciesSourcesScope() = cache.allDependenciesSourcesScope
fun getAllScriptsDependenciesClassFiles() = cache.allDependenciesClassFiles
fun getAllScriptDependenciesSources() = cache.allDependenciesSources
fun getScriptClasspath(file: KtFile): List<VirtualFile>
fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper?
fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope
fun getScriptSdk(file: VirtualFile): Sdk?
fun getFirstScriptsSdk(): Sdk?
fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope
fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope
fun getAllScriptsDependenciesClassFiles(): List<VirtualFile>
fun getAllScriptDependenciesSources(): List<VirtualFile>
}
@Deprecated("Use ScriptConfigurationManager instead")
interface ScriptDependenciesManager : ScriptConfigurationManager {
companion object {
@JvmStatic
fun getInstance(project: Project): ScriptDependenciesManager =
@@ -93,7 +124,7 @@ class ScriptDependenciesManager internal constructor(
return anyJavaSdk
}
log.warn(
LOG.warn(
"Default Script SDK is null: " +
"projectSdk = ${ProjectRootManager.getInstance(project).projectSdk}, " +
"all sdks = ${getAllProjectSdks().joinToString("\n")}"
@@ -118,17 +149,9 @@ class ScriptDependenciesManager internal constructor(
return res
}
internal val log = Logger.getInstance(ScriptDependenciesManager::class.java)
@TestOnly
fun updateScriptDependenciesSynchronously(file: PsiFile, project: Project) {
val loader = SyncScriptDependenciesLoader(project)
val scriptDefinition = file.findScriptDefinition() ?: return
assert(file is KtFile) {
"PsiFile should be a KtFile, otherwise script dependencies cannot be loaded"
}
loader.loadDependencies(file as KtFile, scriptDefinition)
loader.notifyRootsChanged()
(getInstance(project) as ScriptConfigurationManagerImpl).updateScriptDependenciesSynchronously(file)
}
}
}
@@ -16,7 +16,7 @@ import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.HyperlinkLabel
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) {
withSelectedEditor(project) { manager ->
@@ -28,9 +28,9 @@ fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) {
}
fun VirtualFile.addScriptDependenciesNotificationPanel(
compilationConfigurationResult: ScriptCompilationConfigurationResult,
compilationConfigurationResult: ScriptCompilationConfigurationWrapper,
project: Project,
onClick: (ScriptCompilationConfigurationResult) -> Unit
onClick: (ScriptCompilationConfigurationWrapper) -> Unit
) {
withSelectedEditor(project) { manager ->
val existingPanel = notificationPanel
@@ -61,8 +61,8 @@ private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(File
private var FileEditor.notificationPanel: NewScriptDependenciesNotificationPanel? by UserDataProperty<FileEditor, NewScriptDependenciesNotificationPanel>(Key.create("script.dependencies.panel"))
private class NewScriptDependenciesNotificationPanel(
onClick: (ScriptCompilationConfigurationResult) -> Unit,
val compilationConfigurationResult: ScriptCompilationConfigurationResult,
onClick: (ScriptCompilationConfigurationWrapper) -> Unit,
val compilationConfigurationResult: ScriptCompilationConfigurationWrapper,
project: Project
) : EditorNotificationPanel() {
@@ -29,7 +29,7 @@ class ScriptTrafficLightRendererContributor : TrafficLightRendererContributor {
if (!ScriptDefinitionsManager.getInstance(file.project).isReady()) {
status.reasonWhySuspended = "Loading kotlin script definitions"
status.errorAnalyzingFinished = false
} else if (!ScriptsCompilationConfigurationUpdater.areDependenciesCached(file)) {
} else if (!ScriptDependenciesManager.getInstance(project).isConfigurationCached(file)) {
status.reasonWhySuspended = "Loading kotlin script dependencies"
status.errorAnalyzingFinished = false
}
@@ -1,195 +0,0 @@
/*
* 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.Application
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.util.Alarm
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.dependencies.AsyncScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.FromFileAttributeScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.OutsiderFileDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.dependencies.SyncScriptDependenciesLoader
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
class ScriptsCompilationConfigurationUpdater(
private val project: Project,
private val cache: ScriptsCompilationConfigurationCache
) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400
private val loaders = arrayListOf(
FromFileAttributeScriptDependenciesLoader(project),
OutsiderFileDependenciesLoader(project),
AsyncScriptDependenciesLoader(project),
SyncScriptDependenciesLoader(project)
)
init {
listenForChangesInScripts()
}
fun getCurrentCompilationConfiguration(file: KtFile): ScriptCompilationConfigurationResult? {
val virtualFile = file.originalFile.virtualFile
cache[virtualFile]?.let { return it }
// Try-catch block is a temporary workaround for ProcessCanceledException
// that may happen during VirtualFile to PsiFile transformation
try {
updateDependencies(file)
} finally {
makeRootsChangeIfNeeded()
}
return cache[virtualFile]
}
fun updateDependenciesIfNeeded(files: List<KtFile>): Boolean {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
var wasDependenciesUpdateStarted = false
for (file in files) {
if (!areDependenciesCached(file)) {
wasDependenciesUpdateStarted = true
updateDependencies(file)
}
}
if (wasDependenciesUpdateStarted) {
makeRootsChangeIfNeeded()
}
return wasDependenciesUpdateStarted
}
private fun updateDependencies(file: KtFile) {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return
if (!cache.shouldRunDependenciesUpdate(file)) return
val scriptDefinition = file.findScriptDefinition() ?: return
loaders.filter { it.isApplicable(file, scriptDefinition) }.forEach { it.loadDependencies(file, scriptDefinition) }
}
private fun makeRootsChangeIfNeeded() {
loaders.firstOrNull {
it.notifyRootsChanged()
}
}
private fun listenForChangesInScripts() {
project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
runScriptDependenciesUpdateIfNeeded(file)
}
override fun selectionChanged(event: FileEditorManagerEvent) {
event.newFile?.let { runScriptDependenciesUpdateIfNeeded(it) }
}
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
updateDependencies(ktFile)
makeRootsChangeIfNeeded()
}
})
EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
val document = event.document
val file = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
// only update dependencies for scripts that were touched recently
if (cache[file] == null) {
return
}
scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest(
{
FileDocumentManager.getInstance().saveDocument(document)
updateDependencies(ktFile)
makeRootsChangeIfNeeded()
},
scriptChangesListenerDelay,
true
)
}
}, project.messageBus.connect())
}
private fun getKtFileToStartConfigurationUpdate(file: VirtualFile): KtFile? {
if (project.isDisposed || !file.isValid || file.isNonScript()) {
return null
}
if (
ApplicationManager.getApplication().isUnitTestMode &&
ApplicationManager.getApplication().isScriptDependenciesUpdaterDisabled == true
) {
return null
}
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null
if (ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) {
return ktFile
}
return null
}
private fun areDependenciesCached(file: VirtualFile): Boolean {
return cache[file] != null || file.scriptDependencies != null || file.scriptCompilationConfiguration != null
}
companion object {
@JvmStatic
fun getInstance(project: Project): ScriptsCompilationConfigurationUpdater =
ServiceManager.getService(project, ScriptsCompilationConfigurationUpdater::class.java)
fun areDependenciesCached(file: KtFile): Boolean {
return getInstance(file.project).areDependenciesCached(file.virtualFile)
}
}
}
@set: TestOnly
var Application.isScriptDependenciesUpdaterDisabled by NotNullableUserDataProperty(
Key.create("SCRIPT_DEPENDENCIES_UPDATER_DISABLED"),
false
)
@@ -0,0 +1,88 @@
/*
* 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.core.script
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.event.DocumentListener
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.util.Alarm
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
class ScriptsListener(
private val project: Project,
private val scriptsManager: ScriptConfigurationManagerImpl
) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400
init {
listenForChangesInScripts()
}
private fun listenForChangesInScripts() {
project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
override fun fileOpened(source: FileEditorManager, file: VirtualFile) {
runScriptDependenciesUpdateIfNeeded(file)
}
override fun selectionChanged(event: FileEditorManagerEvent) {
event.newFile?.let { runScriptDependenciesUpdateIfNeeded(it) }
}
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
scriptsManager.updateConfigurationsIfNotCached(listOf(ktFile))
}
})
EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener {
override fun documentChanged(event: DocumentEvent) {
val document = event.document
val file = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest(
{ scriptsManager.updateConfigurationsIfNotCached(listOf(ktFile)) },
scriptChangesListenerDelay,
true
)
}
}, project.messageBus.connect())
}
private fun getKtFileToStartConfigurationUpdate(file: VirtualFile): KtFile? {
if (project.isDisposed || !file.isValid || file.isNonScript()) {
return null
}
if (
ApplicationManager.getApplication().isUnitTestMode &&
ApplicationManager.getApplication().isScriptDependenciesUpdaterDisabled == true
) {
return null
}
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null
if (ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) {
return ktFile
}
return null
}
}
@@ -1,159 +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.core.script.dependencies
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import kotlinx.coroutines.Runnable
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.refineScriptCompilationConfiguration
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
// TODO: rename and provide alias for compatibility - this is not only about dependencies anymore
class AsyncScriptDependenciesLoader internal constructor(project: Project) : ScriptDependenciesLoader(project) {
private val lock = ReentrantReadWriteLock()
private var notifyRootChange: Boolean = false
private var backgroundTasksQueue: LoaderBackgroundTask? = null
override fun isApplicable(
file: KtFile,
scriptDefinition: ScriptDefinition
): Boolean {
return isAsyncDependencyResolver(scriptDefinition)
}
override fun loadDependencies(
file: KtFile,
scriptDefinition: ScriptDefinition
) {
lock.write {
if (backgroundTasksQueue == null) {
backgroundTasksQueue = LoaderBackgroundTask()
backgroundTasksQueue!!.addTask(file)
backgroundTasksQueue!!.start()
} else {
backgroundTasksQueue!!.addTask(file)
}
}
}
override fun shouldShowNotification(): Boolean = !KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
override fun notifyRootsChanged(): Boolean {
lock.write {
if (notifyRootChange) return false
if (backgroundTasksQueue == null) {
return submitMakeRootsChange()
}
notifyRootChange = true
backgroundTasksQueue!!.addOnFinishTask {
lock.write {
notifyRootChange = false
}
submitMakeRootsChange()
}
}
return false
}
private fun runDependenciesUpdate(file: KtFile) {
val scriptDef = file.findScriptDefinition() ?: return
debug(file) { "start async dependencies loading" }
val result = refineScriptCompilationConfiguration(KtFileScriptSource(file), scriptDef, project)
debug(file) { "finish async dependencies loading" }
processRefinedConfiguration(result, file.originalFile.virtualFile)
}
private inner class LoaderBackgroundTask {
private val sequenceOfFiles: ConcurrentLinkedQueue<KtFile> = ConcurrentLinkedQueue()
private var forceStop : Boolean = false
private var startedSilently : Boolean = false
private var onFinish: (() -> Unit)? = null
fun start() {
if (shouldShowNotification()) {
startSilently()
} else {
startWithProgress()
}
}
private fun restartWithProgress() {
forceStop = true
startWithProgress()
forceStop = false
}
private fun startSilently() {
startedSilently = true
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
loadDependencies(EmptyProgressIndicator())
})
}
private fun startWithProgress() {
startedSilently = false
object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
loadDependencies(indicator)
}
}.queue()
}
fun addTask(file: KtFile) {
if (sequenceOfFiles.contains(file)) return
debug(file) { "added to update queue" }
sequenceOfFiles.add(file)
// If the queue is longer than 3, show progress and cancel button
if (sequenceOfFiles.size > 3 && startedSilently) {
restartWithProgress()
}
}
fun addOnFinishTask(task: () -> Unit) {
onFinish = task
}
private fun loadDependencies(indicator: ProgressIndicator?) {
while (true) {
if (forceStop) return
if (indicator?.isCanceled == true || sequenceOfFiles.isEmpty()) {
lock.write {
onFinish?.invoke()
backgroundTasksQueue = null
}
return
}
runDependenciesUpdate(sequenceOfFiles.poll())
}
}
}
}
@@ -0,0 +1,108 @@
/*
* 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.core.script.dependencies
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.idea.core.script.ScriptClassRootsManager
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.psi.KtFile
import java.util.concurrent.ConcurrentLinkedQueue
internal class BackgroundLoader(
private val project: Project,
private val rootsManager: ScriptClassRootsManager,
private val loadDependencies: (KtFile) -> Unit
) {
private var task: Task? = null
@Synchronized
fun scheduleAsync(file: KtFile) {
if (task == null) {
startBatch(file)
} else {
task!!.addTask(file)
}
}
@Synchronized
private fun startBatch(file: KtFile) {
rootsManager.startTransaction()
task = Task()
task!!.addTask(file)
task!!.start()
}
@Synchronized
private fun endBatch() {
task = null
rootsManager.commit()
}
private inner class Task {
private val sequenceOfFiles: ConcurrentLinkedQueue<KtFile> = ConcurrentLinkedQueue()
private var forceStop: Boolean = false
private var startedSilently: Boolean = false
fun start() {
if (KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled) {
startWithProgress()
} else {
startSilently()
}
}
private fun restartWithProgress() {
forceStop = true
startWithProgress()
forceStop = false
}
private fun startSilently() {
startedSilently = true
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
loadDependencies(EmptyProgressIndicator())
})
}
private fun startWithProgress() {
startedSilently = false
object : com.intellij.openapi.progress.Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
loadDependencies(indicator)
}
}.queue()
}
fun addTask(file: KtFile) {
if (file in sequenceOfFiles) return
debug(file) { "added to update queue" }
sequenceOfFiles.add(file)
// If the queue is longer than 3, show progress and cancel button
if (sequenceOfFiles.size > 3 && startedSilently) {
restartWithProgress()
}
}
private fun loadDependencies(indicator: ProgressIndicator?) {
while (true) {
if (forceStop) return
if (indicator?.isCanceled == true || sequenceOfFiles.isEmpty()) {
endBatch()
return
}
loadDependencies(sequenceOfFiles.poll())
}
}
}
}
@@ -1,66 +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.core.script.dependencies
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.scriptCompilationConfiguration
import org.jetbrains.kotlin.idea.core.script.scriptDependencies
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import kotlin.script.experimental.api.asSuccess
// TODO: rename and provide alias for compatibility - this is not only about dependencies anymore
class FromFileAttributeScriptDependenciesLoader(project: Project) : ScriptDependenciesLoader(project) {
override fun isApplicable(
file: KtFile,
scriptDefinition: ScriptDefinition
): Boolean {
return file.scriptDependencies != null || file.scriptCompilationConfiguration != null
}
override fun loadDependencies(
file: KtFile,
scriptDefinition: ScriptDefinition
) {
val virtualFile = file.originalFile.virtualFile
virtualFile.scriptCompilationConfiguration?.let {
ScriptCompilationConfigurationWrapper.FromCompilationConfiguration(KtFileScriptSource(file), it).apply {
debug(virtualFile) { "refined configuration from fileAttributes = $it" }
}
} ?: virtualFile.scriptDependencies?.let {
ScriptCompilationConfigurationWrapper.FromLegacy(KtFileScriptSource(file), it, scriptDefinition).apply {
debug(virtualFile) { "dependencies from fileAttributes = $it" }
}
}?.let {
if (areDependenciesValid(virtualFile, it)) {
saveToCache(virtualFile, it.asSuccess(), skipSaveToAttributes = true)
} else {
virtualFile.scriptCompilationConfiguration = null
virtualFile.scriptDependencies = null
}
}
}
override fun shouldShowNotification(): Boolean = false
private fun areDependenciesValid(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper.FromLegacy): Boolean {
return configuration.dependenciesClassPath.all {
if (it.exists()) {
true
} else {
debug(file) {
"classpath root saved to file attribute doesn't exist: ${it.path}"
}
false
}
}
}
}
@@ -0,0 +1,40 @@
/*
* 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.core.script.dependencies
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.KotlinScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.LegacyResolverWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.refineScriptCompilationConfiguration
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
class FromRefinedConfigurationLoader internal constructor() : ScriptDependenciesLoader {
override fun isAsync(file: KtFile, scriptDefinition: ScriptDefinition): Boolean {
return scriptDefinition.asLegacyOrNull<KotlinScriptDefinition>()?.dependencyResolver?.let {
it is AsyncDependenciesResolver || it is LegacyResolverWrapper
} ?: false
}
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult? {
debug(file) { "start dependencies loading" }
val result = refineScriptCompilationConfiguration(KtFileScriptSource(file), scriptDefinition, file.project)
debug(file) { "finish dependencies loading" }
return result
}
}
@@ -5,34 +5,31 @@
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManagerImpl
import org.jetbrains.kotlin.idea.highlighter.OutsidersPsiFileSupportUtils
import org.jetbrains.kotlin.idea.highlighter.OutsidersPsiFileSupportWrapper
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import kotlin.script.experimental.api.asSuccess
class OutsiderFileDependenciesLoader(project: Project) : ScriptDependenciesLoader(project) {
override fun isApplicable(
file: KtFile,
scriptDefinition: ScriptDefinition
): Boolean {
val virtualFile = file.virtualFile ?: return false
return OutsidersPsiFileSupportWrapper.isOutsiderFile(virtualFile)
}
class OutsiderFileDependenciesLoader(private val manager: ScriptConfigurationManagerImpl) : ScriptDependenciesLoader {
override val skipSaveToAttributes: Boolean
get() = true
override val skipNotification: Boolean
get() = true
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
) {
val virtualFile = file.virtualFile ?: return
val fileOrigin = OutsidersPsiFileSupportUtils.getOutsiderFileOrigin(project, virtualFile) ?: return
val psiFileOrigin = PsiManager.getInstance(project).findFile(fileOrigin) as? KtFile ?: return
val compilationConfiguration =
ScriptDependenciesManager.getInstance(project).getRefinedCompilationConfiguration(psiFileOrigin) ?: return
saveToCache(virtualFile, compilationConfiguration)
}
): ScriptCompilationConfigurationResult? {
val virtualFile = file.virtualFile ?: return null
val project = file.project
override fun shouldShowNotification(): Boolean = false
val fileOrigin = OutsidersPsiFileSupportUtils.getOutsiderFileOrigin(project, virtualFile) ?: return null
val psiFileOrigin = PsiManager.getInstance(project).findFile(fileOrigin) as? KtFile ?: return null
return manager.getConfiguration(psiFileOrigin)?.asSuccess()
}
}
@@ -0,0 +1,134 @@
/*
* 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.core.script.dependencies
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.core.util.*
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper.FromCompilationConfiguration
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper.FromLegacy
import java.io.*
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.dependencies.ScriptDependencies
class ScriptConfigurationFileAttributeCache : ScriptDependenciesLoader {
operator fun contains(file: VirtualFile): Boolean =
file.scriptDependencies != null || file.scriptCompilationConfiguration != null
override val skipSaveToAttributes: Boolean
get() = true
override val skipNotification: Boolean
get() = true
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult? {
if (!firstLoad) return null
val virtualFile = file.originalFile.virtualFile
val configurationFromAttributes =
virtualFile.scriptCompilationConfiguration?.let {
FromCompilationConfiguration(KtFileScriptSource(file), it)
} ?: virtualFile.scriptDependencies?.let {
FromLegacy(KtFileScriptSource(file), it, scriptDefinition)
} ?: return null
debug(virtualFile) { "configuration from fileAttributes = $configurationFromAttributes" }
if (!areDependenciesValid(virtualFile, configurationFromAttributes)) {
save(virtualFile, null)
return null
}
return configurationFromAttributes.asSuccess()
}
private fun areDependenciesValid(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper): Boolean {
return configuration.dependenciesClassPath.all {
if (it.exists()) {
true
} else {
debug(file) {
"classpath root saved to file attribute doesn't exist: ${it.path}"
}
false
}
}
}
fun save(file: VirtualFile, value: ScriptCompilationConfigurationWrapper?) {
if (value == null) {
file.scriptDependencies = null
file.scriptCompilationConfiguration = null
} else {
if (value is ScriptCompilationConfigurationWrapper.FromLegacy) {
file.scriptDependencies = value.legacyDependencies
} else {
if (file.scriptDependencies != null) file.scriptDependencies = null
file.scriptCompilationConfiguration = value.configuration
}
}
}
}
private var VirtualFile.scriptDependencies: ScriptDependencies? by cachedFileAttribute(
name = "kotlin-script-dependencies",
version = 3,
read = {
ScriptDependencies(
classpath = readFileList(),
imports = readStringList(),
javaHome = readNullable(DataInput::readFile),
scripts = readFileList(),
sources = readFileList()
)
},
write = {
with(it) {
writeFileList(classpath)
writeStringList(imports)
writeNullable(javaHome, DataOutput::writeFile)
writeFileList(scripts)
writeFileList(sources)
}
}
)
private var VirtualFile.scriptCompilationConfiguration: ScriptCompilationConfiguration? by cachedFileAttribute(
name = "kotlin-script-compilation-configuration",
version = 1,
read = {
val size = readInt()
val bytes = ByteArray(size)
read(bytes, 0, size)
val bis = ByteArrayInputStream(bytes)
ObjectInputStream(bis).use { ois ->
ois.readObject() as ScriptCompilationConfiguration
}
},
write = {
val os = ByteArrayOutputStream()
ObjectOutputStream(os).use { oos ->
oos.writeObject(it)
}
val bytes = os.toByteArray()
writeInt(bytes.size)
write(bytes)
}
)
@@ -5,160 +5,23 @@
package org.jetbrains.kotlin.idea.core.script.dependencies
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.KotlinScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.LegacyResolverWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
// TODO: rename and provide alias for compatibility - this is not only about dependencies anymore
abstract class ScriptDependenciesLoader(protected val project: Project) {
// TODO: rename - this is not only about dependencies anymore
internal interface ScriptDependenciesLoader {
fun isAsync(file: KtFile, scriptDefinition: ScriptDefinition) = false
abstract fun isApplicable(file: KtFile, scriptDefinition: ScriptDefinition): Boolean
abstract fun loadDependencies(file: KtFile, scriptDefinition: ScriptDefinition)
val skipSaveToAttributes: Boolean
get() = false
protected abstract fun shouldShowNotification(): Boolean
val skipNotification: Boolean
get() = false
protected var shouldNotifyRootsChanged = false
protected val cache: ScriptsCompilationConfigurationCache = ServiceManager.getService(project, ScriptsCompilationConfigurationCache::class.java)
private val reporter: ScriptReportSink = ServiceManager.getService(project, ScriptReportSink::class.java)
protected fun processRefinedConfiguration(result: ScriptCompilationConfigurationResult, file: VirtualFile) {
debug(file) { "refined script compilation configuration from ${this.javaClass} received = $result" }
val oldResult = cache[file]
if (oldResult == null) {
save(result, file)
attachReportsIfChanged(result, file)
return
}
if (oldResult != result) {
if (shouldShowNotification()
&& oldResult.valueOrNull() != result.valueOrNull() // Only compilation configuration changed
&& !ApplicationManager.getApplication().isUnitTestMode
) {
debug(file) {
"dependencies changed, notification is shown: old = $oldResult, new = $result"
}
file.addScriptDependenciesNotificationPanel(result, project) {
save(it, file)
attachReportsIfChanged(result, file)
submitMakeRootsChange()
}
} else {
debug(file) {
"dependencies changed, new dependencies are applied automatically: old = $oldResult, new = $result"
}
save(result, file)
attachReportsIfChanged(result, file)
}
} else {
attachReportsIfChanged(result, file)
if (shouldShowNotification()) {
file.removeScriptDependenciesNotificationPanel(project)
}
}
}
private fun attachReportsIfChanged(result: ResultWithDiagnostics<*>, file: VirtualFile) {
reporter.attachReports(file, result.reports)
}
private fun save(compilationConfigurationResult: ScriptCompilationConfigurationResult?, file: VirtualFile) {
if (shouldShowNotification()) {
file.removeScriptDependenciesNotificationPanel(project)
}
if (compilationConfigurationResult != null) {
saveToCache(file, compilationConfigurationResult)
}
}
protected fun saveToCache(
file: VirtualFile, compilationConfigurationResult: ScriptCompilationConfigurationResult, skipSaveToAttributes: Boolean = false
) {
val rootsChanged = compilationConfigurationResult.valueOrNull()?.let { cache.hasNotCachedRoots(it) } ?: false
if (cache.save(file, compilationConfigurationResult)
&& !skipSaveToAttributes
&& compilationConfigurationResult is ResultWithDiagnostics.Success
) {
debug(file) {
"refined configuration is saved to file attributes: $compilationConfigurationResult"
}
if (compilationConfigurationResult.value is ScriptCompilationConfigurationWrapper.FromLegacy)
file.scriptDependencies = compilationConfigurationResult.value.legacyDependencies
else
file.scriptCompilationConfiguration = compilationConfigurationResult.value.configuration
}
if (rootsChanged) {
shouldNotifyRootsChanged = true
}
}
open fun notifyRootsChanged(): Boolean = submitMakeRootsChange()
protected fun submitMakeRootsChange(): Boolean {
if (!shouldNotifyRootsChanged) return false
val doNotifyRootsChanged = Runnable {
runWriteAction {
if (project.isDisposed) return@runWriteAction
debug(null) { "root change event for ${this.javaClass}" }
shouldNotifyRootsChanged = false
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
TransactionGuard.submitTransaction(project, doNotifyRootsChanged)
} else {
TransactionGuard.getInstance().submitTransactionLater(project, doNotifyRootsChanged)
}
return true
}
protected fun isAsyncDependencyResolver(scriptDef: ScriptDefinition): Boolean =
scriptDef.asLegacyOrNull<KotlinScriptDefinition>()?.dependencyResolver?.let {
it is AsyncDependenciesResolver || it is LegacyResolverWrapper
} ?: false
}
private val LOG = Logger.getInstance("#org.jetbrains.kotlin.idea.script")
internal fun debug(file: VirtualFile? = null, message: () -> String) {
if (LOG.isDebugEnabled) {
LOG.debug("[KOTLIN SCRIPT] ${file?.path} " + message())
}
}
internal fun debug(file: KtFile, message: () -> String) {
if (LOG.isDebugEnabled) {
LOG.debug("[KOTLIN SCRIPT] ${file.originalFile.virtualFile?.path} " + message())
}
}
fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult?
}
@@ -1,33 +0,0 @@
/*
* Copyright 2010-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.core.script.dependencies
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KtFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.refineScriptCompilationConfiguration
class SyncScriptDependenciesLoader(project: Project) : ScriptDependenciesLoader(project) {
override fun isApplicable(
file: KtFile,
scriptDefinition: ScriptDefinition
): Boolean {
return !isAsyncDependencyResolver(scriptDefinition)
}
override fun loadDependencies(
file: KtFile,
scriptDefinition: ScriptDefinition
) {
debug(file) { "start sync dependencies loading" }
val result = refineScriptCompilationConfiguration(KtFileScriptSource(file), scriptDefinition, project)
debug(file) { "finish sync dependencies loading" }
processRefinedConfiguration(result, file.originalFile.virtualFile)
}
override fun shouldShowNotification(): Boolean = false
}
@@ -0,0 +1,31 @@
/*
* 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.core.script
import com.intellij.openapi.application.Application
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.util.Key
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
@set: org.jetbrains.annotations.TestOnly
var Application.isScriptDependenciesUpdaterDisabled by NotNullableUserDataProperty(
Key.create("SCRIPT_DEPENDENCIES_UPDATER_DISABLED"),
false
)
internal val LOG = Logger.getInstance("#org.jetbrains.kotlin.idea.script")
internal fun debug(file: KtFile, message: () -> String) {
debug(file.originalFile.virtualFile, message)
}
internal fun debug(file: VirtualFile? = null, message: () -> String) {
if (LOG.isDebugEnabled) {
LOG.debug("[KOTLIN SCRIPT] ${file?.let { file.path + " "} ?: ""}" + message())
}
}
@@ -30,7 +30,6 @@ import org.jetbrains.kotlin.idea.util.JavaParametersBuilder
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import java.io.File
import kotlin.script.experimental.api.valueOrNull
class KtScratchExecutionSession(
private val file: ScratchFile,
@@ -166,7 +165,7 @@ class KtScratchExecutionSession(
}
ScriptDependenciesManager.getInstance(originalFile.project)
.getRefinedCompilationConfiguration(originalFile)?.valueOrNull()?.let {
.getConfiguration(originalFile)?.let {
javaParameters.classPath.addAll(it.dependenciesClassPath.map { f -> f.absolutePath })
}
+1 -7
View File
@@ -297,13 +297,7 @@
serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptDependenciesProvider"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationCache"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationCache"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationUpdater"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationUpdater"/>
serviceImplementation="org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManagerImpl"/>
<projectService serviceInterface="org.jetbrains.kotlin.scripting.resolve.ScriptReportSink"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink"/>
@@ -43,7 +43,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.findModuleDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.core.isInheritable
import org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesManager
import org.jetbrains.kotlin.idea.core.toDescriptor
import org.jetbrains.kotlin.idea.findUsages.KotlinFindUsagesHandlerFactory
import org.jetbrains.kotlin.idea.findUsages.handlers.KotlinFindClassUsagesHandler
@@ -136,7 +136,7 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
val usedScripts = findScriptsWithUsages(declaration)
if (usedScripts.isNotEmpty()) {
if (ScriptsCompilationConfigurationUpdater.getInstance(declaration.project).updateDependenciesIfNeeded(usedScripts)) {
if (ScriptDependenciesManager.getInstance(declaration.project).updateConfigurationsIfNotCached(usedScripts)) {
return TOO_MANY_OCCURRENCES
}
}
@@ -49,7 +49,6 @@ import org.jetbrains.kotlin.resolve.scopes.utils.findVariable
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.*
import kotlin.script.experimental.api.valueOrNull
class ImportInsertHelperImpl(private val project: Project) : ImportInsertHelper() {
@@ -66,8 +65,7 @@ class ImportInsertHelperImpl(private val project: Project) : ImportInsertHelper(
val scriptExtraImports = contextFile.takeIf { it.isScript() }?.let { ktFile ->
val scriptDependencies = ScriptDependenciesProvider.getInstance(ktFile.project)
?.getScriptConfigurationResult(ktFile.originalFile as KtFile)
?.valueOrNull()
?.getScriptConfiguration(ktFile.originalFile as KtFile)
scriptDependencies?.defaultImports?.map { ImportPath.fromString(it) }
}.orEmpty()
@@ -22,7 +22,7 @@ import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.core.script.ScriptsCompilationConfigurationUpdaterKt;
import org.jetbrains.kotlin.idea.core.script.ScriptUtilsKt;
import org.jetbrains.kotlin.test.InTextDirectivesUtils;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.plugins.groovy.GroovyFileType;
@@ -42,12 +42,12 @@ public abstract class AbstractConfigureProjectByChangingFileTest<C extends Kotli
ApplicationManager.getApplication().runWriteAction(
() -> FileTypeManager.getInstance().associateExtension(GroovyFileType.GROOVY_FILE_TYPE, "gradle")
);
ScriptsCompilationConfigurationUpdaterKt.setScriptDependenciesUpdaterDisabled(ApplicationManager.getApplication(), true);
ScriptUtilsKt.setScriptDependenciesUpdaterDisabled(ApplicationManager.getApplication(), true);
}
@Override
protected void tearDown() throws Exception {
ScriptsCompilationConfigurationUpdaterKt.setScriptDependenciesUpdaterDisabled(ApplicationManager.getApplication(), false);
ScriptUtilsKt.setScriptDependenciesUpdaterDisabled(ApplicationManager.getApplication(), false);
moduleInfoFile = null;
super.tearDown();
}
@@ -12,6 +12,7 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.dependencies.ScriptDependencies
@@ -22,17 +23,19 @@ open class ScriptDependenciesProvider constructor(
@Deprecated("Migrating to configuration refinement", level = DeprecationLevel.ERROR)
fun getScriptDependencies(file: VirtualFile): ScriptDependencies? {
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null
return getScriptConfigurationResult(ktFile)?.valueOrNull()?.legacyDependencies
return getScriptConfiguration(ktFile)?.legacyDependencies
}
@Deprecated("Migrating to configuration refinement", level = DeprecationLevel.ERROR)
fun getScriptDependencies(file: PsiFile): ScriptDependencies? {
if (file !is KtFile) return null
return getScriptConfigurationResult(file)?.valueOrNull()?.legacyDependencies
return getScriptConfiguration(file)?.legacyDependencies
}
open fun getScriptConfigurationResult(file: KtFile): ScriptCompilationConfigurationResult? = null
open fun getScriptConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper? = getScriptConfigurationResult(file)?.valueOrNull()
companion object {
fun getInstance(project: Project): ScriptDependenciesProvider? =
ServiceManager.getService(project, ScriptDependenciesProvider::class.java)
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResu
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.valueOr
import kotlin.script.experimental.api.valueOrNull
fun PsiFile.findScriptCompilationConfiguration(): ScriptCompilationConfiguration? {
// Do not use psiFile.script, see comments in findScriptDefinition
@@ -21,7 +20,7 @@ fun PsiFile.findScriptCompilationConfiguration(): ScriptCompilationConfiguration
val provider = ScriptDependenciesProvider.getInstance(project)
// Ignoring the error here, assuming that it will be reported elsewhere anyway (this is important scenario in IDE)
return provider?.getScriptConfigurationResult(this)?.valueOrNull()?.configuration
return provider?.getScriptConfiguration(this)?.configuration
?: findScriptDefinition()?.compilationConfiguration
}
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.psi.KtImportInfo
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.resolve.extensions.ExtraImportsProviderExtension
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import kotlin.script.experimental.api.valueOrNull
class ScriptExtraImportsProviderExtension : ExtraImportsProviderExtension {
@@ -30,8 +29,7 @@ class ScriptExtraImportsProviderExtension : ExtraImportsProviderExtension {
override fun getExtraImports(ktFile: KtFile): Collection<KtImportInfo> =
ktFile.takeIf { it.isScript() }?.let { file ->
val refinedConfiguration = ScriptDependenciesProvider.getInstance(file.project)
?.getScriptConfigurationResult(file.originalFile as KtFile)
?.valueOrNull()
?.getScriptConfiguration(file.originalFile as KtFile)
refinedConfiguration?.defaultImports?.map {
ScriptExtraImportImpl(
ImportPath.fromString(it)
@@ -205,7 +205,7 @@ class LazyScriptDescriptor(
val res = ArrayList<ClassDescriptor>()
val importedScriptsFiles = ScriptDependenciesProvider.getInstance(scriptInfo.script.project)
?.getScriptConfigurationResult(scriptInfo.script.containingKtFile)?.valueOrNull()?.importedScripts
?.getScriptConfiguration(scriptInfo.script.containingKtFile)?.importedScripts
if (importedScriptsFiles != null) {
val findImportedScriptDescriptor = ImportedScriptDescriptorsFinder()
importedScriptsFiles.mapNotNullTo(res) {
@@ -13,7 +13,6 @@ import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import java.io.File
import kotlin.script.experimental.api.valueOrNull
data class ScriptsCompilationDependencies(
val classpath: List<File>,
@@ -42,7 +41,7 @@ fun collectScriptsCompilationDependencies(
while (true) {
val newRemainingSources = ArrayList<KtFile>()
for (source in remainingSources) {
val refinedConfiguration = importsProvider.getScriptConfigurationResult(source)?.valueOrNull()
val refinedConfiguration = importsProvider.getScriptConfiguration(source)
if (refinedConfiguration != null) {
collectedClassPath.addAll(refinedConfiguration.dependenciesClassPath)
@@ -154,7 +154,7 @@ class KJvmReplCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) :
sourceFiles.first(),
sourceDependencies
) { ktFile ->
dependenciesProvider?.getScriptConfigurationResult(ktFile)?.valueOrNull()?.configuration
dependenciesProvider?.getScriptConfiguration(ktFile)?.configuration
?: context.baseScriptCompilationConfiguration
}
@@ -98,7 +98,7 @@ private fun compileImpl(
val dependenciesProvider = ScriptDependenciesProvider.getInstance(context.environment.project)
val getScriptConfiguration = { ktFile: KtFile ->
(dependenciesProvider?.getScriptConfigurationResult(ktFile)?.valueOrNull()?.configuration ?: context.baseScriptCompilationConfiguration)
(dependenciesProvider?.getScriptConfiguration(ktFile)?.configuration ?: context.baseScriptCompilationConfiguration)
.with {
// Adjust definitions so all compiler dependencies are saved in the resulting compilation configuration, so evaluation
// performed with the expected classpath
@@ -33,7 +33,6 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.compilerOptions
import kotlin.script.experimental.api.dependencies
import kotlin.script.experimental.api.valueOrNull
import kotlin.script.experimental.host.ScriptingHostConfiguration
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.jdkHome
@@ -244,7 +243,7 @@ private fun CompilerConfiguration.updateWithRefinedConfigurations(
) {
val dependenciesProvider = ScriptDependenciesProvider.getInstance(context.environment.project)
val updatedCompilerOptions = sourceFiles.flatMap {
dependenciesProvider?.getScriptConfigurationResult(it)?.valueOrNull()?.configuration?.get(
dependenciesProvider?.getScriptConfiguration(it)?.configuration?.get(
ScriptCompilationConfiguration.compilerOptions
) ?: emptyList()
}
@@ -23,7 +23,6 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.script.experimental.api.valueOrNull
// WARNING: not thread safe, assuming external synchronization
@@ -71,7 +70,7 @@ open class GenericReplCompiler(
}
val newDependencies =
ScriptDependenciesProvider.getInstance(checker.environment.project)?.getScriptConfigurationResult(psiFile)?.valueOrNull()
ScriptDependenciesProvider.getInstance(checker.environment.project)?.getScriptConfiguration(psiFile)
?.legacyDependencies
var classpathAddendum: List<File>? = null
if (compilerState.lastDependencies != newDependencies) {