diff --git a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/KotlinHighlightingUtil.kt b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/KotlinHighlightingUtil.kt index 86f6c85a864..f2656bc597c 100644 --- a/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/KotlinHighlightingUtil.kt +++ b/idea/idea-analysis/src/org/jetbrains/kotlin/idea/highlighter/KotlinHighlightingUtil.kt @@ -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 } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationManager.kt index 79899237894..f2f004b2f94 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationManager.kt @@ -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 + + /** + * @see [getConfiguration] + */ + fun getScriptClasspath(file: KtFile): List + + /** + * 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>) + fun saveCompilationConfigurationAfterImport(files: List>) - /** - * 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): 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 - - fun getScriptClasspath(file: KtFile): List - 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 @@ -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) } } } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt index 9d86decbbe0..4971806fefe 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt @@ -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(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 } } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptTrafficLightRendererContributor.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptTrafficLightRendererContributor.kt index aaae8fcdb74..a1444ea1911 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptTrafficLightRendererContributor.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptTrafficLightRendererContributor.kt @@ -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 } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt new file mode 100644 index 00000000000..efcf142db78 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt @@ -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 { + val ktFile = project.getKtFile(file) ?: return emptyList() + return getScriptClasspath(ktFile) + } + + override fun getScriptClasspath(file: KtFile): List = + 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): 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, 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) { + 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() + .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 = classpathRoots.allDependenciesClassFiles + + override fun getAllScriptDependenciesSources(): List = classpathRoots.allDependenciesSources +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt index 354c66a0c22..cf724d971aa 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt @@ -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 = listOf( + ScriptOutsiderFileConfigurationLoader(project), + ScriptConfigurationFileAttributeCache(project), + DefaultScriptConfigurationLoader(project) ) - private val backgroundLoader = BackgroundLoader(project, rootsManager, ::reloadConfigurationAsync) + private val listeners: List = 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() + 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>) { - rootsManager.transaction { + override fun saveCompilationConfigurationAfterImport(files: List>) { + 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): 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 { - val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return emptyList() - return getScriptClasspath(ktFile) - } - - override fun getScriptClasspath(file: KtFile): List = - 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) { - 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) { - 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] - } } \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationCache.kt new file mode 100644 index 00000000000..fbd448103d8 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationCache.kt @@ -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> +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationFileAttributeCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationFileAttributeCache.kt index e1225b98e8e..0beec77fea4 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationFileAttributeCache.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationFileAttributeCache.kt @@ -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 diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationMemoryCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationMemoryCache.kt index 1898c8c5e62..adb0cac9278 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationMemoryCache.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/cache/ScriptConfigurationMemoryCache.kt @@ -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(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() - private val scriptsModificationStampsCache = - SLRUCacheWithLock() - - 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 = - 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 = - 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.filterNonModuleSdk(): List { - val moduleSdks = ModuleManager.getInstance(project).modules.map { ModuleRootManager.getInstance(it).sdk } - return filterNot { moduleSdks.contains(it) } - } - - fun clearConfigurationCaches(): List { - 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() - .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 KProperty0.clearValue() { - isAccessible = true - (getDelegate() as ClearableLazyValue<*, *>).clear() -} - -private class ClearableLazyValue( - private val lock: ReentrantReadWriteLock, - private val compute: () -> T -) : ReadOnlyProperty { - 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 { - private val lock = ReentrantReadWriteLock() - - val cache = SLRUMap( - 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> = 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 } } \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/DefaultScriptChangeListener.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/DefaultScriptChangeListener.kt new file mode 100644 index 00000000000..54b5ba93126 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/DefaultScriptChangeListener.kt @@ -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 + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt new file mode 100644 index 00000000000..92b3a26fc9a --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt @@ -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 +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangesNotifier.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangesNotifier.kt index 74dbb302824..e942bfbdeeb 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangesNotifier.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangesNotifier.kt @@ -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 ) { 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 ) diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt new file mode 100644 index 00000000000..4c2b11435fb --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt @@ -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): Boolean + + /** + * Invalidate current configuration and start loading new + */ + fun forceConfigurationReload(file: KtFile) +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/DefaultScriptConfigurationLoader.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/DefaultScriptConfigurationLoader.kt index 34586c7fc95..1d1d788bc33 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/DefaultScriptConfigurationLoader.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/DefaultScriptConfigurationLoader.kt @@ -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()?.dependencyResolver?.let { - it is AsyncDependenciesResolver || it is LegacyResolverWrapper - } ?: false - } +open class DefaultScriptConfigurationLoader(val project: Project) : ScriptConfigurationLoader { + override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean = + scriptDefinition + .asLegacyOrNull() + ?.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) +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/LoadedScriptConfiguration.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/LoadedScriptConfiguration.kt new file mode 100644 index 00000000000..6313da467d0 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/LoadedScriptConfiguration.kt @@ -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, + val configuration: ScriptCompilationConfigurationWrapper? +) \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt index ae5c2c75ba2..19c8976db84 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt @@ -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 } \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoadingContext.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoadingContext.kt new file mode 100644 index 00000000000..c202f8c41ec --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoadingContext.kt @@ -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 + ) +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptOutsiderFileConfigurationLoader.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptOutsiderFileConfigurationLoader.kt index 452560ea83a..6750fc9fce0 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptOutsiderFileConfigurationLoader.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptOutsiderFileConfigurationLoader.kt @@ -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 } } \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt index 54f165cd666..4ec9a9634f8 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt @@ -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 = 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 = 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 } } } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt new file mode 100644 index 00000000000..c7618ae6b0e --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt @@ -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> 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 = + 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.filterNonModuleSdk(): List { + 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 = + 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 + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsIndexer.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsIndexer.kt index d637b7c91fc..1c2547fb474 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsIndexer.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsIndexer.kt @@ -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 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() } } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/utils.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/utils.kt new file mode 100644 index 00000000000..8f9de3061ab --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/utils.kt @@ -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 } + } +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleBuildScriptsDataService.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleBuildScriptsDataService.kt index cfb1a6da6f3..ccf036600ca 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleBuildScriptsDataService.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/configuration/KotlinGradleBuildScriptsDataService.kt @@ -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() { @@ -57,16 +58,21 @@ class KotlinGradleBuildScriptsDataService : AbstractProjectDataService>() + val files = mutableListOf>() 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( diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt index f80ea5dfbc9..723e7a91cba 100644 --- a/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt +++ b/idea/src/org/jetbrains/kotlin/idea/inspections/UnusedSymbolInspection.kt @@ -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 } }