diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/LoadScriptConfigurationNotificationFactory.kt similarity index 84% rename from idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt rename to idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/LoadScriptConfigurationNotificationFactory.kt index ca57b194fb1..aab2960e052 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/LoadScriptConfigurationNotificationFactory.kt @@ -6,7 +6,6 @@ package org.jetbrains.kotlin.idea.core.script import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.project.Project @@ -15,28 +14,17 @@ import com.intellij.openapi.util.Ref import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.HyperlinkLabel -import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle import org.jetbrains.kotlin.psi.UserDataProperty -interface ScriptConfigurationNotificationFactory { - fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean - fun hideNotification(file: VirtualFile, project: Project): Boolean - - companion object { - val NOTIFICATION_FACTORY: ExtensionPointName = - ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.notificationFactory") - } -} - -class DefaultScriptConfigurationNotificationFactory : ScriptConfigurationNotificationFactory { - override fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean { +object LoadScriptConfigurationNotificationFactory { + fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean { file.addLoadConfigurationNotificationPanel(project, onClick) return true } - override fun hideNotification(file: VirtualFile, project: Project): Boolean { + fun hideNotification(file: VirtualFile, project: Project): Boolean { file.removeLoadConfigurationNotificationPanel(project) return true } 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 3a65a322d98..c4a374bb7ca 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 @@ -29,8 +29,8 @@ 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.AbstractScriptConfigurationManager -import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot +import org.jetbrains.kotlin.idea.core.script.configuration.CompositeManager +import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader import org.jetbrains.kotlin.psi.KtFile @@ -38,6 +38,7 @@ import org.jetbrains.kotlin.psi.UserDataProperty import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import java.io.File import kotlin.script.experimental.api.asSuccess import kotlin.script.experimental.api.makeFailureResult @@ -106,13 +107,6 @@ interface ScriptConfigurationManager { */ fun clearConfigurationCachesAndRehighlight() - /** - * Save configurations into cache. - * Start indexing for new class/source roots. - * Re-highlight opened scripts with changed configuration. - */ - fun saveCompilationConfigurationAfterImport(files: List>) - /////////////// // classpath roots info: @@ -168,12 +162,16 @@ interface ScriptConfigurationManager { @TestOnly fun updateScriptDependenciesSynchronously(file: PsiFile) { // TODO: review the usages of this method - (getInstance(file.project) as AbstractScriptConfigurationManager).updateScriptDependenciesSynchronously(file) + (getInstance(file.project) as CompositeManager).managers + .firstIsInstance() + .updateScriptDependenciesSynchronously(file) } @TestOnly fun clearCaches(project: Project) { - (getInstance(project) as AbstractScriptConfigurationManager).clearCaches() + (getInstance(project) as CompositeManager).managers + .firstIsInstance() + .clearCaches() } fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) { 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 index 07c110a4230..503d8ade1b4 100644 --- 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 @@ -6,293 +6,45 @@ package org.jetbrains.kotlin.idea.core.script.configuration import com.intellij.ProjectTopics -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.openapi.extensions.ExtensionPointName 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.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootListener 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.ScriptConfigurationCache -import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCacheScope -import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot -import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationState +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.listener.ScriptChangesNotifier import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader 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.idea.core.script.debug 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 org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull 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 [setAppliedConfiguration] - * 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) +abstract class ScriptingSupport { + abstract fun isRelated(file: VirtualFile): Boolean - @Suppress("LeakingThis") - protected val cache: ScriptConfigurationCache = createCache() + abstract fun clearCaches() + abstract fun hasCachedConfiguration(file: KtFile): Boolean + abstract fun getOrLoadConfiguration(virtualFile: VirtualFile, preloadedKtFile: KtFile? = null): ScriptCompilationConfigurationWrapper? - 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 [setAppliedConfiguration] - * 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 - * @param isPostponedLoad is used to postspone loading: show a notification for out of date script and start loading when user request - */ - protected abstract fun reloadOutOfDateConfiguration( - file: KtFile, - isFirstLoad: Boolean = getAppliedConfiguration(file.originalFile.virtualFile) == null, - loadEvenWillNotBeApplied: Boolean = false, - forceSync: Boolean = false, - isPostponedLoad: Boolean = false - ) - - /** - * Will be called on user action - * Load configuration event it is already cached or inputs are up-to-date - * - * @param loader is used to load configuration. Other loaders aren't taken into account. - */ - protected abstract fun forceReloadConfiguration( - file: KtFile, - loader: ScriptConfigurationLoader - ) - - @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 getCachedConfigurationState(file: VirtualFile?): ScriptConfigurationState? { - if (file == null) return null - return cache[file] - } - - fun getAppliedConfiguration(file: VirtualFile?): ScriptConfigurationSnapshot? = - getCachedConfigurationState(file)?.applied - - override fun hasConfiguration(file: KtFile): Boolean { - return getAppliedConfiguration(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 = getAppliedConfiguration(virtualFile) - if (cached != null) return cached.configuration - - val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null - rootsIndexer.transaction { - reloadOutOfDateConfiguration(ktFile, isFirstLoad = true) - } - - return getAppliedConfiguration(virtualFile)?.configuration - } - - override fun forceReloadConfiguration( - file: VirtualFile, - loader: ScriptConfigurationLoader - ): ScriptCompilationConfigurationWrapper? { - val ktFile = project.getKtFile(file, null) ?: return null - - rootsIndexer.transaction { - forceReloadConfiguration(ktFile, loader) - } - - return getAppliedConfiguration(file)?.configuration - } - - override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater { - override fun ensureUpToDatedConfigurationSuggested(file: KtFile) { - reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false) - } - - override fun ensureConfigurationUpToDate(files: List): Boolean { - return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false) - } - - override fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope) { - cache.markOutOfDate(scope) - } - - override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) { - reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true) - } - } - - private fun reloadIfOutOfDate(files: List, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: 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.isUpToDate(project, virtualFile, file)) { - upToDate = false - reloadOutOfDateConfiguration( - file, - isFirstLoad = state == null, - loadEvenWillNotBeApplied = loadEvenWillNotBeApplied, - isPostponedLoad = isPostponedLoad - ) - } - } - } - } - - 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]?.isUpToDate(project, virtualFile, file) == true) return - - rootsIndexer.transaction { - reloadOutOfDateConfiguration(file, forceSync = true, loadEvenWillNotBeApplied = true) - } - } - - protected open fun setAppliedConfiguration( - file: VirtualFile, - newConfigurationSnapshot: ScriptConfigurationSnapshot? - ) { - rootsIndexer.checkInTransaction() - val newConfiguration = newConfigurationSnapshot?.configuration - debug(file) { "configuration changed = $newConfiguration" } - - if (newConfiguration != null) { - if (hasNotCachedRoots(newConfiguration)) { - rootsIndexer.markNewRoot(file, newConfiguration) - } - - cache.setApplied(file, newConfigurationSnapshot) - - clearClassRootsCaches() - } - - updateHighlighting(listOf(file)) - } - - protected fun setLoadedConfiguration( - file: VirtualFile, - configurationSnapshot: ScriptConfigurationSnapshot - ) { - cache.setLoaded(file, configurationSnapshot) - } - - private fun hasNotCachedRoots(configuration: ScriptCompilationConfigurationWrapper): Boolean { - return classpathRoots.hasNotCachedRoots(configuration) - } - - init { - val connection = project.messageBus.connect(project) - connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { - override fun rootsChanged(event: ModuleRootEvent) { - clearClassRootsCaches() - } - }) - } - - /** - * Clear configuration caches - * Start re-highlighting for opened scripts - */ - override fun clearConfigurationCachesAndRehighlight() { - ScriptDependenciesModificationTracker.getInstance(project).incModificationCount() - - // todo: cache.clear() - clearClassRootsCaches() - - if (project.isOpen) { - val openedScripts = FileEditorManager.getInstance(project).openFiles.filterNot { it.isNonScript() } - updateHighlighting(openedScripts) - } - } - - @TestOnly - internal fun clearCaches() { - cache.clear() - } - - 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 + abstract val updater: ScriptConfigurationUpdater private val classpathRootsLock = ReentrantLock() @Volatile private var _classpathRoots: ScriptClassRootsCache? = null - private val classpathRoots: ScriptClassRootsCache + val classpathRoots: ScriptClassRootsCache get() { val value1 = _classpathRoots if (value1 != null) return value1 @@ -301,13 +53,16 @@ internal abstract class AbstractScriptConfigurationManager( val value2 = _classpathRoots if (value2 != null) return value2 - val value3 = ScriptClassRootsCache(project, cache.allApplied()) + val value3 = recreateRootsCache() + value3.saveClassRootsToStorage() _classpathRoots = value3 return value3 } } - private fun clearClassRootsCaches() { + protected abstract fun recreateRootsCache(): ScriptClassRootsCache + + fun clearClassRootsCaches(project: Project) { debug { "class roots caches cleared" } classpathRootsLock.withLock { @@ -324,31 +79,133 @@ internal abstract class AbstractScriptConfigurationManager( ScriptDependenciesModificationTracker.getInstance(project).incModificationCount() } + companion object { + val SCRIPTING_SUPPORT: ExtensionPointName = + ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.scriptingSupport") + } +} + +class CompositeManager(val project: Project) : ScriptConfigurationManager { + @Suppress("unused") + private val notifier = ScriptChangesNotifier(project, updater) + + // todo public for tests + val managers = ScriptingSupport.SCRIPTING_SUPPORT.getPoint(project).extensionList + + private fun getRelatedManager(file: VirtualFile): ScriptingSupport = managers.first { it.isRelated(file) } + private fun getRelatedManager(file: KtFile): ScriptingSupport = + getRelatedManager(file.originalFile.virtualFile) + + private fun getOrLoadConfiguration( + virtualFile: VirtualFile, + preloadedKtFile: KtFile? = null + ): ScriptCompilationConfigurationWrapper? = + getRelatedManager(virtualFile).getOrLoadConfiguration(virtualFile, preloadedKtFile) + + override fun getConfiguration(file: KtFile) = getOrLoadConfiguration(file.originalFile.virtualFile, file) + + override fun hasConfiguration(file: KtFile): Boolean = + getRelatedManager(file).hasCachedConfiguration(file) + + override val updater: ScriptConfigurationUpdater + get() = object : ScriptConfigurationUpdater { + override fun ensureUpToDatedConfigurationSuggested(file: KtFile) = + getRelatedManager(file).updater.ensureUpToDatedConfigurationSuggested(file) + + override fun ensureConfigurationUpToDate(files: List): Boolean = + files.groupBy { getRelatedManager(it) }.all { (manager, files) -> + manager.updater.ensureConfigurationUpToDate(files) + } + + override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) = + getRelatedManager(file).updater.suggestToUpdateConfigurationIfOutOfDate(file) + } + + init { + val connection = project.messageBus.connect(project) + connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { + override fun rootsChanged(event: ModuleRootEvent) { + managers.forEach { + it.clearClassRootsCaches(project) + } + } + }) + } + /** * Returns script classpath roots * Loads script configuration if classpath roots don't contain [file] yet */ private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache { + val classpathRoots = getRelatedManager(file).classpathRoots if (classpathRoots.contains(file)) { return classpathRoots } - getConfiguration(file) - return classpathRoots + getOrLoadConfiguration(file) + + return getRelatedManager(file).classpathRoots } - override fun getScriptSdk(file: VirtualFile): Sdk? = getActualClasspathRoots(file).getScriptSdk(file) + override fun getScriptSdk(file: VirtualFile): Sdk? = + getActualClasspathRoots(file).getScriptSdk(file) - override fun getFirstScriptsSdk(): Sdk? = classpathRoots.firstScriptSdk + override fun getFirstScriptsSdk(): Sdk? { + managers.forEach { + it.classpathRoots.firstScriptSdk?.let { sdk -> return sdk } + } + + return null + } override fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope = getActualClasspathRoots(file).getScriptDependenciesClassFilesScope(file) - override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope = classpathRoots.allDependenciesClassFilesScope + override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope = + GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesClassFilesScope }) - override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope = classpathRoots.allDependenciesSourcesScope + override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope = + GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesSourcesScope }) - override fun getAllScriptsDependenciesClassFiles(): List = classpathRoots.allDependenciesClassFiles + override fun getAllScriptsDependenciesClassFiles(): List = + managers.flatMap { it.classpathRoots.allDependenciesClassFiles } + + override fun getAllScriptDependenciesSources(): List = + managers.flatMap { it.classpathRoots.allDependenciesSources } + + override fun forceReloadConfiguration(file: VirtualFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper? { + val ktFile = project.getKtFile(file, null) ?: return null + + return managers.firstIsInstanceOrNull()?.forceReloadConfiguration(ktFile, loader) + } + + /////////////////// + // Adapters for deprecated API + // + + @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 = + ScriptConfigurationManager.toVfsRoots(getConfiguration(file)?.dependenciesClassPath.orEmpty()) + + private fun clearCaches() { + managers.forEach { + it.clearCaches() + } + } + + override fun clearConfigurationCachesAndRehighlight() { + ScriptDependenciesModificationTracker.getInstance(project).incModificationCount() + + clearCaches() + + ScriptingSupportHelper.updateHighlighting(project) { + !it.isNonScript() + } + } +} - 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 0a514ec618f..f80cab65c44 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 @@ -1,5 +1,5 @@ /* - * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2020 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. */ @@ -11,95 +11,36 @@ import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager 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.configuration.DefaultScriptConfigurationManagerExtensions.LOADER -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.cache.ScriptConfigurationSnapshot -import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier +import org.jetbrains.kotlin.idea.core.script.configuration.cache.* import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader 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.loader.ScriptOutsiderFileConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.utils.* -import org.jetbrains.kotlin.idea.core.script.configuration.utils.DefaultBackgroundExecutor import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition +import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock -import kotlin.script.experimental.api.* +import kotlin.script.experimental.api.ScriptDiagnostic -/** - * 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) { - - internal val backgroundExecutor: BackgroundExecutor = +class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(project) { + // TODO public for tests + val backgroundExecutor: BackgroundExecutor = if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer) else DefaultBackgroundExecutor(project, rootsIndexer) @@ -114,8 +55,6 @@ internal class DefaultScriptConfigurationManager(project: Project) : yield(defaultLoader) } - private val notifier = ScriptChangesNotifier(project, updater) - private val saveLock = ReentrantLock() override fun createCache() = object : ScriptConfigurationMemoryCache(project) { @@ -189,10 +128,8 @@ internal class DefaultScriptConfigurationManager(project: Project) : loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) } } else { if (postponeLoading) { - ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { - it.showNotification(virtualFile, project) { - runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true) - } + LoadScriptConfigurationNotificationFactory.showNotification(virtualFile, project) { + runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true) } } else { runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = false) @@ -224,11 +161,11 @@ internal class DefaultScriptConfigurationManager(project: Project) : } } - override fun forceReloadConfiguration(file: KtFile, loader: ScriptConfigurationLoader) { - val virtualFile = file.originalFile.virtualFile ?: return + fun forceReloadConfiguration(file: KtFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper? { + val virtualFile = file.originalFile.virtualFile ?: return null - if (!ScriptDefinitionsManager.getInstance(project).isReady()) return - val scriptDefinition = file.findScriptDefinition() ?: return + if (!ScriptDefinitionsManager.getInstance(project).isReady()) return null + val scriptDefinition = file.findScriptDefinition() ?: return null if (!loader.shouldRunInBackground(scriptDefinition)) { loader.loadDependencies(false, file, scriptDefinition, loadingContext) @@ -237,24 +174,13 @@ internal class DefaultScriptConfigurationManager(project: Project) : loader.loadDependencies(false, file, 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>) { - rootsIndexer.transaction { - for ((file, result) in files) { - loadingContext.saveNewConfiguration(file, result) - } - } + return getAppliedConfiguration(virtualFile)?.configuration } private val loadingContext = object : ScriptConfigurationLoadingContext { override fun getCachedConfiguration(file: VirtualFile): ScriptConfigurationSnapshot? = - this@DefaultScriptConfigurationManager.getAppliedConfiguration(file) + getAppliedConfiguration(file) override fun suggestNewConfiguration(file: VirtualFile, newResult: ScriptConfigurationSnapshot) { suggestOrSaveConfiguration(file, newResult, false) @@ -275,9 +201,7 @@ internal class DefaultScriptConfigurationManager(project: Project) : setLoadedConfiguration(file, newResult) - ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { - it.hideNotification(file, project) - } + LoadScriptConfigurationNotificationFactory.hideNotification(file, project) val newConfiguration = newResult.configuration if (newConfiguration == null) { @@ -349,10 +273,169 @@ internal class DefaultScriptConfigurationManager(project: Project) : } } +abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupport() { + 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 [setAppliedConfiguration] + * 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 + * @param isPostponedLoad is used to postspone loading: show a notification for out of date script and start loading when user request + */ + protected abstract fun reloadOutOfDateConfiguration( + file: KtFile, + isFirstLoad: Boolean = getAppliedConfiguration(file.originalFile.virtualFile) == null, + loadEvenWillNotBeApplied: Boolean = false, + forceSync: Boolean = false, + isPostponedLoad: Boolean = false + ) + + fun getCachedConfigurationState(file: VirtualFile?): ScriptConfigurationState? { + if (file == null) return null + return cache[file] + } + + fun getAppliedConfiguration(file: VirtualFile?): ScriptConfigurationSnapshot? = + getCachedConfigurationState(file)?.applied + + override fun isRelated(file: VirtualFile): Boolean = true + + override fun hasCachedConfiguration(file: KtFile): Boolean = + getAppliedConfiguration(file.originalFile.virtualFile) != null + + override fun getOrLoadConfiguration( + virtualFile: VirtualFile, + preloadedKtFile: KtFile? + ): ScriptCompilationConfigurationWrapper? { + val cached = getAppliedConfiguration(virtualFile) + if (cached != null) return cached.configuration + + val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null + rootsIndexer.transaction { + reloadOutOfDateConfiguration(ktFile, isFirstLoad = true) + } + + return getAppliedConfiguration(virtualFile)?.configuration + } + + override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater { + override fun ensureUpToDatedConfigurationSuggested(file: KtFile) { + reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false) + } + + override fun ensureConfigurationUpToDate(files: List): Boolean { + return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false) + } + + override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) { + reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true) + } + } + + private fun reloadIfOutOfDate(files: List, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: 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.isUpToDate(project, virtualFile, file)) { + upToDate = false + reloadOutOfDateConfiguration( + file, + isFirstLoad = state == null, + loadEvenWillNotBeApplied = loadEvenWillNotBeApplied, + isPostponedLoad = isPostponedLoad + ) + } + } + } + } + + return upToDate + } + + protected open fun setAppliedConfiguration( + file: VirtualFile, + newConfigurationSnapshot: ScriptConfigurationSnapshot? + ) { + rootsIndexer.checkInTransaction() + val newConfiguration = newConfigurationSnapshot?.configuration + debug(file) { "configuration changed = $newConfiguration" } + + if (newConfiguration != null) { + if (hasNotCachedRoots(newConfiguration)) { + debug(file) { "new class roots found: $newConfiguration" } + rootsIndexer.markNewRoot() + } + + cache.setApplied(file, newConfigurationSnapshot) + + clearClassRootsCaches(project) + } + + ScriptingSupportHelper.updateHighlighting(project) { + it == file + } + } + + protected fun setLoadedConfiguration( + file: VirtualFile, + configurationSnapshot: ScriptConfigurationSnapshot + ) { + cache.setLoaded(file, configurationSnapshot) + } + + private fun hasNotCachedRoots(configuration: ScriptCompilationConfigurationWrapper): Boolean { + return classpathRoots.hasNotCachedRoots(DefaultClassRootsCache.extractRoots(project, configuration)) + } + + @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]?.isUpToDate(project, virtualFile, file) == true) return + + rootsIndexer.transaction { + reloadOutOfDateConfiguration(file, forceSync = true, loadEvenWillNotBeApplied = true) + } + } + + override fun clearCaches() { + cache.clear() + } + + override fun recreateRootsCache(): ScriptClassRootsCache { + return DefaultClassRootsCache( + project, + cache.allApplied() + ) + } +} + object DefaultScriptConfigurationManagerExtensions { val LOADER: ExtensionPointName = ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader") } val ScriptConfigurationManager.testingBackgroundExecutor - get() = (this as DefaultScriptConfigurationManager).backgroundExecutor as TestingBackgroundExecutor + get() = (this as CompositeManager).managers + .firstIsInstance() + .backgroundExecutor as TestingBackgroundExecutor \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/ScriptingSupportHelper.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/ScriptingSupportHelper.kt new file mode 100644 index 00000000000..35e1375c9ff --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/ScriptingSupportHelper.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2010-2020 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.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiManager +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.jetbrains.kotlin.idea.core.util.EDT + +object ScriptingSupportHelper { + fun updateHighlighting(project: Project, filter: (VirtualFile) -> Boolean) { + if (!project.isOpen) return + + val openFiles = FileEditorManager.getInstance(project).openFiles + val openedScripts = openFiles.filter { filter(it) } + + if (openedScripts.isEmpty()) return + + GlobalScope.launch(EDT(project)) { + if (project.isDisposed) return@launch + + openedScripts.forEach { + PsiManager.getInstance(project).findFile(it)?.let { psiFile -> + DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + } + } + } + } +} \ 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 index 02dfc17b0a3..9a5877d5e5b 100644 --- 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 @@ -33,6 +33,8 @@ interface ScriptConfigurationCache { fun allApplied(): Map fun clear() + + fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper? } sealed class ScriptConfigurationCacheScope { 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 34720ae9e15..56ce446feb3 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 @@ -24,6 +24,9 @@ open class ScriptConfigurationMemoryCache( return memoryCache.get(file) } + override fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper? = + memoryCache.entrySet().firstOrNull()?.value?.loaded?.configuration + @Synchronized override fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot) { val old = memoryCache[file] ?: ScriptConfigurationState() 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 index bb5fa4d1781..4834c161a50 100644 --- 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 @@ -9,7 +9,6 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiManager -import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.psi.KtFile diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultClassRootsCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultClassRootsCache.kt new file mode 100644 index 00000000000..69cd74b1d45 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultClassRootsCache.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2020 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.projectRoots.Sdk +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.containers.ConcurrentFactoryMap +import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager +import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper +import java.io.File + +internal class DefaultClassRootsCache( + project: Project, + private val all: Map +) : ScriptClassRootsCache(project, extractRoots(all, project)) { + + override val fileToConfiguration: (VirtualFile) -> ScriptCompilationConfigurationWrapper? + get() = { all[it] } + + override fun contains(file: VirtualFile): Boolean = file in all + + override val rootsCacheKey = ScriptClassRootsStorage.Companion.Key("default") + + private val scriptsSdksCache: Map = + ConcurrentFactoryMap.createWeakMap { file -> + return@createWeakMap getScriptSdk( + all[file]?.javaHome + ) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) + } + + override fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file] + + override val firstScriptSdk: Sdk? by lazy { + val firstCachedScript = all.keys.firstOrNull() ?: return@lazy null + return@lazy getScriptSdk(firstCachedScript) + } + + companion object { + fun extractRoots( + project: Project, + configuration: ScriptCompilationConfigurationWrapper + ): ScriptClassRootsStorage.Companion.ScriptClassRoots { + val scriptSdk = + getScriptSdkOfDefault( + configuration.javaHome, + project + ) + if (scriptSdk != null && !scriptSdk.isAlreadyIndexed(project)) { + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + toStringValues( + configuration.dependenciesClassPath + ), + toStringValues( + configuration.dependenciesSources + ), + setOf(scriptSdk) + ) + } + + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + toStringValues( + configuration.dependenciesClassPath + ), + toStringValues( + configuration.dependenciesSources + ), + emptySet() + ) + } + + fun extractRoots( + all: Map, + project: Project + ): ScriptClassRootsStorage.Companion.ScriptClassRoots { + val classpath = mutableSetOf() + val sources = mutableSetOf() + val sdks = mutableSetOf() + + for ((_, configuration) in all) { + val scriptSdk = + getScriptSdk(configuration.javaHome) + if (scriptSdk != null && !scriptSdk.isAlreadyIndexed(project)) { + sdks.add(scriptSdk) + } + + classpath.addAll(configuration.dependenciesClassPath) + sources.addAll(configuration.dependenciesSources) + } + + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + toStringValues(classpath), + toStringValues(sources), + sdks + ) + } + } +} \ No newline at end of file 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 index 88df523b0d2..a97796d5077 100644 --- 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 @@ -10,53 +10,41 @@ 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.openapi.vfs.* 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.util.getProjectJdkTableSafe import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import java.io.File +import java.nio.file.FileSystems -internal class ScriptClassRootsCache( +abstract class ScriptClassRootsCache( private val project: Project, - private val all: Map + private val roots: ScriptClassRootsStorage.Companion.ScriptClassRoots ) { - private val scriptsSdksCache: Map = - ConcurrentFactoryMap.createWeakMap { file -> - return@createWeakMap getScriptSdk(all[file]) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) - } + protected abstract val fileToConfiguration: (VirtualFile) -> ScriptCompilationConfigurationWrapper? - fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file] + protected abstract val rootsCacheKey: ScriptClassRootsStorage.Companion.Key - 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 + abstract val firstScriptSdk: Sdk? - return getAllProjectSdks().find { it.homeDirectory == javaHome } - } + abstract fun getScriptSdk(file: VirtualFile): Sdk? - val firstScriptSdk: Sdk? by lazy { - val firstCachedScript = all.keys.firstOrNull() ?: return@lazy null - return@lazy getScriptSdk(firstCachedScript) - } + abstract fun contains(file: VirtualFile): Boolean - private fun Sdk.isAlreadyIndexed(): Boolean { - return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this } - } + class Fat( + val scriptConfiguration: ScriptCompilationConfigurationWrapper, + val classFilesScope: GlobalSearchScope + ) val allDependenciesClassFiles by lazy { - ScriptClassRootsStorage.getInstance(project).loadClasspathRoots() + ScriptClassRootsStorage.getInstance(project).loadClasspathRoots(rootsCacheKey) } val allDependenciesSources by lazy { - ScriptClassRootsStorage.getInstance(project).loadSourcesRoots() + ScriptClassRootsStorage.getInstance(project).loadSourcesRoots(rootsCacheKey) } val allDependenciesClassFilesScope by lazy { @@ -67,85 +55,71 @@ internal class ScriptClassRootsCache( NonClasspathDirectoriesScope.compose(allDependenciesSources) } - private val scriptsDependenciesClasspathScopeCache: MutableMap = + private val scriptsDependenciesCache: MutableMap = ConcurrentFactoryMap.createWeakMap { file -> - val compilationConfiguration = all[file] - ?: return@createWeakMap GlobalSearchScope.EMPTY_SCOPE + val configuration = fileToConfiguration(file) ?: return@createWeakMap null - val roots = compilationConfiguration.dependenciesClassPath - val sdk = scriptsSdksCache[file] + val roots = configuration.dependenciesClassPath + val sdk = getScriptSdk(file) @Suppress("FoldInitializerAndIfToElvis") if (sdk == null) { - return@createWeakMap NonClasspathDirectoriesScope.compose( - ScriptConfigurationManager.toVfsRoots( - roots - ) + return@createWeakMap Fat( + configuration, + NonClasspathDirectoriesScope.compose(ScriptConfigurationManager.toVfsRoots(roots)) ) } - return@createWeakMap NonClasspathDirectoriesScope.compose( - sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots( - roots + return@createWeakMap Fat( + configuration, + NonClasspathDirectoriesScope.compose( + sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots(roots) ) ) } fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope { - return scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE + return scriptsDependenciesCache[file]?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE } - fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean { - return !ScriptClassRootsStorage.getInstance(project).containsAll(extractRoots(compilationConfiguration)) + fun getScriptConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? { + return scriptsDependenciesCache[file]?.scriptConfiguration } - private fun extractRoots(configuration: ScriptCompilationConfigurationWrapper): ScriptClassRootsStorage.Companion.ScriptClassRoots { - val scriptSdk = getScriptSdk(configuration) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) - if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) { - return ScriptClassRootsStorage.Companion.ScriptClassRoots( - configuration.dependenciesClassPath, - configuration.dependenciesSources, - listOf(scriptSdk) - ) - } - - return ScriptClassRootsStorage.Companion.ScriptClassRoots( - configuration.dependenciesClassPath, - configuration.dependenciesSources, - emptyList() - ) + fun hasNotCachedRoots(roots: ScriptClassRootsStorage.Companion.ScriptClassRoots): Boolean { + return !ScriptClassRootsStorage.getInstance(project).containsAll(rootsCacheKey, roots) } - init { - saveClassRootsToStorage() - } - - private fun saveClassRootsToStorage() { - if (all.isEmpty()) return - - val classpath = mutableSetOf() - val sources = mutableSetOf() - val sdks = mutableSetOf() - - for ((file, configuration) in all) { - val scriptSdk = getScriptSdk(file) - if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) { - sdks.add(scriptSdk) - } - - classpath.addAll(configuration.dependenciesClassPath) - sources.addAll(configuration.dependenciesSources) - } - + fun saveClassRootsToStorage() { val rootsStorage = ScriptClassRootsStorage.getInstance(project) - rootsStorage.save( - ScriptClassRootsStorage.Companion.ScriptClassRoots( - classpath.toList(), - sources.toList(), - sdks.toList() - ) - ) + if (roots.classpathFiles.isNotEmpty() || roots.sourcesFiles.isNotEmpty() || roots.sdks.isNotEmpty()) { + rootsStorage.save(rootsCacheKey, roots) + } } - fun contains(file: VirtualFile) = all.containsKey(file) -} \ No newline at end of file + companion object { + fun toStringValues(prop: Collection): Set { + return prop.mapNotNull { it.absolutePath }.toSet() + } + + fun getScriptSdkOfDefault(javaHomeStr: File?, project: Project): Sdk? { + return getScriptSdk(javaHomeStr) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) + } + + fun getScriptSdk(javaHomeStr: File?): Sdk? { + // workaround for mismatched gradle wrapper and plugin version + val javaHome = try { + javaHomeStr?.let { VfsUtil.findFileByIoFile(it, true) } + } catch (e: Throwable) { + null + } ?: return null + + return getProjectJdkTableSafe().allJdks.find { it.homeDirectory == javaHome } + } + + fun Sdk.isAlreadyIndexed(project: Project): Boolean { + return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this } + } + } +} + 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 1c2547fb474..efe5be39fc6 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 @@ -20,13 +20,12 @@ import java.util.concurrent.atomic.AtomicInteger /** * Utility for postponing indexing of new roots to the end of some bulk operation. */ -internal class ScriptClassRootsIndexer(val project: Project) { +class ScriptClassRootsIndexer(val project: Project) { private var newRootsPresent: Boolean = false private val concurrentTransactions = AtomicInteger() @Synchronized - fun markNewRoot(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper) { - debug(file) { "new class roots found: $configuration" } + fun markNewRoot() { checkInTransaction() newRootsPresent = true } diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt index 7a88ffb57b2..4f817171ed4 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt @@ -10,57 +10,84 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.vfs.JarFileSystem import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.io.URLUtil -import com.intellij.util.xmlb.XmlSerializerUtil +import org.jdom.Element import org.jetbrains.kotlin.idea.core.script.debug -import java.io.File @State( name = "ScriptClassRootsStorage", storages = [Storage(StoragePathMacros.CACHE_FILE)] ) -class ScriptClassRootsStorage : PersistentStateComponent { - private var classpath: Set = hashSetOf() - private var sources: Set = hashSetOf() - private var sdks: Set = hashSetOf() +class ScriptClassRootsStorage : PersistentStateComponent { - override fun getState(): ScriptClassRootsStorage? { - return this + private var classpath: MutableMap> = hashMapOf() + private var sources: MutableMap> = hashMapOf() + private var sdks: MutableMap> = hashMapOf() + + override fun getState(): Element { + val root = Element("ScriptClassRootsStorage") + + storeCollection(root, classpath, "classpath") + storeCollection(root, sources, "sources") + storeCollection(root, sdks, "sdk") + + return root } - override fun loadState(state: ScriptClassRootsStorage) { - XmlSerializerUtil.copyBean(state, this) + private fun storeCollection(root: Element, col: Map>, name: String) { + for ((key, paths) in col) { + for (path in paths) { + val element = Element(name) + element.setAttribute("path", path) + element.setAttribute("key", key) + root.addContent(element) + } + } + } + + override fun loadState(state: Element) { + classpath = readCollection(state, "classpath") + sources = readCollection(state, "sources") + sdks = readCollection(state, "sdks") + } + + private fun readCollection(root: Element, name: String): MutableMap> { + val result: MutableMap> = hashMapOf() + for (it in root.getChildren(name)) { + result.getOrPut(it.getAttributeValue("key")) { + hashSetOf() + }.add(it.getAttributeValue("path")) + } + + @Suppress("UNCHECKED_CAST") + return result as MutableMap> } private fun toStringNames(sdks: Collection): Set { return sdks.map { it.name }.toSet() } - private fun toStringValues(prop: Collection): Set { - return prop.mapNotNull { - when { - it.isDirectory -> it.absolutePath - it.isFile -> it.absolutePath + URLUtil.JAR_SEPARATOR - else -> null - } - }.toSet() - } + private fun toVirtualFiles(prop: Set?, sources: Boolean): List { + if (prop == null) return emptyList() - private fun toVirtualFiles(prop: Set, sources: Boolean): List { val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) } .flatMap { it.rootProvider.getFiles(rootType).toList() } } - private fun toVirtualFiles(prop: Collection): List { + private fun toVirtualFiles(prop: Collection?): List { + if (prop == null) return emptyList() + return prop.mapNotNull { - StandardFileSystems.local()?.findFileByPath(it)?.let { - return@mapNotNull it + if (it.endsWith(JarFileSystem.PROTOCOL)) { + StandardFileSystems.jar()?.findFileByPath(it + JarFileSystem.JAR_SEPARATOR)?.let { + return@mapNotNull it + } } - StandardFileSystems.jar()?.findFileByPath(it)?.let { + StandardFileSystems.local()?.findFileByPath(it)?.let { return@mapNotNull it } @@ -70,36 +97,40 @@ class ScriptClassRootsStorage : PersistentStateComponent { - return toVirtualFiles(sdks, false) + toVirtualFiles(classpath) + fun loadClasspathRoots(key: Key): List { + return toVirtualFiles(sdks[key.value], false) + toVirtualFiles(classpath[key.value]) } - fun loadSourcesRoots(): List { - return toVirtualFiles(sdks, true) + toVirtualFiles(sources) + fun loadSourcesRoots(key: Key): List { + return toVirtualFiles(sdks[key.value], true) + toVirtualFiles(sources[key.value]) } companion object { @@ -107,9 +138,13 @@ class ScriptClassRootsStorage : PersistentStateComponent, - val sourcesFiles: List, - val sdks: List + val classpathFiles: Set, + val sourcesFiles: Set, + val sdks: Set ) + + val EMPTY = ScriptClassRoots(emptySet(), emptySet(), emptySet()) + + data class Key(val value: String) } } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleClassRootsCache.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleClassRootsCache.kt new file mode 100644 index 00000000000..4b6ac6491a6 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleClassRootsCache.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2010-2020 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.scripting.gradle + +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache +import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage +import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage.Companion.ScriptClassRoots +import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper + +internal class GradleClassRootsCache( + project: Project, + configuration: Configuration?, + override val fileToConfiguration: (VirtualFile) -> ScriptCompilationConfigurationWrapper? +) : ScriptClassRootsCache(project, extractRoots(configuration)) { + + override val rootsCacheKey = ScriptClassRootsStorage.Companion.Key("gradle") + + override fun getScriptSdk(file: VirtualFile): Sdk? { + return firstScriptSdk + } + + override val firstScriptSdk: Sdk? = configuration?.let { getScriptSdk(configuration.context.javaHome) } + + // TODO what should we do if no configuration is loaded yet + override fun contains(file: VirtualFile): Boolean = true + + companion object { + fun extractRoots(configuration: Configuration?): ScriptClassRoots { + if (configuration == null) { + return ScriptClassRootsStorage.EMPTY + } + val scriptSdk = getScriptSdk(configuration.context.javaHome) + if (scriptSdk != null && !scriptSdk.isAlreadyIndexed(configuration.context.project)) { + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + configuration.classFilePath, + configuration.sourcePath, + setOf(scriptSdk) + ) + } + return ScriptClassRoots( + configuration.classFilePath, + configuration.sourcePath, + getScriptSdk(configuration.context.javaHome)?.let { setOf(it) } ?: setOf() + ) + } + } +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt index 3e8057d5280..a3579bccdc5 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt @@ -34,15 +34,14 @@ fun runPartialGradleImport(project: Project) { ) } -private val kotlinDslNotificationGroupId = "Gradle Kotlin DSL Scripts" +private const val kotlinDslNotificationGroupId = "Gradle Kotlin DSL Scripts" private var Project.notificationPanel: ScriptConfigurationChangedNotification? by UserDataProperty(Key.create("load.script.configuration.panel")) -fun showNotificationForProjectImport(project: Project, callback: () -> Unit) { +fun showNotificationForProjectImport(project: Project) { val existingPanel = project.notificationPanel if (existingPanel != null) { - existingPanel.update(callback) return } @@ -53,7 +52,7 @@ fun showNotificationForProjectImport(project: Project, callback: () -> Unit) { ) } - val notification = ScriptConfigurationChangedNotification(project, callback) + val notification = ScriptConfigurationChangedNotification(project) project.notificationPanel = notification notification.notify(project) } @@ -64,7 +63,7 @@ fun hideNotificationForProjectImport(project: Project): Boolean { return true } -private class ScriptConfigurationChangedNotification(val project: Project, onClick: () -> Unit) : +private class ScriptConfigurationChangedNotification(val project: Project) : Notification( kotlinDslNotificationGroupId, KotlinIcons.LOAD_SCRIPT_CONFIGURATION, @@ -76,9 +75,7 @@ private class ScriptConfigurationChangedNotification(val project: Project, onCli ) { init { - //NotificationGroup.findRegisteredGroup(s) ?: NotificationGroup.balloonGroup(s) - - addAction(LoadConfigurationAction(onClick)) + addAction(LoadConfigurationAction()) addAction(NotificationAction.createSimple(KotlinIdeaGradleBundle.message("action.label.enable.auto.import")) { val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) val projectSettings = gradleSettings.getLinkedProjectsSettings() @@ -87,22 +84,20 @@ private class ScriptConfigurationChangedNotification(val project: Project, onCli if (projectSettings != null) { projectSettings.isUseAutoImport = true } + runPartialGradleImport(project) }) } - fun update(onClick: () -> Unit) { - actions.filterIsInstance().forEach { it.onClick = onClick } - } - override fun expire() { super.expire() project.notificationPanel = null } - private class LoadConfigurationAction(var onClick: () -> Unit) : AnAction(KotlinIdeaGradleBundle.message("action.label.import.project")) { + private class LoadConfigurationAction : AnAction(KotlinIdeaGradleBundle.message("action.label.import.project")) { override fun actionPerformed(e: AnActionEvent) { - onClick() + val project = e.project ?: return + runPartialGradleImport(project) } } } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt.201 b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt.201 index a51bdfe7140..97b7984ad2a 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt.201 +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleImportHelper.kt.201 @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.idea.scripting.gradle import com.intellij.diff.util.DiffUtil -import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys @@ -46,17 +45,16 @@ fun runPartialGradleImport(project: Project) { ) } -private var Project.notificationPanel: Boolean? - by UserDataProperty(Key.create("load.script.configuration.panel")) +private var Project.shouldShowLoadConfiguraionsAction: Boolean? + by UserDataProperty(Key.create("load.script.configuration.action")) -fun showNotificationForProjectImport(project: Project, callback: () -> Unit) { - project.notificationPanel = true - (ActionManager.getInstance().getAction("LoadConfigurationAction") as LoadConfigurationAction).onClick = callback +fun showNotificationForProjectImport(project: Project) { + project.shouldShowLoadConfiguraionsAction = true } fun hideNotificationForProjectImport(project: Project): Boolean { - project.notificationPanel = false + project.shouldShowLoadConfiguraionsAction = false return true } @@ -65,11 +63,9 @@ class LoadConfigurationAction : AnAction( KotlinIdeaGradleBundle.message("action.description.load.script.configurations"), KotlinIcons.LOAD_SCRIPT_CONFIGURATION ) { - - var onClick = {} - override fun actionPerformed(e: AnActionEvent) { - onClick() + val project = e.project ?: return + runPartialGradleImport(project) } override fun update(e: AnActionEvent) { @@ -79,24 +75,28 @@ class LoadConfigurationAction : AnAction( } private fun ensureValidActionVisibility(e: AnActionEvent) { - if (e.project?.notificationPanel != true) { + if (e.project?.shouldShowLoadConfiguraionsAction != true) { e.presentation.isVisible = false return } val editor = e.getData(CommonDataKeys.EDITOR) ?: return - when { - DiffUtil.isDiffEditor(editor) -> e.presentation.isVisible = false - !editor.isScriptEditor() -> e.presentation.isVisible = false + if (DiffUtil.isDiffEditor(editor)) { + e.presentation.isVisible = false + return } + e.presentation.isVisible = editor.isScriptEditor() } private fun Editor.isScriptEditor(): Boolean { + val project = project ?: return false + val documentManager = FileDocumentManager.getInstance() val virtualFile = documentManager.getFile(document) if (virtualFile is LightVirtualFileBase) return false if (virtualFile == null || !virtualFile.isValid) return false - val project = project ?: return false + + // todo only for gradle script return virtualFile.findScriptDefinition(project) != null } } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt index 9ec3fe09562..811e414f71a 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt @@ -78,12 +78,6 @@ class GradleScriptConfigurationLoader(project: Project) : DefaultScriptConfigura } } - val gradleVersion = getGradleVersion(project) - if (gradleVersion != null && kotlinDslScriptsModelImportSupported(gradleVersion)) { - runPartialGradleImport(project) - return true - } - val result = getConfigurationThroughScriptingApi(ktFile, vFile, scriptDefinition) context.saveNewConfiguration(vFile, result) return true diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationManager.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationManager.kt new file mode 100644 index 00000000000..d7848c271a7 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationManager.kt @@ -0,0 +1,178 @@ +/* + * 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.scripting.gradle + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport +import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupportHelper +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.scripting.gradle.importing.GradleKtsContext +import org.jetbrains.kotlin.idea.scripting.gradle.importing.createGradleKtsContextIfPossible +import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel +import org.jetbrains.kotlin.idea.scripting.gradle.importing.toScriptConfiguration +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition +import org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate +import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper +import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance +import java.nio.file.Paths + + +internal data class ConfigurationData( + val templateClasspath: List, + val models: List +) + +internal class Configuration(val context: GradleKtsContext, data: ConfigurationData) { + val scripts = data.models.associateBy { it.file } + val sourcePath = data.models.flatMapTo(mutableSetOf()) { it.sourcePath } + + val classFilePath: MutableSet = mutableSetOf() + + init { + classFilePath.addAll(data.templateClasspath) + data.models.flatMapTo(classFilePath) { it.classPath } + } + + fun scriptModel(file: VirtualFile): KotlinDslScriptModel? { + return scripts[FileUtil.toSystemDependentName(file.path)] + } +} + +class GradleScriptingSupport(val project: Project) : ScriptingSupport() { + @Volatile + private var configuration: Configuration? = null + + private val rootsIndexer = ScriptClassRootsIndexer(project) + + init { + if (isKotlinDslScriptsModelImportSupported(project)) { + ApplicationManager.getApplication().executeOnPooledThread { + val data = KotlinDslScriptModels.read(project) + val gradleKtsContext = createGradleKtsContextIfPossible(project) + if (data != null && gradleKtsContext != null) { + val newConfiguration = Configuration( + gradleKtsContext, + data + ) + + configuration = newConfiguration + configurationChangedCallback(newConfiguration) + } + } + } + } + + override fun recreateRootsCache(): ScriptClassRootsCache { + return GradleClassRootsCache(project, configuration) { + configuration?.let { conf -> + val model = conf.scriptModel(it) + model?.toScriptConfiguration(conf.context) + } + } + } + + fun replace(context: GradleKtsContext, models: List) { + if (models.isEmpty()) return + + val anyScript = VfsUtil.findFile(Paths.get(models.first().file), true)!! + + val definition = anyScript.findScriptDefinition(context.project) ?: return + val templateClasspath = definition.asLegacyOrNull() + ?.templateClasspath?.map { it.path } ?: return + + val data = ConfigurationData(templateClasspath, models) + KotlinDslScriptModels.write(project, data) + + val newConfiguration = Configuration(context, data) + configuration = newConfiguration + + configurationChangedCallback(newConfiguration) + } + + private fun configurationChangedCallback(newConfiguration: Configuration) { + rootsIndexer.transaction { + if (classpathRoots.hasNotCachedRoots(GradleClassRootsCache.extractRoots(newConfiguration))) { + rootsIndexer.markNewRoot() + } + + clearClassRootsCaches(project) + + ScriptingSupportHelper.updateHighlighting(project) { + configuration?.scriptModel(it) != null + } + } + + hideNotificationForProjectImport(project) + } + + fun updateNotification(file: KtFile) { + val vFile = file.originalFile.virtualFile + val scriptModel = configuration?.scriptModel(vFile) ?: return + + if (scriptModel.inputs.isUpToDate(project, vFile)) { + hideNotificationForProjectImport(project) + } else { + showNotificationForProjectImport(project) + } + } + + override fun isRelated(file: VirtualFile): Boolean { + if (isGradleKotlinScript(file)) { + return isKotlinDslScriptsModelImportSupported(project) + } + + return false + } + + private fun isKotlinDslScriptsModelImportSupported(project: Project): Boolean { + val gradleVersion = getGradleVersion(project) + return gradleVersion != null && kotlinDslScriptsModelImportSupported(gradleVersion) + } + + override fun clearCaches() { + // todo should clear up to date + } + + override fun hasCachedConfiguration(file: KtFile): Boolean = + configuration?.scriptModel(file.originalFile.virtualFile) != null + + override fun getOrLoadConfiguration(virtualFile: VirtualFile, preloadedKtFile: KtFile?): ScriptCompilationConfigurationWrapper? { + val configuration = configuration + if (configuration == null) { + // todo: show notification "Import gradle project" + return null + } else { + return classpathRoots.getScriptConfiguration(virtualFile) + } + } + + override val updater: ScriptConfigurationUpdater + get() = object : ScriptConfigurationUpdater { + override fun ensureUpToDatedConfigurationSuggested(file: KtFile) { + // do nothing for gradle scripts + } + + // unused symbol inspection should not initiate loading + override fun ensureConfigurationUpToDate(files: List): Boolean = true + + override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) { + updateNotification(file) + } + } + + companion object { + fun getInstance(project: Project): GradleScriptingSupport { + return SCRIPTING_SUPPORT.getPoint(project).extensionList.firstIsInstance() + } + } +} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationNotificationFactory.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationNotificationFactory.kt deleted file mode 100644 index 12341d68a42..00000000000 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationNotificationFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2010-2020 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.scripting.gradle - -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationNotificationFactory - -class GradleScriptConfigurationNotificationFactory : ScriptConfigurationNotificationFactory { - override fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean { - if (isGradleKotlinScript(file) && isGradleImportCanBeUsed(project)) { - showNotificationForProjectImport(project, onClick) - return true - } - return false - } - - override fun hideNotification(file: VirtualFile, project: Project): Boolean { - return hideNotificationForProjectImport(project) - } - - private fun isGradleImportCanBeUsed(project: Project): Boolean { - val gradleVersion = getGradleVersion(project) - if (gradleVersion != null && kotlinDslScriptsModelImportSupported(gradleVersion)) { - return true - } - return false - } -} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScriptModel.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScriptModel.kt index e6eb72ca21a..e3e84613228 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScriptModel.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScriptModel.kt @@ -5,9 +5,11 @@ package org.jetbrains.kotlin.idea.scripting.gradle.importing +import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs + data class KotlinDslScriptModel( val file: String, - val inputsTimeStamp: Long, + val inputs: GradleKotlinScriptConfigurationInputs, val classPath: List, val sourcePath: List, val imports: List, diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt index bffdedcd50c..127f5cc6197 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt @@ -7,16 +7,14 @@ package org.jetbrains.kotlin.idea.scripting.gradle.importing import com.intellij.openapi.components.service import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil -import com.intellij.openapi.vfs.VirtualFile import org.gradle.tooling.model.kotlin.dsl.EditorReportSeverity import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle -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.cache.ScriptConfigurationSnapshot import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptInputsWatcher +import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupport import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper @@ -24,6 +22,8 @@ import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource import org.jetbrains.kotlin.scripting.resolve.adjustByDefinition import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings +import org.jetbrains.plugins.gradle.settings.GradleProjectSettings +import org.jetbrains.plugins.gradle.util.GradleConstants import java.io.File import kotlin.script.experimental.api.* import kotlin.script.experimental.jvm.JvmDependency @@ -40,8 +40,9 @@ fun processScriptModel( "Couldn't get KotlinDslScriptsModel for $projectName:\n${model.message}\n${model.stackTrace}" ) } else { - val models = model.toListOfScriptModels() - resolverCtx.externalSystemTaskId.findProject()?.kotlinDslModels?.addAll( + val project = resolverCtx.externalSystemTaskId.findProject() ?: return + val models = model.toListOfScriptModels(project) + project.kotlinDslModels.addAll( models ) if (models.containsErrors()) { @@ -58,7 +59,7 @@ private fun Collection.containsErrors(): Boolean { return any { it.messages.any { it.severity == KotlinDslScriptModel.Severity.ERROR } } } -private fun KotlinDslScriptsModel.toListOfScriptModels(): List = +private fun KotlinDslScriptsModel.toListOfScriptModels(project: Project): List = scriptModels.map { (file, model) -> val messages = mutableListOf() @@ -91,10 +92,14 @@ private fun KotlinDslScriptsModel.toListOfScriptModels(): List().firstOrNull() ?: return null + + val gradleExeSettings = ExternalSystemApiUtil.getExecutionSettings( + project, + gradleProjectSettings.externalProjectPath, + GradleConstants.SYSTEM_ID + ) + + val javaHome = gradleExeSettings.javaHome?.let { File(it)} + + return GradleKtsContext(project, javaHome) +} + +class GradleKtsContext(val project: Project, val javaHome: File?) + +fun KotlinDslScriptModel.toScriptConfiguration(context: GradleKtsContext): ScriptCompilationConfigurationWrapper? { + val scriptFile = File(file) + val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!! + + val definition = virtualFile.findScriptDefinition(context.project) ?: return null + + return ScriptCompilationConfigurationWrapper.FromCompilationConfiguration( + VirtualFileScriptSource(virtualFile), + definition.compilationConfiguration.with { + if (context.javaHome != null) { + jvm.jdkHome(context.javaHome) + } + defaultImports(imports) + dependencies(JvmDependency(classPath.map { File(it) })) + ide.dependenciesSources(JvmDependency(sourcePath.map { File(it) })) + }.adjustByDefinition(definition) + ) +} + fun saveScriptModels( project: Project, task: ExternalSystemTaskId, javaHomeStr: String?, models: List ) { - val scriptConfigurations = mutableListOf>() - val errorReporter = KotlinGradleDslErrorReporter(project, task) val javaHome = javaHomeStr?.let { File(it) } + val context = GradleKtsContext(project, javaHome) + models.forEach { model -> - val scriptFile = File(model.file) - val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!! - - val inputs = getGradleScriptInputsStamp( - project, - virtualFile, - givenTimeStamp = model.inputsTimeStamp - ) - - val definition = virtualFile.findScriptDefinition(project) ?: return@forEach - - val configuration = - definition.compilationConfiguration.with { - if (javaHome != null) { - jvm.jdkHome(javaHome) - } - defaultImports(model.imports) - dependencies(JvmDependency(model.classPath.map { - File( - it - ) - })) - ide.dependenciesSources(JvmDependency(model.sourcePath.map { - File( - it - ) - })) - }.adjustByDefinition(definition) - - scriptConfigurations.add( - Pair( - virtualFile, - ScriptConfigurationSnapshot( - inputs - ?: CachedConfigurationInputs.OutOfDate, - listOf(), - ScriptCompilationConfigurationWrapper.FromCompilationConfiguration( - VirtualFileScriptSource(virtualFile), - configuration, - ), - ), - ), - ) - - errorReporter.reportError(scriptFile, model) + errorReporter.reportError(File(model.file), model) } project.service().saveGradleProjectRootsAfterImport( - scriptConfigurations.map { it.first.parent.path }.toSet() + models.map { File(it.file).parent }.toSet() ) - project.service().saveCompilationConfigurationAfterImport(scriptConfigurations) + GradleScriptingSupport.getInstance(project).replace(context, models) project.service().clearState() } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/serizliation.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/serizliation.kt new file mode 100644 index 00000000000..4b24838d229 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/serizliation.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2010-2020 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.scripting.gradle + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.newvfs.FileAttribute +import org.jetbrains.kotlin.idea.core.util.readString +import org.jetbrains.kotlin.idea.core.util.writeString +import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel +import java.io.DataInputStream +import java.io.DataOutputStream + +internal object KotlinDslScriptModels { + private val attribute = FileAttribute("kotlin-dsl-script-models", 2, false) + + fun read(project: Project): ConfigurationData? { + return attribute.readAttribute(project.projectFile ?: return null)?.use { + readKotlinDslScriptModels(it) + } + } + + fun write(project: Project, configuration: ConfigurationData) { + attribute.writeAttribute(project.projectFile ?: return).use { + writeKotlinDslScriptModels(it, configuration) + } + } +} + +internal fun writeKotlinDslScriptModels(output: DataOutputStream, data: ConfigurationData) { + val strings = StringsPool.writer(output) + strings.addStrings(data.templateClasspath) + val scriptModels = data.models + scriptModels.forEach { + strings.addString(it.file) + strings.addStrings(it.classPath) + strings.addStrings(it.sourcePath) + strings.addStrings(it.imports) + } + strings.writeHeader() + strings.writeStringIds(data.templateClasspath) + output.writeList(scriptModels) { + strings.writeStringId(it.file) + output.writeString(it.inputs.sections) + output.writeLong(it.inputs.inputsTS) + strings.writeStringIds(it.classPath) + strings.writeStringIds(it.sourcePath) + strings.writeStringIds(it.imports) + } +} + +internal fun readKotlinDslScriptModels(input: DataInputStream): ConfigurationData { + val strings = StringsPool.reader(input) + + val templateClasspath = strings.readStrings() + + val models = input.readList { + KotlinDslScriptModel( + strings.readString(), + GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong()), + strings.readStrings(), + strings.readStrings(), + strings.readStrings(), + listOf() + ) + } + + return ConfigurationData(templateClasspath, models) +} + +private object StringsPool { + fun writer(output: DataOutputStream) = Writer(output) + + class Writer(val output: DataOutputStream) { + var freeze = false + val ids = mutableMapOf() + + fun getStringId(string: String) = ids.getOrPut(string) { + check(!freeze) + ids.size + } + + fun addString(string: String) { + getStringId(string) + } + + fun addStrings(list: Collection) { + list.forEach { addString(it) } + } + + fun writeHeader() { + freeze = true + + output.writeInt(ids.size) + + // sort for optimal performance and compression + ids.keys.sorted().forEachIndexed { index, s -> + ids[s] = index + output.writeString(s) + } + } + + fun writeStringId(it: String) { + output.writeInt(getStringId(it)) + } + + fun writeStringIds(strings: List) { + output.writeInt(strings.size) + strings.forEach { + writeStringId(it) + } + } + } + + fun reader(input: DataInputStream): Reader { + val strings = input.readList { input.readString() } + return Reader(input, strings) + } + + class Reader(val input: DataInputStream, val strings: List) { + fun getString(id: Int) = strings[id] + + fun readString() = getString(input.readInt()) + + fun readStrings(): List = input.readList { readString() } + } +} + +private inline fun DataOutputStream.writeList(list: Collection, write: (T) -> Unit) { + writeInt(list.size) + list.forEach { write(it) } +} + +private inline fun DataInputStream.readList(read: () -> T): List { + val n = readInt() + val result = ArrayList(n) + repeat(n) { + result.add(read()) + } + return result +} \ No newline at end of file diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/KotlinDslScriptModelsTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/KotlinDslScriptModelsTest.kt new file mode 100644 index 00000000000..89c75a2a001 --- /dev/null +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/KotlinDslScriptModelsTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2010-2020 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.scripting.gradle + +import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel +import org.junit.Test +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import kotlin.test.assertEquals + +class KotlinDslScriptModelsTest { + @Test + fun write() { + val buffer = ByteArrayOutputStream() + + val conf = ConfigurationData( + listOf("a", "b", "c"), + listOf( + KotlinDslScriptModel( + "a", + GradleKotlinScriptConfigurationInputs("b", 1), + listOf("c", "a", "b"), + listOf("b", "c", "a"), + listOf("i", "c", "b"), + listOf() + ), + KotlinDslScriptModel( + "a", + GradleKotlinScriptConfigurationInputs("b", 1), + listOf("c", "a", "b"), + listOf("b", "c", "a"), + listOf("i", "c", "b"), + listOf() + ) + ) + ) + + writeKotlinDslScriptModels(DataOutputStream(buffer), conf) + + val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray()))) + + assertEquals(conf.toString(), restored.toString()) + } +} \ No newline at end of file diff --git a/idea/resources/META-INF/extensions/ide.xml b/idea/resources/META-INF/extensions/ide.xml index a1998d0a649..0a023d83e2e 100644 --- a/idea/resources/META-INF/extensions/ide.xml +++ b/idea/resources/META-INF/extensions/ide.xml @@ -68,10 +68,6 @@ interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener" area="IDEA_PROJECT"/> - - @@ -105,11 +101,6 @@ - - diff --git a/idea/resources/META-INF/gradle.xml b/idea/resources/META-INF/gradle.xml index 7151437ef44..cd8fb13c113 100644 --- a/idea/resources/META-INF/gradle.xml +++ b/idea/resources/META-INF/gradle.xml @@ -15,8 +15,8 @@ - diff --git a/idea/resources/META-INF/gradle.xml.201 b/idea/resources/META-INF/gradle.xml.201 index 7be649b16cc..ba6cabe4ae8 100644 --- a/idea/resources/META-INF/gradle.xml.201 +++ b/idea/resources/META-INF/gradle.xml.201 @@ -15,8 +15,8 @@ - diff --git a/idea/resources/META-INF/plugin-common.xml b/idea/resources/META-INF/plugin-common.xml index 4cc19631437..e3899aec1a0 100644 --- a/idea/resources/META-INF/plugin-common.xml +++ b/idea/resources/META-INF/plugin-common.xml @@ -231,7 +231,7 @@ serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptDependenciesProvider"/> + serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.CompositeManager"/> diff --git a/idea/resources/META-INF/scripting-support.xml b/idea/resources/META-INF/scripting-support.xml index 1be40949391..764f2c73725 100644 --- a/idea/resources/META-INF/scripting-support.xml +++ b/idea/resources/META-INF/scripting-support.xml @@ -1,7 +1,20 @@ + + + + + + + + \ No newline at end of file