Refactoring: extract separate manager to update script configurations of gradle scripts

The main goal is to extract logic that is related to Gradle scripts only to separate place.
Configurations for gradle scripts are updated during import simultaneously, there is no need to manage script configuration update for one file
Note that for Gradle less than 6.0 The DefaultScriptingSupport is used
This commit is contained in:
Natalia Selezneva
2020-04-16 16:18:16 +03:00
parent c6609e9a97
commit 59bdf67aa2
27 changed files with 1126 additions and 667 deletions
@@ -6,7 +6,6 @@
package org.jetbrains.kotlin.idea.core.script package org.jetbrains.kotlin.idea.core.script
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project 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.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.HyperlinkLabel 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.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle
import org.jetbrains.kotlin.psi.UserDataProperty import org.jetbrains.kotlin.psi.UserDataProperty
interface ScriptConfigurationNotificationFactory { object LoadScriptConfigurationNotificationFactory {
fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean {
fun hideNotification(file: VirtualFile, project: Project): Boolean
companion object {
val NOTIFICATION_FACTORY: ExtensionPointName<ScriptConfigurationNotificationFactory> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.notificationFactory")
}
}
class DefaultScriptConfigurationNotificationFactory : ScriptConfigurationNotificationFactory {
override fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean {
file.addLoadConfigurationNotificationPanel(project, onClick) file.addLoadConfigurationNotificationPanel(project, onClick)
return true return true
} }
override fun hideNotification(file: VirtualFile, project: Project): Boolean { fun hideNotification(file: VirtualFile, project: Project): Boolean {
file.removeLoadConfigurationNotificationPanel(project) file.removeLoadConfigurationNotificationPanel(project)
return true return true
} }
@@ -29,8 +29,8 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.io.URLUtil import com.intellij.util.io.URLUtil
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks 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.CompositeManager
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot 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.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.psi.KtFile 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.definitions.ScriptDependenciesProvider
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationResult
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import java.io.File import java.io.File
import kotlin.script.experimental.api.asSuccess import kotlin.script.experimental.api.asSuccess
import kotlin.script.experimental.api.makeFailureResult import kotlin.script.experimental.api.makeFailureResult
@@ -106,13 +107,6 @@ interface ScriptConfigurationManager {
*/ */
fun clearConfigurationCachesAndRehighlight() fun clearConfigurationCachesAndRehighlight()
/**
* Save configurations into cache.
* Start indexing for new class/source roots.
* Re-highlight opened scripts with changed configuration.
*/
fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, ScriptConfigurationSnapshot>>)
/////////////// ///////////////
// classpath roots info: // classpath roots info:
@@ -168,12 +162,16 @@ interface ScriptConfigurationManager {
@TestOnly @TestOnly
fun updateScriptDependenciesSynchronously(file: PsiFile) { fun updateScriptDependenciesSynchronously(file: PsiFile) {
// TODO: review the usages of this method // TODO: review the usages of this method
(getInstance(file.project) as AbstractScriptConfigurationManager).updateScriptDependenciesSynchronously(file) (getInstance(file.project) as CompositeManager).managers
.firstIsInstance<DefaultScriptingSupport>()
.updateScriptDependenciesSynchronously(file)
} }
@TestOnly @TestOnly
fun clearCaches(project: Project) { fun clearCaches(project: Project) {
(getInstance(project) as AbstractScriptConfigurationManager).clearCaches() (getInstance(project) as CompositeManager).managers
.firstIsInstance<DefaultScriptingSupport>()
.clearCaches()
} }
fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) { fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) {
@@ -6,293 +6,45 @@
package org.jetbrains.kotlin.idea.core.script.configuration package org.jetbrains.kotlin.idea.core.script.configuration
import com.intellij.ProjectTopics 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.extensions.Extensions
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootEvent import com.intellij.openapi.roots.ModuleRootEvent
import com.intellij.openapi.roots.ModuleRootListener import com.intellij.openapi.roots.ModuleRootListener
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder import com.intellij.psi.PsiElementFinder
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScope
import kotlinx.coroutines.GlobalScope import org.jetbrains.kotlin.idea.core.script.KotlinScriptDependenciesClassFinder
import kotlinx.coroutines.launch import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.* import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier
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.configuration.listener.ScriptConfigurationUpdater 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.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache 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.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.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.isNonScript import org.jetbrains.kotlin.scripting.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
/** abstract class ScriptingSupport {
* Abstract [ScriptConfigurationManager] implementation based on [cache] and [reloadOutOfDateConfiguration]. abstract fun isRelated(file: VirtualFile): Boolean
* 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)
@Suppress("LeakingThis") abstract fun clearCaches()
protected val cache: ScriptConfigurationCache = createCache() abstract fun hasCachedConfiguration(file: KtFile): Boolean
abstract fun getOrLoadConfiguration(virtualFile: VirtualFile, preloadedKtFile: KtFile? = null): ScriptCompilationConfigurationWrapper?
protected abstract fun createCache(): ScriptConfigurationCache abstract val updater: ScriptConfigurationUpdater
/**
* 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<VirtualFile> {
val ktFile = project.getKtFile(file) ?: return emptyList()
return getScriptClasspath(ktFile)
}
override fun getScriptClasspath(file: KtFile): List<VirtualFile> =
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<KtFile>): 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<KtFile>, 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<VirtualFile>) {
if (files.isEmpty()) return
GlobalScope.launch(EDT(project)) {
if (project.isDisposed) return@launch
val openFiles = FileEditorManager.getInstance(project).openFiles
val openScripts = files.filter { it.isValid && openFiles.contains(it) }
openScripts.forEach {
PsiManager.getInstance(project).findFile(it)?.let { psiFile ->
DaemonCodeAnalyzer.getInstance(project).restart(psiFile)
}
}
}
}
///////////////////
// ScriptRootsCache
private val classpathRootsLock = ReentrantLock() private val classpathRootsLock = ReentrantLock()
@Volatile @Volatile
private var _classpathRoots: ScriptClassRootsCache? = null private var _classpathRoots: ScriptClassRootsCache? = null
private val classpathRoots: ScriptClassRootsCache val classpathRoots: ScriptClassRootsCache
get() { get() {
val value1 = _classpathRoots val value1 = _classpathRoots
if (value1 != null) return value1 if (value1 != null) return value1
@@ -301,13 +53,16 @@ internal abstract class AbstractScriptConfigurationManager(
val value2 = _classpathRoots val value2 = _classpathRoots
if (value2 != null) return value2 if (value2 != null) return value2
val value3 = ScriptClassRootsCache(project, cache.allApplied()) val value3 = recreateRootsCache()
value3.saveClassRootsToStorage()
_classpathRoots = value3 _classpathRoots = value3
return value3 return value3
} }
} }
private fun clearClassRootsCaches() { protected abstract fun recreateRootsCache(): ScriptClassRootsCache
fun clearClassRootsCaches(project: Project) {
debug { "class roots caches cleared" } debug { "class roots caches cleared" }
classpathRootsLock.withLock { classpathRootsLock.withLock {
@@ -324,31 +79,133 @@ internal abstract class AbstractScriptConfigurationManager(
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount() ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
} }
companion object {
val SCRIPTING_SUPPORT: ExtensionPointName<ScriptingSupport> =
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<KtFile>): 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 * Returns script classpath roots
* Loads script configuration if classpath roots don't contain [file] yet * Loads script configuration if classpath roots don't contain [file] yet
*/ */
private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache { private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache {
val classpathRoots = getRelatedManager(file).classpathRoots
if (classpathRoots.contains(file)) { if (classpathRoots.contains(file)) {
return classpathRoots return classpathRoots
} }
getConfiguration(file) getOrLoadConfiguration(file)
return classpathRoots
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 = override fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope =
getActualClasspathRoots(file).getScriptDependenciesClassFilesScope(file) 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<VirtualFile> = classpathRoots.allDependenciesClassFiles override fun getAllScriptsDependenciesClassFiles(): List<VirtualFile> =
managers.flatMap { it.classpathRoots.allDependenciesClassFiles }
override fun getAllScriptDependenciesSources(): List<VirtualFile> =
managers.flatMap { it.classpathRoots.allDependenciesSources }
override fun forceReloadConfiguration(file: VirtualFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper? {
val ktFile = project.getKtFile(file, null) ?: return null
return managers.firstIsInstanceOrNull<DefaultScriptingSupport>()?.forceReloadConfiguration(ktFile, loader)
}
///////////////////
// Adapters for deprecated API
//
@Deprecated("Use getScriptClasspath(KtFile) instead")
override fun getScriptClasspath(file: VirtualFile): List<VirtualFile> {
val ktFile = project.getKtFile(file) ?: return emptyList()
return getScriptClasspath(ktFile)
}
override fun getScriptClasspath(file: KtFile): List<VirtualFile> =
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<VirtualFile> = classpathRoots.allDependenciesSources
}
@@ -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. * 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.extensions.ExtensionPointName
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import com.intellij.ui.EditorNotifications import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.* 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.DefaultScriptConfigurationManagerExtensions.LOADER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache import org.jetbrains.kotlin.idea.core.script.configuration.cache.*
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.listener.ScriptConfigurationUpdater 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.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader 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.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptOutsiderFileConfigurationLoader 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.*
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.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition 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.scripting.resolve.ScriptReportSink
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.script.experimental.api.* import kotlin.script.experimental.api.ScriptDiagnostic
/** class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(project) {
* Standard implementation of scripts configuration loading and caching // TODO public for tests
* (we have plans to extract separate implementation for Gradle scripts). val backgroundExecutor: BackgroundExecutor =
*
* ## 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 =
if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer) if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer)
else DefaultBackgroundExecutor(project, rootsIndexer) else DefaultBackgroundExecutor(project, rootsIndexer)
@@ -114,8 +55,6 @@ internal class DefaultScriptConfigurationManager(project: Project) :
yield(defaultLoader) yield(defaultLoader)
} }
private val notifier = ScriptChangesNotifier(project, updater)
private val saveLock = ReentrantLock() private val saveLock = ReentrantLock()
override fun createCache() = object : ScriptConfigurationMemoryCache(project) { override fun createCache() = object : ScriptConfigurationMemoryCache(project) {
@@ -189,10 +128,8 @@ internal class DefaultScriptConfigurationManager(project: Project) :
loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) } loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) }
} else { } else {
if (postponeLoading) { if (postponeLoading) {
ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { LoadScriptConfigurationNotificationFactory.showNotification(virtualFile, project) {
it.showNotification(virtualFile, project) { runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true)
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true)
}
} }
} else { } else {
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = false) runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = false)
@@ -224,11 +161,11 @@ internal class DefaultScriptConfigurationManager(project: Project) :
} }
} }
override fun forceReloadConfiguration(file: KtFile, loader: ScriptConfigurationLoader) { fun forceReloadConfiguration(file: KtFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper? {
val virtualFile = file.originalFile.virtualFile ?: return val virtualFile = file.originalFile.virtualFile ?: return null
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return if (!ScriptDefinitionsManager.getInstance(project).isReady()) return null
val scriptDefinition = file.findScriptDefinition() ?: return val scriptDefinition = file.findScriptDefinition() ?: return null
if (!loader.shouldRunInBackground(scriptDefinition)) { if (!loader.shouldRunInBackground(scriptDefinition)) {
loader.loadDependencies(false, file, scriptDefinition, loadingContext) loader.loadDependencies(false, file, scriptDefinition, loadingContext)
@@ -237,24 +174,13 @@ internal class DefaultScriptConfigurationManager(project: Project) :
loader.loadDependencies(false, file, scriptDefinition, loadingContext) loader.loadDependencies(false, file, scriptDefinition, loadingContext)
} }
} }
}
/** return getAppliedConfiguration(virtualFile)?.configuration
* Save configurations into cache.
* Start indexing for new class/source roots.
* Re-highlight opened scripts with changed configuration.
*/
override fun saveCompilationConfigurationAfterImport(files: List<Pair<VirtualFile, ScriptConfigurationSnapshot>>) {
rootsIndexer.transaction {
for ((file, result) in files) {
loadingContext.saveNewConfiguration(file, result)
}
}
} }
private val loadingContext = object : ScriptConfigurationLoadingContext { private val loadingContext = object : ScriptConfigurationLoadingContext {
override fun getCachedConfiguration(file: VirtualFile): ScriptConfigurationSnapshot? = override fun getCachedConfiguration(file: VirtualFile): ScriptConfigurationSnapshot? =
this@DefaultScriptConfigurationManager.getAppliedConfiguration(file) getAppliedConfiguration(file)
override fun suggestNewConfiguration(file: VirtualFile, newResult: ScriptConfigurationSnapshot) { override fun suggestNewConfiguration(file: VirtualFile, newResult: ScriptConfigurationSnapshot) {
suggestOrSaveConfiguration(file, newResult, false) suggestOrSaveConfiguration(file, newResult, false)
@@ -275,9 +201,7 @@ internal class DefaultScriptConfigurationManager(project: Project) :
setLoadedConfiguration(file, newResult) setLoadedConfiguration(file, newResult)
ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { LoadScriptConfigurationNotificationFactory.hideNotification(file, project)
it.hideNotification(file, project)
}
val newConfiguration = newResult.configuration val newConfiguration = newResult.configuration
if (newConfiguration == null) { 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<KtFile>): 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<KtFile>, 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 { object DefaultScriptConfigurationManagerExtensions {
val LOADER: ExtensionPointName<ScriptConfigurationLoader> = val LOADER: ExtensionPointName<ScriptConfigurationLoader> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader") ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader")
} }
val ScriptConfigurationManager.testingBackgroundExecutor val ScriptConfigurationManager.testingBackgroundExecutor
get() = (this as DefaultScriptConfigurationManager).backgroundExecutor as TestingBackgroundExecutor get() = (this as CompositeManager).managers
.firstIsInstance<DefaultScriptingSupport>()
.backgroundExecutor as TestingBackgroundExecutor
@@ -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)
}
}
}
}
}
@@ -33,6 +33,8 @@ interface ScriptConfigurationCache {
fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper> fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper>
fun clear() fun clear()
fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper?
} }
sealed class ScriptConfigurationCacheScope { sealed class ScriptConfigurationCacheScope {
@@ -24,6 +24,9 @@ open class ScriptConfigurationMemoryCache(
return memoryCache.get(file) return memoryCache.get(file)
} }
override fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper? =
memoryCache.entrySet().firstOrNull()?.value?.loaded?.configuration
@Synchronized @Synchronized
override fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot) { override fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot) {
val old = memoryCache[file] ?: ScriptConfigurationState() val old = memoryCache[file] ?: ScriptConfigurationState()
@@ -9,7 +9,6 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager 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.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
@@ -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<VirtualFile, ScriptCompilationConfigurationWrapper>
) : 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<VirtualFile, Sdk?> =
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<VirtualFile, ScriptCompilationConfigurationWrapper>,
project: Project
): ScriptClassRootsStorage.Companion.ScriptClassRoots {
val classpath = mutableSetOf<File>()
val sources = mutableSetOf<File>()
val sdks = mutableSetOf<Sdk>()
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
)
}
}
}
@@ -10,53 +10,41 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.*
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.NonClasspathDirectoriesScope import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.ConcurrentFactoryMap import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File import java.io.File
import java.nio.file.FileSystems
internal class ScriptClassRootsCache( abstract class ScriptClassRootsCache(
private val project: Project, private val project: Project,
private val all: Map<VirtualFile, ScriptCompilationConfigurationWrapper> private val roots: ScriptClassRootsStorage.Companion.ScriptClassRoots
) { ) {
private val scriptsSdksCache: Map<VirtualFile, Sdk?> = protected abstract val fileToConfiguration: (VirtualFile) -> ScriptCompilationConfigurationWrapper?
ConcurrentFactoryMap.createWeakMap { file ->
return@createWeakMap getScriptSdk(all[file]) ?: ScriptConfigurationManager.getScriptDefaultSdk(project)
}
fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file] protected abstract val rootsCacheKey: ScriptClassRootsStorage.Companion.Key
private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? { abstract val firstScriptSdk: Sdk?
// workaround for mismatched gradle wrapper and plugin version
val javaHome = try {
compilationConfiguration?.javaHome?.let { VfsUtil.findFileByIoFile(it, true) }
} catch (e: Throwable) {
null
} ?: return null
return getAllProjectSdks().find { it.homeDirectory == javaHome } abstract fun getScriptSdk(file: VirtualFile): Sdk?
}
val firstScriptSdk: Sdk? by lazy { abstract fun contains(file: VirtualFile): Boolean
val firstCachedScript = all.keys.firstOrNull() ?: return@lazy null
return@lazy getScriptSdk(firstCachedScript)
}
private fun Sdk.isAlreadyIndexed(): Boolean { class Fat(
return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this } val scriptConfiguration: ScriptCompilationConfigurationWrapper,
} val classFilesScope: GlobalSearchScope
)
val allDependenciesClassFiles by lazy { val allDependenciesClassFiles by lazy {
ScriptClassRootsStorage.getInstance(project).loadClasspathRoots() ScriptClassRootsStorage.getInstance(project).loadClasspathRoots(rootsCacheKey)
} }
val allDependenciesSources by lazy { val allDependenciesSources by lazy {
ScriptClassRootsStorage.getInstance(project).loadSourcesRoots() ScriptClassRootsStorage.getInstance(project).loadSourcesRoots(rootsCacheKey)
} }
val allDependenciesClassFilesScope by lazy { val allDependenciesClassFilesScope by lazy {
@@ -67,85 +55,71 @@ internal class ScriptClassRootsCache(
NonClasspathDirectoriesScope.compose(allDependenciesSources) NonClasspathDirectoriesScope.compose(allDependenciesSources)
} }
private val scriptsDependenciesClasspathScopeCache: MutableMap<VirtualFile, GlobalSearchScope> = private val scriptsDependenciesCache: MutableMap<VirtualFile, Fat> =
ConcurrentFactoryMap.createWeakMap { file -> ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = all[file] val configuration = fileToConfiguration(file) ?: return@createWeakMap null
?: return@createWeakMap GlobalSearchScope.EMPTY_SCOPE
val roots = compilationConfiguration.dependenciesClassPath val roots = configuration.dependenciesClassPath
val sdk = scriptsSdksCache[file] val sdk = getScriptSdk(file)
@Suppress("FoldInitializerAndIfToElvis") @Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) { if (sdk == null) {
return@createWeakMap NonClasspathDirectoriesScope.compose( return@createWeakMap Fat(
ScriptConfigurationManager.toVfsRoots( configuration,
roots NonClasspathDirectoriesScope.compose(ScriptConfigurationManager.toVfsRoots(roots))
)
) )
} }
return@createWeakMap NonClasspathDirectoriesScope.compose( return@createWeakMap Fat(
sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots( configuration,
roots NonClasspathDirectoriesScope.compose(
sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots(roots)
) )
) )
} }
fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope { fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return scriptsDependenciesClasspathScopeCache[file] ?: GlobalSearchScope.EMPTY_SCOPE return scriptsDependenciesCache[file]?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE
} }
fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean { fun getScriptConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
return !ScriptClassRootsStorage.getInstance(project).containsAll(extractRoots(compilationConfiguration)) return scriptsDependenciesCache[file]?.scriptConfiguration
} }
private fun extractRoots(configuration: ScriptCompilationConfigurationWrapper): ScriptClassRootsStorage.Companion.ScriptClassRoots { fun hasNotCachedRoots(roots: ScriptClassRootsStorage.Companion.ScriptClassRoots): Boolean {
val scriptSdk = getScriptSdk(configuration) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) return !ScriptClassRootsStorage.getInstance(project).containsAll(rootsCacheKey, roots)
if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) {
return ScriptClassRootsStorage.Companion.ScriptClassRoots(
configuration.dependenciesClassPath,
configuration.dependenciesSources,
listOf(scriptSdk)
)
}
return ScriptClassRootsStorage.Companion.ScriptClassRoots(
configuration.dependenciesClassPath,
configuration.dependenciesSources,
emptyList()
)
} }
init { fun saveClassRootsToStorage() {
saveClassRootsToStorage()
}
private fun saveClassRootsToStorage() {
if (all.isEmpty()) return
val classpath = mutableSetOf<File>()
val sources = mutableSetOf<File>()
val sdks = mutableSetOf<Sdk>()
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)
}
val rootsStorage = ScriptClassRootsStorage.getInstance(project) val rootsStorage = ScriptClassRootsStorage.getInstance(project)
rootsStorage.save( if (roots.classpathFiles.isNotEmpty() || roots.sourcesFiles.isNotEmpty() || roots.sdks.isNotEmpty()) {
ScriptClassRootsStorage.Companion.ScriptClassRoots( rootsStorage.save(rootsCacheKey, roots)
classpath.toList(), }
sources.toList(),
sdks.toList()
)
)
} }
fun contains(file: VirtualFile) = all.containsKey(file) companion object {
} fun toStringValues(prop: Collection<File>): Set<String> {
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 }
}
}
}
@@ -20,13 +20,12 @@ import java.util.concurrent.atomic.AtomicInteger
/** /**
* Utility for postponing indexing of new roots to the end of some bulk operation. * Utility for postponing indexing of new roots to the end of some bulk operation.
*/ */
internal class ScriptClassRootsIndexer(val project: Project) { class ScriptClassRootsIndexer(val project: Project) {
private var newRootsPresent: Boolean = false private var newRootsPresent: Boolean = false
private val concurrentTransactions = AtomicInteger() private val concurrentTransactions = AtomicInteger()
@Synchronized @Synchronized
fun markNewRoot(file: VirtualFile, configuration: ScriptCompilationConfigurationWrapper) { fun markNewRoot() {
debug(file) { "new class roots found: $configuration" }
checkInTransaction() checkInTransaction()
newRootsPresent = true newRootsPresent = true
} }
@@ -10,57 +10,84 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.JarFileSystem
import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.URLUtil import org.jdom.Element
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.kotlin.idea.core.script.debug import org.jetbrains.kotlin.idea.core.script.debug
import java.io.File
@State( @State(
name = "ScriptClassRootsStorage", name = "ScriptClassRootsStorage",
storages = [Storage(StoragePathMacros.CACHE_FILE)] storages = [Storage(StoragePathMacros.CACHE_FILE)]
) )
class ScriptClassRootsStorage : PersistentStateComponent<ScriptClassRootsStorage> { class ScriptClassRootsStorage : PersistentStateComponent<Element> {
private var classpath: Set<String> = hashSetOf()
private var sources: Set<String> = hashSetOf()
private var sdks: Set<String> = hashSetOf()
override fun getState(): ScriptClassRootsStorage? { private var classpath: MutableMap<String, Set<String>> = hashMapOf()
return this private var sources: MutableMap<String, Set<String>> = hashMapOf()
private var sdks: MutableMap<String, Set<String>> = 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) { private fun storeCollection(root: Element, col: Map<String, Set<String>>, name: String) {
XmlSerializerUtil.copyBean(state, this) 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<String, Set<String>> {
val result: MutableMap<String, HashSet<String>> = hashMapOf()
for (it in root.getChildren(name)) {
result.getOrPut(it.getAttributeValue("key")) {
hashSetOf()
}.add(it.getAttributeValue("path"))
}
@Suppress("UNCHECKED_CAST")
return result as MutableMap<String, Set<String>>
} }
private fun toStringNames(sdks: Collection<Sdk>): Set<String> { private fun toStringNames(sdks: Collection<Sdk>): Set<String> {
return sdks.map { it.name }.toSet() return sdks.map { it.name }.toSet()
} }
private fun toStringValues(prop: Collection<File>): Set<String> { private fun toVirtualFiles(prop: Set<String>?, sources: Boolean): List<VirtualFile> {
return prop.mapNotNull { if (prop == null) return emptyList()
when {
it.isDirectory -> it.absolutePath
it.isFile -> it.absolutePath + URLUtil.JAR_SEPARATOR
else -> null
}
}.toSet()
}
private fun toVirtualFiles(prop: Set<String>, sources: Boolean): List<VirtualFile> {
val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES
return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) } return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) }
.flatMap { it.rootProvider.getFiles(rootType).toList() } .flatMap { it.rootProvider.getFiles(rootType).toList() }
} }
private fun toVirtualFiles(prop: Collection<String>): List<VirtualFile> { private fun toVirtualFiles(prop: Collection<String>?): List<VirtualFile> {
if (prop == null) return emptyList()
return prop.mapNotNull { return prop.mapNotNull {
StandardFileSystems.local()?.findFileByPath(it)?.let { if (it.endsWith(JarFileSystem.PROTOCOL)) {
return@mapNotNull it StandardFileSystems.jar()?.findFileByPath(it + JarFileSystem.JAR_SEPARATOR)?.let {
return@mapNotNull it
}
} }
StandardFileSystems.jar()?.findFileByPath(it)?.let { StandardFileSystems.local()?.findFileByPath(it)?.let {
return@mapNotNull it return@mapNotNull it
} }
@@ -70,36 +97,40 @@ class ScriptClassRootsStorage : PersistentStateComponent<ScriptClassRootsStorage
}.distinct() }.distinct()
} }
fun containsAll(configuration: ScriptClassRoots): Boolean { fun containsAll(key: Key, configuration: ScriptClassRoots): Boolean {
if (!classpath.containsAll(toStringValues(configuration.classpathFiles))) { if (configuration.classpathFiles.isNotEmpty() && classpath[key.value]?.containsAll(configuration.classpathFiles) != true) {
debug { "class roots were changed: old = $classpath, new = ${configuration.classpathFiles}" } debug { "class roots were changed: old = $classpath, new = ${configuration.classpathFiles}" }
return false return false
} }
if (!sources.containsAll(toStringValues(configuration.sourcesFiles))) { if (configuration.sourcesFiles.isNotEmpty() && sources[key.value]?.containsAll(configuration.sourcesFiles) != true) {
debug { "source roots were changed: old = $sources, new = ${configuration.sourcesFiles}" } debug { "source roots were changed: old = $sources, new = ${configuration.sourcesFiles}" }
return false return false
} }
if (!sdks.containsAll(toStringNames(configuration.sdks))) { if (configuration.sdks.isNotEmpty() && sdks[key.value]?.containsAll(toStringNames(configuration.sdks)) != true) {
debug { "sdk classes were changed: old = $sdks, new = ${configuration.sdks.map { it.homePath }}" } debug { "sdk classes were changed: old = $sdks, new = ${configuration.sdks.map { it.homePath }}" }
return false return false
} }
return true return true
} }
fun save(configuration: ScriptClassRoots) { fun save(key: Key, configuration: ScriptClassRoots) {
// TODO: do not drop all storage on save: KT-34444 // TODO: do not drop all storage on save: KT-34444
classpath = toStringValues(configuration.classpathFiles) classpath.getOrPut(key.value, { emptySet() })
sources = toStringValues(configuration.sourcesFiles) classpath.replace(key.value, configuration.classpathFiles)
sdks = toStringNames(configuration.sdks) sources.getOrPut(key.value, { emptySet() })
sources.replace(key.value, configuration.sourcesFiles)
sdks.getOrPut(key.value, { emptySet() })
sdks.replace(key.value, toStringNames(configuration.sdks))
} }
fun loadClasspathRoots(): List<VirtualFile> { fun loadClasspathRoots(key: Key): List<VirtualFile> {
return toVirtualFiles(sdks, false) + toVirtualFiles(classpath) return toVirtualFiles(sdks[key.value], false) + toVirtualFiles(classpath[key.value])
} }
fun loadSourcesRoots(): List<VirtualFile> { fun loadSourcesRoots(key: Key): List<VirtualFile> {
return toVirtualFiles(sdks, true) + toVirtualFiles(sources) return toVirtualFiles(sdks[key.value], true) + toVirtualFiles(sources[key.value])
} }
companion object { companion object {
@@ -107,9 +138,13 @@ class ScriptClassRootsStorage : PersistentStateComponent<ScriptClassRootsStorage
ServiceManager.getService(project, ScriptClassRootsStorage::class.java) ServiceManager.getService(project, ScriptClassRootsStorage::class.java)
data class ScriptClassRoots( data class ScriptClassRoots(
val classpathFiles: List<File>, val classpathFiles: Set<String>,
val sourcesFiles: List<File>, val sourcesFiles: Set<String>,
val sdks: List<Sdk> val sdks: Set<Sdk>
) )
val EMPTY = ScriptClassRoots(emptySet(), emptySet(), emptySet())
data class Key(val value: String)
} }
} }
@@ -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()
)
}
}
}
@@ -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? private var Project.notificationPanel: ScriptConfigurationChangedNotification?
by UserDataProperty<Project, ScriptConfigurationChangedNotification>(Key.create("load.script.configuration.panel")) by UserDataProperty<Project, ScriptConfigurationChangedNotification>(Key.create("load.script.configuration.panel"))
fun showNotificationForProjectImport(project: Project, callback: () -> Unit) { fun showNotificationForProjectImport(project: Project) {
val existingPanel = project.notificationPanel val existingPanel = project.notificationPanel
if (existingPanel != null) { if (existingPanel != null) {
existingPanel.update(callback)
return return
} }
@@ -53,7 +52,7 @@ fun showNotificationForProjectImport(project: Project, callback: () -> Unit) {
) )
} }
val notification = ScriptConfigurationChangedNotification(project, callback) val notification = ScriptConfigurationChangedNotification(project)
project.notificationPanel = notification project.notificationPanel = notification
notification.notify(project) notification.notify(project)
} }
@@ -64,7 +63,7 @@ fun hideNotificationForProjectImport(project: Project): Boolean {
return true return true
} }
private class ScriptConfigurationChangedNotification(val project: Project, onClick: () -> Unit) : private class ScriptConfigurationChangedNotification(val project: Project) :
Notification( Notification(
kotlinDslNotificationGroupId, kotlinDslNotificationGroupId,
KotlinIcons.LOAD_SCRIPT_CONFIGURATION, KotlinIcons.LOAD_SCRIPT_CONFIGURATION,
@@ -76,9 +75,7 @@ private class ScriptConfigurationChangedNotification(val project: Project, onCli
) { ) {
init { init {
//NotificationGroup.findRegisteredGroup(s) ?: NotificationGroup.balloonGroup(s) addAction(LoadConfigurationAction())
addAction(LoadConfigurationAction(onClick))
addAction(NotificationAction.createSimple(KotlinIdeaGradleBundle.message("action.label.enable.auto.import")) { addAction(NotificationAction.createSimple(KotlinIdeaGradleBundle.message("action.label.enable.auto.import")) {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
val projectSettings = gradleSettings.getLinkedProjectsSettings() val projectSettings = gradleSettings.getLinkedProjectsSettings()
@@ -87,22 +84,20 @@ private class ScriptConfigurationChangedNotification(val project: Project, onCli
if (projectSettings != null) { if (projectSettings != null) {
projectSettings.isUseAutoImport = true projectSettings.isUseAutoImport = true
} }
runPartialGradleImport(project)
}) })
} }
fun update(onClick: () -> Unit) {
actions.filterIsInstance<LoadConfigurationAction>().forEach { it.onClick = onClick }
}
override fun expire() { override fun expire() {
super.expire() super.expire()
project.notificationPanel = null 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) { override fun actionPerformed(e: AnActionEvent) {
onClick() val project = e.project ?: return
runPartialGradleImport(project)
} }
} }
} }
@@ -8,7 +8,6 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.diff.util.DiffUtil import com.intellij.diff.util.DiffUtil
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
@@ -46,17 +45,16 @@ fun runPartialGradleImport(project: Project) {
) )
} }
private var Project.notificationPanel: Boolean? private var Project.shouldShowLoadConfiguraionsAction: Boolean?
by UserDataProperty<Project, Boolean>(Key.create("load.script.configuration.panel")) by UserDataProperty<Project, Boolean>(Key.create("load.script.configuration.action"))
fun showNotificationForProjectImport(project: Project, callback: () -> Unit) { fun showNotificationForProjectImport(project: Project) {
project.notificationPanel = true project.shouldShowLoadConfiguraionsAction = true
(ActionManager.getInstance().getAction("LoadConfigurationAction") as LoadConfigurationAction).onClick = callback
} }
fun hideNotificationForProjectImport(project: Project): Boolean { fun hideNotificationForProjectImport(project: Project): Boolean {
project.notificationPanel = false project.shouldShowLoadConfiguraionsAction = false
return true return true
} }
@@ -65,11 +63,9 @@ class LoadConfigurationAction : AnAction(
KotlinIdeaGradleBundle.message("action.description.load.script.configurations"), KotlinIdeaGradleBundle.message("action.description.load.script.configurations"),
KotlinIcons.LOAD_SCRIPT_CONFIGURATION KotlinIcons.LOAD_SCRIPT_CONFIGURATION
) { ) {
var onClick = {}
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
onClick() val project = e.project ?: return
runPartialGradleImport(project)
} }
override fun update(e: AnActionEvent) { override fun update(e: AnActionEvent) {
@@ -79,24 +75,28 @@ class LoadConfigurationAction : AnAction(
} }
private fun ensureValidActionVisibility(e: AnActionEvent) { private fun ensureValidActionVisibility(e: AnActionEvent) {
if (e.project?.notificationPanel != true) { if (e.project?.shouldShowLoadConfiguraionsAction != true) {
e.presentation.isVisible = false e.presentation.isVisible = false
return return
} }
val editor = e.getData(CommonDataKeys.EDITOR) ?: return val editor = e.getData(CommonDataKeys.EDITOR) ?: return
when { if (DiffUtil.isDiffEditor(editor)) {
DiffUtil.isDiffEditor(editor) -> e.presentation.isVisible = false e.presentation.isVisible = false
!editor.isScriptEditor() -> e.presentation.isVisible = false return
} }
e.presentation.isVisible = editor.isScriptEditor()
} }
private fun Editor.isScriptEditor(): Boolean { private fun Editor.isScriptEditor(): Boolean {
val project = project ?: return false
val documentManager = FileDocumentManager.getInstance() val documentManager = FileDocumentManager.getInstance()
val virtualFile = documentManager.getFile(document) val virtualFile = documentManager.getFile(document)
if (virtualFile is LightVirtualFileBase) return false if (virtualFile is LightVirtualFileBase) return false
if (virtualFile == null || !virtualFile.isValid) return false if (virtualFile == null || !virtualFile.isValid) return false
val project = project ?: return false
// todo only for gradle script
return virtualFile.findScriptDefinition(project) != null return virtualFile.findScriptDefinition(project) != null
} }
} }
@@ -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) val result = getConfigurationThroughScriptingApi(ktFile, vFile, scriptDefinition)
context.saveNewConfiguration(vFile, result) context.saveNewConfiguration(vFile, result)
return true return true
@@ -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<String>,
val models: List<KotlinDslScriptModel>
)
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<String> = 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<KotlinDslScriptModel>) {
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<KotlinScriptDefinitionFromAnnotatedTemplate>()
?.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<KtFile>): Boolean = true
override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) {
updateNotification(file)
}
}
companion object {
fun getInstance(project: Project): GradleScriptingSupport {
return SCRIPTING_SUPPORT.getPoint(project).extensionList.firstIsInstance()
}
}
}
@@ -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
}
}
@@ -5,9 +5,11 @@
package org.jetbrains.kotlin.idea.scripting.gradle.importing package org.jetbrains.kotlin.idea.scripting.gradle.importing
import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs
data class KotlinDslScriptModel( data class KotlinDslScriptModel(
val file: String, val file: String,
val inputsTimeStamp: Long, val inputs: GradleKotlinScriptConfigurationInputs,
val classPath: List<String>, val classPath: List<String>,
val sourcePath: List<String>, val sourcePath: List<String>,
val imports: List<String>, val imports: List<String>,
@@ -7,16 +7,14 @@ package org.jetbrains.kotlin.idea.scripting.gradle.importing
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId 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.project.Project
import com.intellij.openapi.vfs.VfsUtil 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.EditorReportSeverity
import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle 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.GradleScriptInputsWatcher
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupport
import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper 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.kotlin.scripting.resolve.adjustByDefinition
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings 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 java.io.File
import kotlin.script.experimental.api.* import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency import kotlin.script.experimental.jvm.JvmDependency
@@ -40,8 +40,9 @@ fun processScriptModel(
"Couldn't get KotlinDslScriptsModel for $projectName:\n${model.message}\n${model.stackTrace}" "Couldn't get KotlinDslScriptsModel for $projectName:\n${model.message}\n${model.stackTrace}"
) )
} else { } else {
val models = model.toListOfScriptModels() val project = resolverCtx.externalSystemTaskId.findProject() ?: return
resolverCtx.externalSystemTaskId.findProject()?.kotlinDslModels?.addAll( val models = model.toListOfScriptModels(project)
project.kotlinDslModels.addAll(
models models
) )
if (models.containsErrors()) { if (models.containsErrors()) {
@@ -58,7 +59,7 @@ private fun Collection<KotlinDslScriptModel>.containsErrors(): Boolean {
return any { it.messages.any { it.severity == KotlinDslScriptModel.Severity.ERROR } } return any { it.messages.any { it.severity == KotlinDslScriptModel.Severity.ERROR } }
} }
private fun KotlinDslScriptsModel.toListOfScriptModels(): List<KotlinDslScriptModel> = private fun KotlinDslScriptsModel.toListOfScriptModels(project: Project): List<KotlinDslScriptModel> =
scriptModels.map { (file, model) -> scriptModels.map { (file, model) ->
val messages = mutableListOf<KotlinDslScriptModel.Message>() val messages = mutableListOf<KotlinDslScriptModel.Message>()
@@ -91,10 +92,14 @@ private fun KotlinDslScriptsModel.toListOfScriptModels(): List<KotlinDslScriptMo
) )
} }
// TODO: NPE
val virtualFile = VfsUtil.findFile(file.toPath(), true)!!
// todo(KT-34440): take inputs snapshot before starting import // todo(KT-34440): take inputs snapshot before starting import
KotlinDslScriptModel( KotlinDslScriptModel(
file.absolutePath, file.absolutePath,
System.currentTimeMillis(), // TODO: NPE
getGradleScriptInputsStamp(project, virtualFile)!!,
model.classPath.map { it.absolutePath }, model.classPath.map { it.absolutePath },
model.sourcePath.map { it.absolutePath }, model.sourcePath.map { it.absolutePath },
model.implicitImports, model.implicitImports,
@@ -102,69 +107,62 @@ private fun KotlinDslScriptsModel.toListOfScriptModels(): List<KotlinDslScriptMo
) )
} }
fun createGradleKtsContextIfPossible(project: Project): GradleKtsContext? {
val gradleProjectSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
.getLinkedProjectsSettings()
.filterIsInstance<GradleProjectSettings>().firstOrNull() ?: return null
val gradleExeSettings = ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>(
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( fun saveScriptModels(
project: Project, project: Project,
task: ExternalSystemTaskId, task: ExternalSystemTaskId,
javaHomeStr: String?, javaHomeStr: String?,
models: List<KotlinDslScriptModel> models: List<KotlinDslScriptModel>
) { ) {
val scriptConfigurations = mutableListOf<Pair<VirtualFile, ScriptConfigurationSnapshot>>()
val errorReporter = KotlinGradleDslErrorReporter(project, task) val errorReporter = KotlinGradleDslErrorReporter(project, task)
val javaHome = javaHomeStr?.let { File(it) } val javaHome = javaHomeStr?.let { File(it) }
val context = GradleKtsContext(project, javaHome)
models.forEach { model -> models.forEach { model ->
val scriptFile = File(model.file) errorReporter.reportError(File(model.file), model)
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)
} }
project.service<GradleScriptInputsWatcher>().saveGradleProjectRootsAfterImport( project.service<GradleScriptInputsWatcher>().saveGradleProjectRootsAfterImport(
scriptConfigurations.map { it.first.parent.path }.toSet() models.map { File(it.file).parent }.toSet()
) )
project.service<ScriptConfigurationManager>().saveCompilationConfigurationAfterImport(scriptConfigurations) GradleScriptingSupport.getInstance(project).replace(context, models)
project.service<GradleScriptInputsWatcher>().clearState() project.service<GradleScriptInputsWatcher>().clearState()
} }
@@ -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<String, Int>()
fun getStringId(string: String) = ids.getOrPut(string) {
check(!freeze)
ids.size
}
fun addString(string: String) {
getStringId(string)
}
fun addStrings(list: Collection<String>) {
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<String>) {
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<String>) {
fun getString(id: Int) = strings[id]
fun readString() = getString(input.readInt())
fun readStrings(): List<String> = input.readList { readString() }
}
}
private inline fun <T> DataOutputStream.writeList(list: Collection<T>, write: (T) -> Unit) {
writeInt(list.size)
list.forEach { write(it) }
}
private inline fun <T> DataInputStream.readList(read: () -> T): List<T> {
val n = readInt()
val result = ArrayList<T>(n)
repeat(n) {
result.add(read())
}
return result
}
@@ -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())
}
}
@@ -68,10 +68,6 @@
interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener" interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener"
area="IDEA_PROJECT"/> area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.notificationFactory"
interface="org.jetbrains.kotlin.idea.core.script.ScriptConfigurationNotificationFactory"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.additionalExtractableAnalyser" <extensionPoint qualifiedName="org.jetbrains.kotlin.additionalExtractableAnalyser"
interface="org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AdditionalExtractableAnalyser"/> interface="org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AdditionalExtractableAnalyser"/>
@@ -105,11 +101,6 @@
<scriptDefinitionsProvider id="MainKtsScriptDefinitionSource" <scriptDefinitionsProvider id="MainKtsScriptDefinitionSource"
implementation="org.jetbrains.kotlin.idea.script.MainKtsScriptDefinitionSource"/> implementation="org.jetbrains.kotlin.idea.script.MainKtsScriptDefinitionSource"/>
<scripting.idea.notificationFactory
implementation="org.jetbrains.kotlin.idea.core.script.DefaultScriptConfigurationNotificationFactory"
order="last"
/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JvmPlatformKindResolution"/> <idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JvmPlatformKindResolution"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JsPlatformKindResolution"/> <idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JsPlatformKindResolution"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.CommonPlatformKindResolution"/> <idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.CommonPlatformKindResolution"/>
+2 -2
View File
@@ -15,8 +15,8 @@
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea"> <extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/> <loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/>
<notificationFactory <scriptingSupport
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationNotificationFactory" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupport"
/> />
</extensions> </extensions>
+2 -2
View File
@@ -15,8 +15,8 @@
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea"> <extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/> <loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/>
<notificationFactory <scriptingSupport
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationNotificationFactory" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupport"
/> />
</extensions> </extensions>
+1 -1
View File
@@ -231,7 +231,7 @@
serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptDependenciesProvider"/> serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptDependenciesProvider"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager" <projectService serviceInterface="org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager"/> serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.CompositeManager"/>
<projectService serviceInterface="org.jetbrains.kotlin.scripting.resolve.ScriptReportSink" <projectService serviceInterface="org.jetbrains.kotlin.scripting.resolve.ScriptReportSink"
serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink"/> serviceImplementation="org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink"/>
@@ -1,7 +1,20 @@
<idea-plugin> <idea-plugin>
<extensionPoints>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.scriptingSupport"
interface="org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport"
area="IDEA_PROJECT"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<projectService serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage"/> <projectService serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage"/>
<trafficLightRendererContributor implementation="org.jetbrains.kotlin.idea.core.script.ScriptTrafficLightRendererContributor"/> <trafficLightRendererContributor implementation="org.jetbrains.kotlin.idea.core.script.ScriptTrafficLightRendererContributor"/>
</extensions> </extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<scriptingSupport
implementation="org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport"
order="last"
/>
</extensions>
</idea-plugin> </idea-plugin>