ide, scripting: rework script configuration caching, introduce persistent fs cache

## Simplify ScriptConfigurationManager mental model
Everything related to cache managing moved to `org.jetbrains.kotlin.idea.core.script.configuration`.
DefaultScriptConfigurationManager slitted into:
- `AbstractScriptConfigurationManager`
- `DefaultScriptConfigurationManager`
- `ScriptClassRootsCache`
The main idea is to simplify the mental model of ScriptConfigurationManager.
Concrete implementation should provide just two things:
- `createCache(): ScriptConfigurationCache` that should return cache implementation with `get` and `set` methods
- `reloadOutOfDateConfiguration(...)` will be called on [cache] miss or when [file] is changed.

Implementation should initiate loading of [file]'s script configuration and call [saveChangedConfiguration]
immediately or in some future (e.g. after user will click "apply context" or/and configuration will be
calculated by some background thread).
Everything around it is implemented in `AbstractScriptConfigurationManager`.

`ScriptClassRootsCache` is extracted from `DefaultScriptConfigurationManager`:
this simplifies `AbstractScriptConfigurationManager`
Also it simplifies ScriptClassRootsCache reset: we can just drop old `ScriptClassRootsCache`
and create new one instead of resetting all internals of `ScriptClassRootsCache`.
Please see KDoc of these classes for more details:
- AbstractScriptConfigurationManager
- DefaultScriptConfigurationManager
- ScriptClassRootsCache

## Cache and up-to-date checks
Writer should put related inputs snapshot for loaded configuration.
This allows reader make up-to-date check for existed entry to avoid starting loading
process for same inputs.
Cache entry may be marked out-of-date.
Also it can be marked up-to-date, but it means that we have up-to-date loaded configuration,
ScriptConfigurationSnapshot.configuration will still contain old applied configuration.
todo: make it more clear by splitting cache entry into loaded and applied configuration with
it's own inputs.

## Loaders
Previously both sync and background loaders may be started for same files - this was useful, but not
required anymore and at the same time greatly complicates the model. So, it was dropped
and now exactly one loader should work for given file (first applicable loader will be called).
Also `ScriptConfigurationLoadingContext` is introduced to limit access to the `ScriptConfigurationManager`
internals.

## Listeners and updating configuration
This things is introduces:
- `ScriptChangesNotifier`
- `ScriptChangeListener`
- `ScriptConfigurationUpdater`
`ScriptChangesNotifier` will call first applicable `ScriptChangeListener` when editor is activated or document changed.
(it treated as applicable if [editorActivated] or [documentChanged] will return true).
Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
Plugins may override default listening logic by adding it's oven `ScriptChangeListener`.
`ScriptConfigurationUpdater` was extracted from `ScriptConfigurationManagerImpl`.

## Fixed issues and implement persistent FS cache
- Script configuration was not reloaded if query for same file already in progress or ready but not yet applied
- Don't start async loaders for outsider files
- Unused imports: don't start loading script configuration if the result will be not applied automatically
- Fix cases when virtual file can be null

See `DefaultScriptConfigurationManager.reloadOutOfDateConfiguration` KDoc for more details on
dealing with concurrent operations.

## Background executor and progress indicator
This things changed and fixed:
- progress displayed not only when we have more then 3 loading tasks, but also
after 1sec of loading (including case with 1 loading task for example)
- current file is displayed under the progress bar
- progress indication added: it works similar to indexing indicator. When
progress bar is displayed, max is fixed. When all work done for this max,
progress will be started from zero and new max is fixed, and so on
- cancel will drop all loading operation, not just current
- concurrency issues fixed: EA-5244265 and other related to switching between silent and
under progress worker
See `BackgroundExecutor` for more details.

## Gradle specific behavior
We have plans to extract separate implementation for Gradle scripts, but it
would be better to support something at this point.
For now, we have custom listener, loader and up-to-date check for default configuration manager:
- listener will do forced reload on editor activation, even it is already up-to-date
this is required for Gradle scripts, since it's classpath may depend on other files (`.properties` for example)
- loader will force save fails before running loader. also it will skip loading if
`kotlin.gradle.scripts.useIdeaProjectImport` registry key is enabled
- loader will return inputs with overridden up-to-date checks: file will be considered
out-of-date only when `buildscript` or `plugins` blocks are changed
This commit is contained in:
Sergey Rostov
2019-10-02 22:44:10 +03:00
committed by Natalia Selezneva
parent 915c9c367c
commit 9d2cc484aa
24 changed files with 1272 additions and 774 deletions
@@ -72,7 +72,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 (!ScriptConfigurationManager.getInstance(ktFile.project).isConfigurationCached(ktFile)) return false
if (!ScriptConfigurationManager.getInstance(ktFile.project).hasConfiguration(ktFile)) return false
if (IdeScriptReportSink.getReports(ktFile).any { it.severity == ScriptDiagnostic.Severity.FATAL }) {
return false
}
@@ -28,7 +28,10 @@ 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.configuration.DefaultScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.AbstractScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.LoadedScriptConfiguration
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
@@ -58,48 +61,58 @@ class IdeScriptDependenciesProvider(project: Project) : ScriptDependenciesProvid
/**
* 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.
* of opened files when configuration will be loaded or updated.
*/
interface ScriptConfigurationManager {
/**
* Get cached configuration for [file] or load it.
* May return null even configuration was loaded but was not yet applied.
*/
fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper?
@Deprecated("Use getScriptClasspath(KtFile) instead")
fun getScriptClasspath(file: VirtualFile): List<VirtualFile>
/**
* @see [getConfiguration]
*/
fun getScriptClasspath(file: KtFile): List<VirtualFile>
/**
* Check if configuration is already cached for [file] (in cache or FileAttributes).
* The result may be true, even cached configuration is considered out-of-date.
*
* Supposed to be used to switch highlighting off for scripts without configuration
* to avoid all file being highlighted in red.
*/
fun hasConfiguration(file: KtFile): Boolean
/**
* See [ScriptConfigurationUpdater].
*/
val updater: ScriptConfigurationUpdater
/**
* Clear all caches and re-highlighting opened scripts
*/
fun clearConfigurationCachesAndRehighlight()
/**
* 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>>)
fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, LoadedScriptConfiguration>>)
/**
* 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
///////////////
// classpath roots info:
/**
* 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>
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 getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope
fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope
fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope
fun getAllScriptsDependenciesClassFiles(): List<VirtualFile>
@@ -146,7 +159,7 @@ interface ScriptConfigurationManager {
@TestOnly
fun updateScriptDependenciesSynchronously(file: PsiFile, project: Project) {
(getInstance(project) as DefaultScriptConfigurationManager).updateScriptDependenciesSynchronously(file)
(getInstance(project) as AbstractScriptConfigurationManager).updateScriptDependenciesSynchronously(file)
}
}
}
@@ -17,6 +17,10 @@ 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.ScriptCompilationConfigurationWrapper
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import javax.swing.JPanel
fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) {
withSelectedEditor(project) { manager ->
@@ -30,7 +34,8 @@ fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) {
fun VirtualFile.addScriptDependenciesNotificationPanel(
compilationConfigurationResult: ScriptCompilationConfigurationWrapper,
project: Project,
onClick: (ScriptCompilationConfigurationWrapper) -> Unit
onClick: () -> Unit,
onHide: () -> Unit
) {
withSelectedEditor(project) { manager ->
val existingPanel = notificationPanel
@@ -45,6 +50,11 @@ fun VirtualFile.addScriptDependenciesNotificationPanel(
val panel = NewScriptDependenciesNotificationPanel(onClick, compilationConfigurationResult, project)
notificationPanel = panel
panel.addComponentListener(object: ComponentAdapter() {
override fun componentHidden(e: ComponentEvent) {
onHide()
}
})
manager.addTopComponent(this, panel)
}
}
@@ -63,7 +73,7 @@ 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: (ScriptCompilationConfigurationWrapper) -> Unit,
onClick: () -> Unit,
val compilationConfigurationResult: ScriptCompilationConfigurationWrapper,
project: Project
) : EditorNotificationPanel() {
@@ -71,11 +81,11 @@ private class NewScriptDependenciesNotificationPanel(
init {
setText("There is a new script context available.")
createComponentActionLabel("Apply context") {
onClick(compilationConfigurationResult)
onClick()
}
createComponentActionLabel("Enable auto-reload") {
onClick(compilationConfigurationResult)
onClick()
KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled = true
}
}
@@ -29,7 +29,7 @@ class ScriptTrafficLightRendererContributor : TrafficLightRendererContributor {
if (!ScriptDefinitionsManager.getInstance(file.project).isReady()) {
status.reasonWhySuspended = "Loading kotlin script definitions"
status.errorAnalyzingFinished = false
} else if (!ScriptConfigurationManager.getInstance(project).isConfigurationCached(file)) {
} else if (!ScriptConfigurationManager.getInstance(project).hasConfiguration(file)) {
status.reasonWhySuspended = "Loading kotlin script dependencies"
status.errorAnalyzingFinished = false
}
@@ -0,0 +1,283 @@
/*
* 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.configuration
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.extensions.Extensions
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.PsiElementFinder
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.toVfsRoots
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCache
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsIndexer
import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Abstract [ScriptConfigurationManager] implementation based on [cache] and [reloadOutOfDateConfiguration].
* Among this two methods concrete implementation should provide script changes listening (by calling [updater] on some event).
*
* Basically all requests routed to [cache]. If there is no entry in [cache] or it is considered out-of-date,
* then [reloadOutOfDateConfiguration] will be called, which, in turn, should call [saveChangedConfiguration]
* immediately or in some future (e.g. after user will click "apply context" or/and configuration will
* be calculated by some background thread).
*
* [classpathRoots] will be calculated lazily based on [cache]d configurations.
* Every change in [cache] will invalidate [classpathRoots] cache.
* Some internal state changes in [cache] may also invalidate [classpathRoots] by calling [clearClassRootsCaches]
* (for example, when cache loaded from FS to memory)
*/
internal abstract class AbstractScriptConfigurationManager(
protected val project: Project
) : ScriptConfigurationManager {
protected val rootsIndexer = ScriptClassRootsIndexer(project)
@Suppress("LeakingThis")
protected val cache: ScriptConfigurationCache = createCache()
protected abstract fun createCache(): ScriptConfigurationCache
/**
* Will be called on [cache] miss or when [file] is changed.
* Implementation should initiate loading of [file]'s script configuration and call [saveChangedConfiguration]
* immediately or in some future
* (e.g. after user will click "apply context" or/and configuration will be calculated by some background thread).
*
* @param isFirstLoad may be set explicitly for optimization reasons (to avoid expensive fs cache access)
* @param loadEvenWillNotBeApplied may should be set to false only on requests from particular editor, when
* user can see potential notification and accept new configuration. In other cases this should be `false` since
* loaded configuration will be just leaved in hidden user notification and cannot be used in any way.
* @param forceSync should be used in tests only
*/
protected abstract fun reloadOutOfDateConfiguration(
file: KtFile,
isFirstLoad: Boolean = getCachedConfiguration(file.originalFile.virtualFile) == null,
loadEvenWillNotBeApplied: Boolean = false,
forceSync: Boolean = false
)
@Deprecated("Use getScriptClasspath(KtFile) instead")
override fun getScriptClasspath(file: VirtualFile): List<VirtualFile> {
val ktFile = project.getKtFile(file) ?: return emptyList()
return getScriptClasspath(ktFile)
}
override fun getScriptClasspath(file: KtFile): List<VirtualFile> =
toVfsRoots(getConfiguration(file)?.dependenciesClassPath.orEmpty())
fun getCachedConfiguration(file: VirtualFile?): CachedConfigurationSnapshot? {
if (file == null) return null
return cache[file]
}
override fun hasConfiguration(file: KtFile): Boolean {
return getCachedConfiguration(file.originalFile.virtualFile) != null
}
override fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper? {
return getConfiguration(file.originalFile.virtualFile, file)
}
fun getConfiguration(
virtualFile: VirtualFile,
preloadedKtFile: KtFile? = null
): ScriptCompilationConfigurationWrapper? {
val cached = getCachedConfiguration(virtualFile)
if (cached != null) return cached.configuration
val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null
rootsIndexer.transaction {
reloadOutOfDateConfiguration(ktFile, isFirstLoad = true)
}
return getCachedConfiguration(virtualFile)?.configuration
}
override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater {
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) {
reloadIfOutOfDate(listOf(file), true)
}
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean {
return reloadIfOutOfDate(files, false)
}
override fun forceConfigurationReload(file: KtFile) {
val virtualFile = file.originalFile.virtualFile ?: return
cache.markOutOfDate(virtualFile)
rootsIndexer.transaction {
reloadOutOfDateConfiguration(file, loadEvenWillNotBeApplied = true)
}
}
}
private fun reloadIfOutOfDate(files: List<KtFile>, loadEvenWillNotBeApplied: Boolean): Boolean {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
var upToDate = true
rootsIndexer.transaction {
files.forEach { file ->
val virtualFile = file.originalFile.virtualFile
if (virtualFile != null) {
val state = cache[virtualFile]
if (state == null || !state.inputs.isUpToDate(project, virtualFile, file)) {
upToDate = false
reloadOutOfDateConfiguration(
file,
isFirstLoad = state == null,
loadEvenWillNotBeApplied = loadEvenWillNotBeApplied
)
}
}
}
}
return upToDate
}
@TestOnly
internal fun updateScriptDependenciesSynchronously(file: PsiFile) {
file.findScriptDefinition() ?: return
file as? KtFile ?: error("PsiFile $file should be a KtFile, otherwise script dependencies cannot be loaded")
val virtualFile = file.virtualFile
if (cache[virtualFile]?.inputs?.isUpToDate(project, virtualFile, file) == true) return
rootsIndexer.transaction {
reloadOutOfDateConfiguration(file, isFirstLoad = true, forceSync = true)
}
}
protected open fun saveChangedConfiguration(
file: VirtualFile,
newConfigurationSnapshot: CachedConfigurationSnapshot?
) {
rootsIndexer.checkInTransaction()
val newConfiguration = newConfigurationSnapshot?.configuration
debug(file) { "configuration changed = $newConfiguration" }
if (newConfiguration != null) {
if (hasNotCachedRoots(newConfiguration)) {
rootsIndexer.markNewRoot(file, newConfiguration)
}
cache[file] = newConfigurationSnapshot
clearClassRootsCaches()
}
updateHighlighting(listOf(file))
}
private fun hasNotCachedRoots(configuration: ScriptCompilationConfigurationWrapper): Boolean {
return classpathRoots.hasNotCachedRoots(configuration)
}
/**
* Clear configuration caches
* Start re-highlighting for opened scripts
*/
override fun clearConfigurationCachesAndRehighlight() {
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
// todo: invalidate caches?
if (project.isOpen) {
val openedScripts = FileEditorManager.getInstance(project).openFiles.filterNot { it.isNonScript() }
updateHighlighting(openedScripts)
}
}
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)
}
}
}
}
///////////////////
// ScriptRootsCache
private val classpathRootsLock = ReentrantLock()
@Volatile
private var _classpathRoots: ScriptClassRootsCache? = null
private val classpathRoots: ScriptClassRootsCache
get() {
val value1 = _classpathRoots
if (value1 != null) return value1
classpathRootsLock.withLock {
val value2 = _classpathRoots
if (value2 != null) return value2
val value3 = ScriptClassRootsCache(project, cache)
_classpathRoots = value3
return value3
}
}
private fun clearClassRootsCaches() {
debug { "class roots caches cleared" }
classpathRootsLock.withLock {
_classpathRoots = null
}
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project).getExtensionPoint(PsiElementFinder.EP_NAME).extensions
.filterIsInstance<KotlinScriptDependenciesClassFinder>()
.single()
kotlinScriptDependenciesClassFinder.clearCache()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
override fun getScriptSdk(file: VirtualFile): Sdk? = classpathRoots.getScriptSdk(file)
override fun getFirstScriptsSdk(): Sdk? = classpathRoots.getFirstScriptsSdk()
override fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope =
classpathRoots.getScriptDependenciesClassFilesScope(file)
override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope = classpathRoots.allDependenciesClassFilesScope
override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope = classpathRoots.allDependenciesSourcesScope
override fun getAllScriptsDependenciesClassFiles(): List<VirtualFile> = classpathRoots.allDependenciesClassFiles
override fun getAllScriptDependenciesSources(): List<VirtualFile> = classpathRoots.allDependenciesSources
}
@@ -8,287 +8,297 @@ package org.jetbrains.kotlin.idea.core.script.configuration
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.*
import org.jetbrains.kotlin.idea.core.script.LOG
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.toVfsRoots
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptsListener
import org.jetbrains.kotlin.idea.core.script.configuration.loader.FromRefinedConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.OutsiderFileDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptDependenciesLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.BackgroundLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsManager
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.core.script.configuration.listener.DefaultScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.*
import org.jetbrains.kotlin.idea.core.script.configuration.utils.BackgroundExecutor
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.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.script.experimental.api.ScriptDiagnostic
import kotlin.script.experimental.api.valueOrNull
class DefaultScriptConfigurationManager internal constructor(private val project: Project) :
ScriptConfigurationManager {
private val rootsManager = ScriptClassRootsManager(project)
/**
* Standard implementation of scripts configuration loading and caching
* (we have plans to extract separate implementation for Gradle scripts).
*
* ## Loading initiation
*
* [getConfiguration] will be called when we need to show or analyze some script file.
*
* As described in [AbstractScriptConfigurationManager], configuration may be loaded from [cache]
* or [reloadOutOfDateConfiguration] will be called on [cache] miss.
*
* There are 2 tiers [cache]: memory and FS. For now FS cache implemented by [ScriptConfigurationLoader]
* because we are not storing classpath roots yet. As a workaround cache.all() will return only memory
* cached configurations. So, for now we are indexing roots that loaded from FS with
* default [reloadOutOfDateConfiguration] mechanics. todo(KT-34444): implement fs classpath roots cache
*
* [notifier] will call first applicable [listeners] when editor is activated or document changed.
* Listener may call [updater] to invalidate configuration and schedule reloading.
*
* Also, [ScriptConfigurationUpdater.ensureConfigurationUpToDate] may be called from [UnusedSymbolInspection]
* to ensure that configuration of all scripts containing some symbol are up-to-date or try load it in sync.
* Note: it makes sense only in case of "auto apply" mode and sync loader, in other cases all symbols just
* will be treated as used.
*
* ## Loading
*
* When requested, configuration will be loaded using first applicable [loaders].
* It can work synchronously or asynchronously.
*
* Synchronous loader will be called just immediately. Despite this, its result may not be applied immediately,
* see next section for details.
*
* Asynchronous loader will be called in background thread (by [BackgroundExecutor]).
*
* ## Applying
*
* By default loaded configuration will *not* be applied immediately. Instead, we show in editor notification
* that suggests user to apply changed configuration. This was done to avoid sporadically starting indexing of new roots,
* which may happens regularly for large Gradle projects.
*
* Notification will be displayed when configuration is going to be updated. First configuration will be loaded
* without notification.
*
* This behavior may be disabled by enabling "auto reload" in project settings.
* When enabled, all loaded configurations will be applied immediately, without any notification.
*
* ## Concurrency
*
* Each files may be in on of this state:
* - scriptDefinition is not ready
* - not loaded
* - up-to-date
* - invalid, in queue (in [BackgroundExecutor] queue)
* - invalid, loading
* - invalid, waiting for apply
*
* [reloadOutOfDateConfiguration] guard this states. See it's docs for more details.
*/
internal class DefaultScriptConfigurationManager(project: Project) :
AbstractScriptConfigurationManager(project) {
private val backgroundExecutor = BackgroundExecutor(project, rootsIndexer)
private val memoryCache = ScriptConfigurationMemoryCache(project)
private val fileAttributesCache = ScriptConfigurationFileAttributeCache()
private val fromRefinedLoader = FromRefinedConfigurationLoader()
private val loaders = arrayListOf(
OutsiderFileDependenciesLoader(this),
fromRefinedLoader,
fileAttributesCache
private val loaders: List<ScriptConfigurationLoader> = listOf(
ScriptOutsiderFileConfigurationLoader(project),
ScriptConfigurationFileAttributeCache(project),
DefaultScriptConfigurationLoader(project)
)
private val backgroundLoader = BackgroundLoader(project, rootsManager, ::reloadConfigurationAsync)
private val listeners: List<ScriptChangeListener> = listOf(
DefaultScriptChangeListener()
)
private val listener = ScriptsListener(project, this)
private val notifier = ScriptChangesNotifier(project, updater, listeners)
/**
* Loaded but not applied result.
* Weakness required since it is hard to track editor and notification hiding.
*/
private val notApplied = WeakHashMap<VirtualFile, LoadedScriptConfiguration>()
private val saveLock = ReentrantLock()
override fun createCache(): ScriptConfigurationCache {
return object : ScriptConfigurationMemoryCache(project) {
override fun markOutOfDate(file: VirtualFile) {
super.markOutOfDate(file)
synchronized(notApplied) {
notApplied.remove(file)
}
}
}
}
/**
* Will be called on [cache] miss to initiate loading of [file]'s script configuration.
*
* ## Concurrency
*
* Each files may be in on of the states described below:
* - scriptDefinition is not ready. `ScriptDefinitionsManager.getInstance(project).isReady() == false`.
* [clearConfigurationCachesAndRehighlight] will be called when [ScriptDefinitionsManager] will be ready
* which will call [reloadOutOfDateConfiguration] for opened editors.
* - unknown. When [isFirstLoad] true (`cache[file] == null`).
* - up-to-date. `cache[file]?.upToDate == true`.
* - invalid, in queue. `cache[file]?.upToDate == false && file in backgroundExecutor`.
* - invalid, loading. `cache[file]?.upToDate == false && file !in backgroundExecutor`.
* - invalid, waiting for apply. `cache[file]?.upToDate == false && file !in backgroundExecutor` and has notification panel?
*
* Async:
* - up-to-date:
* [reloadOutOfDateConfiguration] will not be called.
* - `unknown` and `invalid, in queue`:
* Concurrent async loading will be guarded by `backgroundExecutor.ensureScheduled`
* (only one task per file will be scheduled at same time)
* - `invalid`:
* Loading should be rescheduled, since the work already started for old input.
* This will work, because file will be removed from backgroundExecutor.
* - `loading`: Scheduled loading for unchanged file will be noop thanks to isUpToDate check
* - `not applied`: Scheduled loading for unchanged file with loaded but not applied
* configuration will be also noop thanks check in [notApplied] map.
*
* Sync:
* - up-to-date:
* [reloadOutOfDateConfiguration] will not be called.
* - all other states, i.e: `unknown`, `invalid, in queue`, `invalid, loading` and `invalid, ready for apply`:
* everything will be computed just in place, possible concurrently.
* [suggestOrSaveConfiguration] calls will be serialized by the [saveLock]
*/
override fun reloadOutOfDateConfiguration(
file: KtFile,
isFirstLoad: Boolean,
loadEvenWillNotBeApplied: Boolean,
forceSync: Boolean
) {
val virtualFile = file.originalFile.virtualFile ?: return
val autoReloadEnabled = KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
val shouldLoad = isFirstLoad || loadEvenWillNotBeApplied || autoReloadEnabled
if (!shouldLoad) return
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return
val scriptDefinition = file.findScriptDefinition() ?: return
val (async, sync) = loaders.partition { it.shouldRunInBackground(scriptDefinition) }
val syncLoader = sync.firstOrNull { it.loadDependencies(isFirstLoad, virtualFile, scriptDefinition, loadingContext) }
if (syncLoader == null) {
// run async loader
if (forceSync) {
async.first { it.loadDependencies(isFirstLoad, virtualFile, scriptDefinition, loadingContext) }
} else {
backgroundExecutor.ensureScheduled(virtualFile) {
// don't start loading if nothing was changed
// (in case we checking for up-to-date and loading concurrently)
val cached = getCachedConfiguration(virtualFile)
if (cached?.inputs?.isUpToDate(project, virtualFile) != true) {
val prevNotApplied = synchronized(notApplied) { notApplied[virtualFile] }
if (prevNotApplied?.inputs?.isUpToDate(project, virtualFile) == true) {
// reuse loaded but not applied result
// (in case we checking for up-to-date and waiting notification answer concurrently)
loadingContext.suggestNewConfiguration(
virtualFile,
prevNotApplied
)
} else {
synchronized(notApplied) {
notApplied.remove(virtualFile)
}
val actualIsFirstLoad = cached == null
async.first { it.loadDependencies(actualIsFirstLoad, virtualFile, scriptDefinition, loadingContext) }
}
}
}
}
}
}
/**
* 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 {
override fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, LoadedScriptConfiguration>>) {
rootsIndexer.transaction {
for ((file, result) in files) {
saveConfiguration(file, result, skipNotification = true)
loadingContext.saveNewConfiguration(file, result)
}
}
}
/**
* 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
private val loadingContext = object : ScriptConfigurationLoadingContext {
override fun getCachedConfiguration(file: VirtualFile): CachedConfigurationSnapshot? =
this@DefaultScriptConfigurationManager.getCachedConfiguration(file)
val notCached = files.filterNot { isConfigurationUpToDate(it.originalFile.virtualFile) }
if (notCached.isNotEmpty()) {
rootsManager.transaction {
for (file in notCached) {
reloadConfiguration(file)
}
}
return true
override fun suggestNewConfiguration(file: VirtualFile, newResult: LoadedScriptConfiguration) {
suggestOrSaveConfiguration(file, newResult, false)
}
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() {
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
if (project.isOpen) {
rehighlightOpenedScripts()
override fun saveNewConfiguration(file: VirtualFile, newResult: LoadedScriptConfiguration) {
suggestOrSaveConfiguration(file, newResult, true)
}
}
@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 ScriptConfigurationManager.getConfiguration
*/
private fun saveConfiguration(
private fun suggestOrSaveConfiguration(
file: VirtualFile,
newResult: ScriptCompilationConfigurationResult,
skipNotification: Boolean = false,
skipSaveToAttributes: Boolean = false
newResult: LoadedScriptConfiguration,
skipNotification: Boolean
) {
debug(file) { "configuration received = $newResult" }
saveLock.withLock {
debug(file) { "configuration received = $newResult" }
saveReports(file, newResult.reports)
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)
}
rootsManager.transaction {
saveChangedConfiguration(file, newConfiguration, skipSaveToAttributes)
}
val newConfiguration = newResult.configuration
if (newConfiguration != null) {
val newConfigurationSnapshot = CachedConfigurationSnapshot(newResult.inputs, newConfiguration)
val oldConfiguration = getCachedConfiguration(file)?.configuration
if (oldConfiguration == newConfiguration) {
file.removeScriptDependenciesNotificationPanel(project)
} else {
debug(file) {
"configuration changed, notification is shown: old = $oldConfiguration, new = $newConfiguration"
}
file.addScriptDependenciesNotificationPanel(
newConfiguration, project,
onClick = {
val autoReload = skipNotification
|| oldConfiguration == null
|| KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
|| ApplicationManager.getApplication().isUnitTestMode
if (autoReload) {
if (oldConfiguration != null) {
file.removeScriptDependenciesNotificationPanel(project)
rootsManager.transaction {
saveChangedConfiguration(file, it, skipSaveToAttributes)
}
}
)
saveChangedConfiguration(file, newConfigurationSnapshot)
} else {
debug(file) {
"configuration changed, notification is shown: old = $oldConfiguration, new = $newConfiguration"
}
synchronized(notApplied) {
notApplied[file] = newResult
}
file.addScriptDependenciesNotificationPanel(
newConfiguration, project,
onClick = {
file.removeScriptDependenciesNotificationPanel(project)
rootsIndexer.transaction {
saveChangedConfiguration(file, newConfigurationSnapshot)
}
},
onHide = {
synchronized(notApplied) {
notApplied.remove(file)
}
}
)
}
}
}
}
}
private fun saveChangedConfiguration(
file: VirtualFile,
newConfiguration: ScriptCompilationConfigurationWrapper?,
skipSaveToAttributes: Boolean
) {
debug(file) { "configuration changed = $newConfiguration" }
override fun saveChangedConfiguration(file: VirtualFile, newConfigurationSnapshot: CachedConfigurationSnapshot?) {
super.saveChangedConfiguration(file, newConfigurationSnapshot)
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()
synchronized(notApplied) {
notApplied.remove(file)
}
updateHighlighting(listOf(file))
}
private fun saveReports(
@@ -304,49 +314,12 @@ class DefaultScriptConfigurationManager internal constructor(private val project
GlobalScope.launch(EDT(project)) {
if (project.isDisposed) return@launch
val ktFile = PsiManager.getInstance(project).findFile(file)
if (ktFile != null) {
DaemonCodeAnalyzer.getInstance(project).restart(ktFile)
}
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 rehighlightOpenedScripts() {
val openedScripts = FileEditorManager.getInstance(project).openFiles.filterNot { it.isNonScript() }
updateHighlighting(openedScripts)
}
private fun scriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return memoryCache.scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE
}
private fun scriptSdk(file: VirtualFile): Sdk? {
return memoryCache.scriptsSdksCache[file]
}
}
@@ -0,0 +1,37 @@
/*
* 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.configuration.cache
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
interface CachedConfigurationInputs {
fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile? = null): Boolean
object OutOfDate : CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean = false
}
data class PsiModificationStamp(val modificationStamp: Long) : CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean =
ktFile?.modificationStamp == modificationStamp
}
}
data class CachedConfigurationSnapshot(
val inputs: CachedConfigurationInputs,
val configuration: ScriptCompilationConfigurationWrapper
)
interface ScriptConfigurationCache {
operator fun get(file: VirtualFile): CachedConfigurationSnapshot?
operator fun set(file: VirtualFile, configurationSnapshot: CachedConfigurationSnapshot)
fun markOutOfDate(file: VirtualFile)
fun all(): Collection<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>
}
@@ -5,47 +5,56 @@
package org.jetbrains.kotlin.idea.core.script.configuration.cache
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.loader.LoadedScriptConfiguration
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile
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.definitions.findScriptDefinition
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 :
ScriptConfigurationLoader {
operator fun contains(file: VirtualFile): Boolean =
file.scriptDependencies != null || file.scriptCompilationConfiguration != null
override val skipSaveToAttributes: Boolean
get() = true
override val skipNotification: Boolean
get() = true
internal class ScriptConfigurationFileAttributeCache(
val project: Project
) : ScriptConfigurationLoader {
/**
* todo(KT-34444): this should be changed to storing all roots in the persistent file cache
*/
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult? {
if (!firstLoad) return null
isFirstLoad: Boolean,
virtualFile: VirtualFile,
scriptDefinition: ScriptDefinition,
context: ScriptConfigurationLoadingContext
): Boolean {
if (!isFirstLoad) return false
val virtualFile = file.originalFile.virtualFile
val fromFs = load(virtualFile) ?: return false
// todo(KT-34444): save inputs to fs
val result = LoadedScriptConfiguration(CachedConfigurationInputs.OutOfDate, listOf(), fromFs)
context.saveNewConfiguration(virtualFile, result)
return true
}
private fun load(
virtualFile: VirtualFile
): ScriptCompilationConfigurationWrapper? {
val ktFile = project.getKtFile(virtualFile) ?: return null
val scriptSource = KtFileScriptSource(ktFile)
val configurationFromAttributes =
virtualFile.scriptCompilationConfiguration?.let {
FromCompilationConfiguration(KtFileScriptSource(file), it)
FromCompilationConfiguration(scriptSource, it)
} ?: virtualFile.scriptDependencies?.let {
FromLegacy(KtFileScriptSource(file), it, scriptDefinition)
FromLegacy(scriptSource, it, ktFile.findScriptDefinition())
} ?: return null
@@ -56,7 +65,7 @@ class ScriptConfigurationFileAttributeCache :
return null
}
return configurationFromAttributes.asSuccess()
return configurationFromAttributes
}
private fun areDependenciesValid(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper): Boolean {
@@ -78,7 +87,7 @@ class ScriptConfigurationFileAttributeCache :
file.scriptDependencies = null
file.scriptCompilationConfiguration = null
} else {
if (value is FromLegacy) {
if (value is ScriptCompilationConfigurationWrapper.FromLegacy) {
file.scriptDependencies = value.legacyDependencies
} else {
if (file.scriptDependencies != null) file.scriptDependencies = null
@@ -5,326 +5,46 @@
package org.jetbrains.kotlin.idea.core.script.configuration.cache
import com.intellij.ProjectTopics
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.PsiManager
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 org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.KotlinScriptDependenciesClassFinder
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache.Companion.MAX_SCRIPTS_CACHED
import org.jetbrains.kotlin.idea.core.script.configuration.AbstractScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.isAccessible
class ScriptConfigurationMemoryCache internal constructor(private val project: Project) {
import java.io.DataInput
import java.io.DataOutput
abstract class ScriptConfigurationMemoryCache(
val project: Project
) : ScriptConfigurationCache {
companion object {
const val MAX_SCRIPTS_CACHED = 50
}
init {
val connection = project.messageBus.connect()
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) {
clearClassRootsCaches()
}
})
private val memoryCache = SLRUMap<VirtualFile, CachedConfigurationSnapshot>(MAX_SCRIPTS_CACHED, MAX_SCRIPTS_CACHED)
@Synchronized
override operator fun get(file: VirtualFile): CachedConfigurationSnapshot? {
return memoryCache.get(file)
}
private val cacheLock = ReentrantReadWriteLock()
private val scriptDependenciesCache =
SLRUCacheWithLock<VirtualFile, ScriptCompilationConfigurationWrapper>()
private val scriptsModificationStampsCache =
SLRUCacheWithLock<VirtualFile, Long>()
fun getCachedConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? = scriptDependenciesCache.get(file)
fun isConfigurationUpToDate(file: VirtualFile): Boolean {
return scriptsModificationStampsCache.get(file) == file.modificationStamp
}
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 = getConfiguration(file)
?: return@createWeakMap GlobalSearchScope.EMPTY_SCOPE
val roots = compilationConfiguration.dependenciesClassPath
val sdk = scriptsSdksCache[file]
@Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) {
return@createWeakMap NonClasspathDirectoriesScope.compose(
ScriptConfigurationManager.toVfsRoots(
roots
)
)
}
return@createWeakMap NonClasspathDirectoriesScope.compose(
sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() +
ScriptConfigurationManager.toVfsRoots(roots)
)
}
val scriptsSdksCache: MutableMap<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = getConfiguration(file)
return@createWeakMap getScriptSdk(compilationConfiguration) ?: ScriptConfigurationManager.getScriptDefaultSdk(
project
)
}
private fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
val configuration = getCachedConfiguration(file)
if (configuration != null) return configuration
val ktFile = runReadAction { PsiManager.getInstance(project).findFile(file) as? KtFile } ?: return null
return ScriptConfigurationManager.getInstance(project)
.getConfiguration(ktFile)
}
private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? {
// workaround for mismatched gradle wrapper and plugin version
val javaHome = try {
compilationConfiguration?.javaHome?.let { VfsUtil.findFileByIoFile(it, true) }
} catch (e: Throwable) {
null
} ?: return null
return getAllProjectSdks().find { it.homeDirectory == javaHome }
}
val firstScriptSdk: Sdk?
get() {
val firstCachedScript = scriptDependenciesCache.getAll().firstOrNull()?.key ?: return null
return scriptsSdksCache[firstCachedScript]
}
private val allSdks by ClearableLazyValue(cacheLock) {
scriptDependenciesCache.getAll()
.mapNotNull { scriptsSdksCache[it.key] }
.distinct()
}
private val allNonIndexedSdks by ClearableLazyValue(
cacheLock
) {
scriptDependenciesCache.getAll()
.mapNotNull { scriptsSdksCache[it.key] }
.filterNonModuleSdk()
.distinct()
}
val allDependenciesClassFiles by ClearableLazyValue(
cacheLock
) {
val sdkFiles = allNonIndexedSdks
.flatMap { it.rootProvider.getFiles(OrderRootType.CLASSES).toList() }
val scriptDependenciesClasspath = scriptDependenciesCache.getAll()
.flatMap { it.value.dependenciesClassPath }.distinct()
sdkFiles + ScriptConfigurationManager.toVfsRoots(
scriptDependenciesClasspath
@Synchronized
override operator fun set(file: VirtualFile, configurationSnapshot: CachedConfigurationSnapshot) {
memoryCache.put(
file,
configurationSnapshot
)
}
val allDependenciesSources by ClearableLazyValue(cacheLock) {
val sdkSources = allNonIndexedSdks
.flatMap { it.rootProvider.getFiles(OrderRootType.SOURCES).toList() }
val scriptDependenciesSources = scriptDependenciesCache.getAll()
.flatMap { it.value.dependenciesSources }.distinct()
sdkSources + ScriptConfigurationManager.toVfsRoots(
scriptDependenciesSources
)
}
val allDependenciesClassFilesScope by ClearableLazyValue(
cacheLock
) {
NonClasspathDirectoriesScope.compose(allDependenciesClassFiles)
}
val allDependenciesSourcesScope by ClearableLazyValue(
cacheLock
) {
NonClasspathDirectoriesScope.compose(allDependenciesSources)
}
private fun List<Sdk>.filterNonModuleSdk(): List<Sdk> {
val moduleSdks = ModuleManager.getInstance(project).modules.map { ModuleRootManager.getInstance(it).sdk }
return filterNot { moduleSdks.contains(it) }
}
fun clearConfigurationCaches(): List<VirtualFile> {
debug { "configuration caches cleared" }
val keys = scriptDependenciesCache.getAll().map { it.key }.toList()
scriptDependenciesCache.clear()
scriptsModificationStampsCache.clear()
return keys
}
fun clearClassRootsCaches() {
debug { "class roots caches cleared" }
this::allSdks.clearValue()
this::allNonIndexedSdks.clearValue()
this::allDependenciesClassFiles.clearValue()
this::allDependenciesClassFilesScope.clearValue()
this::allDependenciesSources.clearValue()
this::allDependenciesSourcesScope.clearValue()
scriptsDependenciesClasspathScopeCache.clear()
scriptsSdksCache.clear()
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project).getExtensionPoint(PsiElementFinder.EP_NAME).extensions
.filterIsInstance<KotlinScriptDependenciesClassFinder>()
.single()
kotlinScriptDependenciesClassFinder.clearCache()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean {
val scriptSdk = getScriptSdk(compilationConfiguration) ?: ScriptConfigurationManager.getScriptDefaultSdk(
project
)
val wasSdkChanged = scriptSdk != null && !allSdks.contains(scriptSdk)
if (wasSdkChanged) {
debug { "sdk was changed: $compilationConfiguration" }
return true
}
val newClassRoots = ScriptConfigurationManager.toVfsRoots(
compilationConfiguration.dependenciesClassPath
)
for (newClassRoot in newClassRoots) {
if (!allDependenciesClassFiles.contains(newClassRoot)) {
debug { "class root was changed: $newClassRoot" }
return true
}
}
val newSourceRoots = ScriptConfigurationManager.toVfsRoots(
compilationConfiguration.dependenciesSources
)
for (newSourceRoot in newSourceRoots) {
if (!allDependenciesSources.contains(newSourceRoot)) {
debug { "source root was changed: $newSourceRoot" }
return true
}
}
return false
}
}
private fun <R> KProperty0<R>.clearValue() {
isAccessible = true
(getDelegate() as ClearableLazyValue<*, *>).clear()
}
private class ClearableLazyValue<in R, out T : Any>(
private val lock: ReentrantReadWriteLock,
private val compute: () -> T
) : ReadOnlyProperty<R, T> {
override fun getValue(thisRef: R, property: KProperty<*>): T {
lock.write {
if (value == null) {
value = compute()
}
return value!!
@Synchronized
override fun markOutOfDate(file: VirtualFile) {
val old = memoryCache[file]
if (old != null) {
memoryCache.put(file, old.copy(inputs = CachedConfigurationInputs.OutOfDate))
}
}
private var value: T? = null
fun clear() {
lock.write {
value = null
}
}
}
private class SLRUCacheWithLock<K, V> {
private val lock = ReentrantReadWriteLock()
val cache = SLRUMap<K, V>(
MAX_SCRIPTS_CACHED,
MAX_SCRIPTS_CACHED
)
fun get(value: K): V? = lock.write {
cache[value]
}
fun getOrPut(key: K, defaultValue: () -> V): V = lock.write {
val value = cache.get(key)
return if (value == null) {
val answer = defaultValue()
replace(key, answer)
answer
} else {
value
}
}
fun remove(file: K) = lock.write {
cache.remove(file)
}
fun getAll(): Collection<Map.Entry<K, V>> = lock.write {
cache.entrySet()
}
fun clear() = lock.write {
cache.clear()
}
fun replace(file: K, value: V): V? = lock.write {
val old = get(file)
cache.put(file, value)
old
}
@Synchronized
override fun all() = memoryCache.entrySet().map { it.key to it.value.configuration }
}
@@ -0,0 +1,19 @@
/*
* 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.configuration.listener
import org.jetbrains.kotlin.psi.KtFile
open class DefaultScriptChangeListener : ScriptChangeListener {
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
updater.ensureUpToDatedConfigurationSuggested(file)
return true
}
override fun documentChanged(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
updater.ensureUpToDatedConfigurationSuggested(file)
return true
}
}
@@ -0,0 +1,22 @@
/*
* 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.configuration.listener
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager
/**
* [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed.
* (it treated as applicable if [editorActivated] or [documentChanged] will return true).
*
* Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
*
* @see DefaultScriptConfigurationManager for more details.
*/
interface ScriptChangeListener {
fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean
fun documentChanged(file: KtFile, updater: ScriptConfigurationUpdater): Boolean
}
@@ -23,9 +23,10 @@ import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
class ScriptChangesNotifier(
internal class ScriptChangesNotifier(
private val project: Project,
private val scriptsManager: DefaultScriptConfigurationManager
private val updater: ScriptConfigurationUpdater,
private val listeners: List<ScriptChangeListener>
) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400
@@ -47,7 +48,7 @@ class ScriptChangesNotifier(
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
scriptsManager.updateConfigurationsIfNotCached(listOf(ktFile))
listeners.first { it.editorActivated(ktFile, updater) }
}
})
@@ -60,7 +61,7 @@ class ScriptChangesNotifier(
scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest(
{ scriptsManager.updateConfigurationsIfNotCached(listOf(ktFile)) },
{ listeners.first { it.documentChanged(ktFile, updater) } },
scriptChangesListenerDelay,
true
)
@@ -0,0 +1,30 @@
/*
* 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.configuration.listener
import org.jetbrains.kotlin.psi.KtFile
interface ScriptConfigurationUpdater {
/**
* Ensure that configuration for [file] is up-to-date,
* or new configuration loading is started
*/
fun ensureUpToDatedConfigurationSuggested(file: KtFile)
/**
* Check that configurations for given [files] is up-to-date or try synchronously update
* them. Synchronous update will be occurred only if auto apply is enabled and
* configuration for given file will not be loaded in background.
*
* @return true, if all files are already up-to-date or updated during this call
*/
fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean
/**
* Invalidate current configuration and start loading new
*/
fun forceConfigurationReload(file: KtFile)
}
@@ -5,37 +5,56 @@
package org.jetbrains.kotlin.idea.core.script.configuration.loader
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile
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.api.valueOrNull
import kotlin.script.experimental.dependencies.AsyncDependenciesResolver
class DefaultScriptConfigurationLoader internal constructor() :
ScriptConfigurationLoader {
override fun isAsync(file: KtFile, scriptDefinition: ScriptDefinition): Boolean {
return scriptDefinition.asLegacyOrNull<KotlinScriptDefinition>()?.dependencyResolver?.let {
it is AsyncDependenciesResolver || it is LegacyResolverWrapper
} ?: false
}
open class DefaultScriptConfigurationLoader(val project: Project) : ScriptConfigurationLoader {
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean =
scriptDefinition
.asLegacyOrNull<KotlinScriptDefinition>()
?.dependencyResolver
?.let { it is AsyncDependenciesResolver || it is LegacyResolverWrapper }
?: false
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult? {
isFirstLoad: Boolean,
virtualFile: VirtualFile,
scriptDefinition: ScriptDefinition,
context: ScriptConfigurationLoadingContext
): Boolean {
val file = project.getKtFile(virtualFile) ?: return false
debug(file) { "start dependencies loading" }
val result = refineScriptCompilationConfiguration(KtFileScriptSource(file), scriptDefinition, file.project)
val inputs = getInputsStamp(file)
val scriptingApiResult = refineScriptCompilationConfiguration(
KtFileScriptSource(file), scriptDefinition, file.project
)
val result = LoadedScriptConfiguration(
inputs,
scriptingApiResult.reports,
scriptingApiResult.valueOrNull()
)
context.suggestNewConfiguration(virtualFile, result)
debug(file) { "finish dependencies loading" }
return result
return true
}
}
protected open fun getInputsStamp(file: KtFile): CachedConfigurationInputs =
CachedConfigurationInputs.PsiModificationStamp(file.modificationStamp)
}
@@ -0,0 +1,16 @@
/*
* 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.configuration.loader
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import kotlin.script.experimental.api.ScriptDiagnostic
data class LoadedScriptConfiguration(
val inputs: CachedConfigurationInputs,
val reports: List<ScriptDiagnostic>,
val configuration: ScriptCompilationConfigurationWrapper?
)
@@ -5,23 +5,26 @@
package org.jetbrains.kotlin.idea.core.script.configuration.loader
import org.jetbrains.kotlin.psi.KtFile
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
// TODO: rename - this is not only about dependencies anymore
/**
* Provides the way to loading and saving script configuration.
*
* @see [org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager] for more details.
*/
internal interface ScriptConfigurationLoader {
fun isAsync(file: KtFile, scriptDefinition: ScriptDefinition) = false
val skipSaveToAttributes: Boolean
get() = false
val skipNotification: Boolean
get() = false
fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean = false
/**
* Implementation should load configuration and call `context.suggestNewConfiguration` or `saveNewConfiguration`.
*
* @return true when this loader is applicable.
*/
fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult?
isFirstLoad: Boolean,
virtualFile: VirtualFile,
scriptDefinition: ScriptDefinition,
context: ScriptConfigurationLoadingContext
): Boolean
}
@@ -0,0 +1,38 @@
/*
* 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.configuration.loader
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache
interface ScriptConfigurationLoadingContext {
fun getCachedConfiguration(file: VirtualFile): CachedConfigurationSnapshot?
/**
* Show notification about new configuration with suggestion to apply it.
* User may disable this notifications, in this case configuration will be saved immediately.
*
* If configuration is null, then the result will be treated as failed, and
* reports will be displayed immediately.
*
* @sample DefaultScriptConfigurationLoader.loadDependencies
*/
fun suggestNewConfiguration(
file: VirtualFile,
newResult: LoadedScriptConfiguration
)
/**
* Save [newResult] for [file] into caches and update highlighting.
*
* @sample ScriptConfigurationFileAttributeCache.loadDependencies
*/
fun saveNewConfiguration(
file: VirtualFile,
newResult: LoadedScriptConfiguration
)
}
@@ -5,32 +5,35 @@
package org.jetbrains.kotlin.idea.core.script.configuration.loader
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.highlighter.OutsidersPsiFileSupportUtils
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 ScriptOutsiderFileConfigurationLoader(private val manager: DefaultScriptConfigurationManager) :
internal class ScriptOutsiderFileConfigurationLoader(val project: Project) :
ScriptConfigurationLoader {
override val skipSaveToAttributes: Boolean
get() = true
override val skipNotification: Boolean
get() = true
override fun loadDependencies(
firstLoad: Boolean,
file: KtFile,
scriptDefinition: ScriptDefinition
): ScriptCompilationConfigurationResult? {
val virtualFile = file.virtualFile ?: return null
val project = file.project
isFirstLoad: Boolean,
virtualFile: VirtualFile,
scriptDefinition: ScriptDefinition,
context: ScriptConfigurationLoadingContext
): Boolean {
if (!isFirstLoad) return 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()
val fileOrigin = OutsidersPsiFileSupportUtils.getOutsiderFileOrigin(
project,
virtualFile
) ?: return false
val original = context.getCachedConfiguration(fileOrigin)
if (original != null) {
context.saveNewConfiguration(virtualFile, LoadedScriptConfiguration(original.inputs, listOf(), original.configuration))
}
// todo(KT-34615): initiate loading configuration for original file and subscribe to it's result?
// should we show "new context" notification for both files?
return true
}
}
@@ -5,102 +5,233 @@
package org.jetbrains.kotlin.idea.core.script.configuration.utils
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 com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.Alarm
import com.intellij.util.containers.HashSetQueue
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
import java.util.*
/**
* Sequentially loads script configuration in background.
* Loading tasks scheduled by calling [ensureScheduled].
*
* Progress indicator will be shown after [PROGRESS_INDICATOR_DELAY] ms or if
* more then [PROGRESS_INDICATOR_MIN_QUEUE] tasks scheduled.
*
* States:
* silentWorker underProgressWorker
* - sleep
* - silent x
* - silent and under progress x x
* - under progress x
*/
internal class BackgroundExecutor(
private val project: Project,
private val rootsManager: ScriptClassRootsIndexer,
private val loadDependencies: (KtFile) -> Unit
val project: Project,
val rootsManager: ScriptClassRootsIndexer
) {
private var task: Task? = null
companion object {
const val PROGRESS_INDICATOR_DELAY = 1000
const val PROGRESS_INDICATOR_MIN_QUEUE = 3
}
private val work = Any()
private val queue: Queue<LoadTask> = HashSetQueue()
/**
* Let's fix queue size when progress bar displayed.
* Progress for rest items will be counted from zero
*/
private var currentProgressSize: Int = 0
private var currentProgressDone: Int = 0
private var silentWorker: SilentWorker? = null
private var underProgressWorker: UnderProgressWorker? = null
private val longRunningAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project)
private var longRunningAlaramRequested = false
private var inTransaction: Boolean = false
private var currentFile: VirtualFile? = null
class LoadTask(val key: VirtualFile, val actions: () -> Unit) {
override fun equals(other: Any?) =
this === other || (other is LoadTask && key == other.key)
override fun hashCode() = key.hashCode()
}
@Synchronized
fun scheduleAsync(file: KtFile) {
if (task == null) {
startBatch(file)
} else {
task!!.addTask(file)
fun ensureScheduled(key: VirtualFile, actions: () -> Unit) {
val task = LoadTask(key, actions)
if (queue.add(task)) {
debug(task.key) { "added to update queue" }
updateProgress()
// If the queue is longer than PROGRESS_INDICATOR_MIN_QUEUE, show progress and cancel button
if (queue.size > PROGRESS_INDICATOR_MIN_QUEUE) {
requireUnderProgressWorker()
} else {
requireSilentWorker()
if (!longRunningAlaramRequested) {
longRunningAlaramRequested = true
longRunningAlarm.addRequest(
{
longRunningAlaramRequested = false
requireUnderProgressWorker()
},
PROGRESS_INDICATOR_DELAY
)
}
}
}
}
@Synchronized
private fun startBatch(file: KtFile) {
private fun requireSilentWorker() {
if (silentWorker == null && underProgressWorker == null) {
silentWorker = SilentWorker().also { it.start() }
}
}
@Synchronized
private fun requireUnderProgressWorker() {
if (queue.isEmpty() && silentWorker == null) return
silentWorker?.stopGracefully()
if (underProgressWorker == null) {
underProgressWorker = UnderProgressWorker().also { it.start() }
restartProgressBar()
updateProgress()
}
}
@Synchronized
private fun restartProgressBar() {
currentProgressSize = queue.size
currentProgressDone = 0
}
@Synchronized
fun updateProgress() {
underProgressWorker?.progressIndicator?.let {
it.text2 = currentFile?.path ?: ""
if (queue.size == 0) {
// last file
it.isIndeterminate = true
} else {
it.isIndeterminate = false
if (currentProgressDone > currentProgressSize) {
restartProgressBar()
}
it.fraction = currentProgressDone.toDouble() / currentProgressSize.toDouble()
}
}
}
@Synchronized
private fun ensureInTransaction() {
if (inTransaction) return
inTransaction = true
rootsManager.startTransaction()
task = Task()
task!!.addTask(file)
task!!.start()
}
@Synchronized
private fun endBatch() {
task = null
check(inTransaction)
rootsManager.commit()
inTransaction = false
}
private inner class Task {
private val sequenceOfFiles: ConcurrentLinkedQueue<KtFile> = ConcurrentLinkedQueue()
private var forceStop: Boolean = false
private var startedSilently: Boolean = false
private abstract inner class Worker {
private var shouldStop = false
fun start() {
if (KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled) {
startWithProgress()
} else {
startSilently()
open fun start() {
ensureInTransaction()
}
fun stopGracefully() {
shouldStop = true
}
protected open fun checkCancelled() = false
protected abstract fun close()
protected fun run() {
try {
while (true) {
// prevent parallel work in both silent and under progress
synchronized(work) {
val next = synchronized(this@BackgroundExecutor) {
if (shouldStop) return
if (checkCancelled() || queue.isEmpty()) {
endBatch()
return
}
queue.poll()?.also {
currentFile = it.key
currentProgressDone++
updateProgress()
}
}
next?.actions?.invoke()
synchronized(work) {
currentFile = null
}
}
}
} finally {
close()
}
}
}
private fun restartWithProgress() {
forceStop = true
startWithProgress()
forceStop = false
}
private inner class UnderProgressWorker : Worker() {
var progressIndicator: ProgressIndicator? = null
private fun startSilently() {
startedSilently = true
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
loadDependencies(EmptyProgressIndicator())
})
}
override fun start() {
super.start()
private fun startWithProgress() {
startedSilently = false
object : com.intellij.openapi.progress.Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) {
override fun run(indicator: ProgressIndicator) {
loadDependencies(indicator)
progressIndicator = indicator
updateProgress()
run()
}
}.queue()
}
fun addTask(file: KtFile) {
if (file in sequenceOfFiles) return
override fun checkCancelled(): Boolean = progressIndicator?.isCanceled == true
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()
override fun close() {
synchronized(this@BackgroundExecutor) {
underProgressWorker = null
progressIndicator = null
}
}
}
private fun loadDependencies(indicator: ProgressIndicator?) {
while (true) {
if (forceStop) return
if (indicator?.isCanceled == true || sequenceOfFiles.isEmpty()) {
endBatch()
return
}
private inner class SilentWorker : Worker() {
override fun start() {
super.start()
loadDependencies(sequenceOfFiles.poll())
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
run()
})
}
override fun close() {
synchronized(this@BackgroundExecutor) {
silentWorker = null
}
}
}
@@ -0,0 +1,150 @@
/*
* 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.configuration.utils
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCache
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
internal class ScriptClassRootsCache(
private val project: Project,
private val cache: ScriptConfigurationCache
) {
private val all: Collection<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>> get() = cache.all()
private fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? = cache[file]?.configuration
private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? {
// workaround for mismatched gradle wrapper and plugin version
val javaHome = try {
compilationConfiguration?.javaHome?.let { VfsUtil.findFileByIoFile(it, true) }
} catch (e: Throwable) {
null
} ?: return null
return getAllProjectSdks().find { it.homeDirectory == javaHome }
}
private val scriptsSdksCache: Map<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = getConfiguration(file)
return@createWeakMap getScriptSdk(compilationConfiguration)
?: ScriptConfigurationManager.getScriptDefaultSdk(project)
}
fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file]
fun getFirstScriptsSdk(): Sdk? {
val firstCachedScript = all.firstOrNull()?.first ?: return null
return scriptsSdksCache[firstCachedScript]
}
private val allSdks by lazy {
all.mapNotNull { scriptsSdksCache[it.first] }
.distinct()
}
private val allNonIndexedSdks by lazy {
all.mapNotNull { scriptsSdksCache[it.first] }
.filterNonModuleSdk()
.distinct()
}
private fun List<Sdk>.filterNonModuleSdk(): List<Sdk> {
val moduleSdks = ModuleManager.getInstance(project).modules.map { ModuleRootManager.getInstance(it).sdk }
return filterNot { moduleSdks.contains(it) }
}
val allDependenciesClassFiles by lazy {
val sdkFiles = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.CLASSES).toList() }
val scriptDependenciesClasspath = all.flatMap { it.second.dependenciesClassPath }.distinct()
sdkFiles + ScriptConfigurationManager.toVfsRoots(scriptDependenciesClasspath)
}
val allDependenciesSources by lazy {
val sdkSources = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.SOURCES).toList() }
val scriptDependenciesSources = all.flatMap { it.second.dependenciesSources }.distinct()
sdkSources + ScriptConfigurationManager.toVfsRoots(scriptDependenciesSources)
}
val allDependenciesClassFilesScope by lazy {
NonClasspathDirectoriesScope.compose(allDependenciesClassFiles)
}
val allDependenciesSourcesScope by lazy {
NonClasspathDirectoriesScope.compose(allDependenciesSources)
}
private val scriptsDependenciesClasspathScopeCache: MutableMap<VirtualFile, GlobalSearchScope> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = getConfiguration(file)
?: return@createWeakMap GlobalSearchScope.EMPTY_SCOPE
val roots = compilationConfiguration.dependenciesClassPath
val sdk = scriptsSdksCache[file]
@Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) {
return@createWeakMap NonClasspathDirectoriesScope.compose(
ScriptConfigurationManager.toVfsRoots(
roots
)
)
}
return@createWeakMap NonClasspathDirectoriesScope.compose(
sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots(
roots
)
)
}
fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE
}
fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean {
val scriptSdk = getScriptSdk(compilationConfiguration)
?: ScriptConfigurationManager.getScriptDefaultSdk(project)
val wasSdkChanged = scriptSdk != null && !allSdks.contains(scriptSdk)
if (wasSdkChanged) {
debug { "sdk was changed: $compilationConfiguration" }
return true
}
val newClassRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesClassPath)
for (newClassRoot in newClassRoots) {
if (!allDependenciesClassFiles.contains(newClassRoot)) {
debug { "class root was changed: $newClassRoot" }
return true
}
}
val newSourceRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesSources)
for (newSourceRoot in newSourceRoots) {
if (!allDependenciesSources.contains(newSourceRoot)) {
debug { "source root was changed: $newSourceRoot" }
return true
}
}
return false
}
}
@@ -12,41 +12,32 @@ 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.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.debug
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.
* Utility for postponing indexing of new roots to the end of some bulk operation.
*/
internal class ScriptClassRootsIndexer(val project: Project) {
private var newRootsPresent: Boolean = false
private val concurrentTransactions = AtomicInteger()
fun checkNonCachedRoots(
cache: ScriptConfigurationMemoryCache,
file: VirtualFile,
configuration: ScriptCompilationConfigurationWrapper
) {
@Synchronized
fun markNewRoot(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper) {
debug(file) { "new class roots found: $configuration" }
checkInTransaction()
if (cache.hasNotCachedRoots(configuration)) {
synchronized(this) {
debug(file) { "new class roots found: $configuration" }
checkInTransaction()
newRootsPresent = true
}
}
newRootsPresent = true
}
private fun checkInTransaction() {
check(concurrentTransactions.get() > 0) { "Transaction is not started" }
fun checkInTransaction() {
check(concurrentTransactions.get() > 0)
}
inline fun transaction(body: () -> Unit) {
inline fun <T> transaction(body: () -> T): T {
startTransaction()
try {
return try {
body()
} finally {
commit()
@@ -77,8 +68,7 @@ internal class ScriptClassRootsIndexer(val project: Project) {
debug { "roots change event" }
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project)
.incModificationCount()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
@@ -0,0 +1,25 @@
/*
* 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.configuration.utils
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile
fun Project.getKtFile(
virtualFile: VirtualFile?,
ktFile: KtFile? = null
): KtFile? {
if (virtualFile == null) return null
if (ktFile != null) {
check(ktFile.virtualFile == virtualFile)
return ktFile
} else {
return runReadAction { PsiManager.getInstance(this).findFile(virtualFile) as? KtFile }
}
}
@@ -23,8 +23,10 @@ import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.pom.Navigatable
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.core.script.configuration.getGradleScriptInputsStamp
import org.jetbrains.kotlin.idea.core.script.configuration.loader.LoadedScriptConfiguration
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.VirtualFileScriptSource
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
@@ -32,7 +34,6 @@ import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import kotlin.script.experimental.api.ResultWithDiagnostics
import kotlin.script.experimental.dependencies.ScriptDependencies
class KotlinGradleBuildScriptsDataService : AbstractProjectDataService<GradleSourceSetData, Void>() {
@@ -57,16 +58,21 @@ class KotlinGradleBuildScriptsDataService : AbstractProjectDataService<GradleSou
)
val javaHome = File(gradleExeSettings.javaHome ?: return)
val files = mutableListOf<Pair<VirtualFile, ScriptCompilationConfigurationResult>>()
val files = mutableListOf<Pair<VirtualFile, LoadedScriptConfiguration>>()
projectDataNode.gradleKotlinBuildScripts?.forEach { buildScript ->
val scriptFile = File(buildScript.file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
// todo(KT-34440): take inputs snapshot before starting import
val inputs = getGradleScriptInputsStamp(project, virtualFile)
files.add(
Pair(
virtualFile,
ResultWithDiagnostics.Success(
LoadedScriptConfiguration(
inputs ?: CachedConfigurationInputs.OutOfDate,
listOf(),
ScriptCompilationConfigurationWrapper.FromLegacy(
VirtualFileScriptSource(virtualFile),
ScriptDependencies(
@@ -136,7 +136,7 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
val usedScripts = findScriptsWithUsages(declaration)
if (usedScripts.isNotEmpty()) {
if (ScriptConfigurationManager.getInstance(declaration.project).updateConfigurationsIfNotCached(usedScripts)) {
if (!ScriptConfigurationManager.getInstance(declaration.project).updater.ensureConfigurationUpToDate(usedScripts)) {
return TOO_MANY_OCCURRENCES
}
}