Revert due to test failing in 201

This commit is contained in:
Sergey Rostov
2020-05-08 10:12:18 +03:00
parent 05797afaf8
commit 14edc26cf1
61 changed files with 1473 additions and 1977 deletions
@@ -95,7 +95,7 @@ private val constantMap = ConcurrentHashMap<Function0<*>, Any>()
fun String.indexOfOrNull(char: Char, startIndex: Int = 0, ignoreCase: Boolean = false): Int? =
indexOf(char, startIndex, ignoreCase).takeIf { it >= 0 }
fun String.lastIndexOfOrNull(char: Char, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int? =
fun String.lastIndexOfOrNull(char: Char, startIndex: Int = 0, ignoreCase: Boolean = false): Int? =
lastIndexOf(char, startIndex, ignoreCase).takeIf { it >= 0 }
inline fun <T, R : Any> Iterable<T>.firstNotNullResult(transform: (T) -> R?): R? {
@@ -27,6 +27,8 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.io.URLUtil
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
@@ -67,6 +69,12 @@ interface ScriptConfigurationManager {
*/
fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper?
/**
* Reload the configuration for [file] even it is already loaded.
* [loader] is used to load configuration. Other loaders aren't taken into account.
*/
fun forceReloadConfiguration(file: VirtualFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper?
@Deprecated("Use getScriptClasspath(KtFile) instead")
fun getScriptClasspath(file: VirtualFile): List<VirtualFile>
@@ -90,9 +98,14 @@ interface ScriptConfigurationManager {
fun isConfigurationLoadingInProgress(file: KtFile): Boolean
/**
* Update caches that depends on script definitions and do update if necessary
* See [ScriptConfigurationUpdater].
*/
fun updateScriptDefinitionReferences()
val updater: ScriptConfigurationUpdater
/**
* Clear all caches and re-highlighting opened scripts
*/
fun clearConfigurationCachesAndRehighlight()
///////////////
// classpath roots info:
@@ -113,14 +126,14 @@ interface ScriptConfigurationManager {
ServiceManager.getService(project, ScriptConfigurationManager::class.java)
fun toVfsRoots(roots: Iterable<File>): List<VirtualFile> {
return roots.mapNotNull { classpathEntryToVfs(it) }
return roots.mapNotNull { it.classpathEntryToVfs() }
}
fun classpathEntryToVfs(file: File): VirtualFile? {
private fun File.classpathEntryToVfs(): VirtualFile? {
val res = when {
!file.exists() -> null
file.isDirectory -> StandardFileSystems.local()?.findFileByPath(file.canonicalPath)
file.isFile -> StandardFileSystems.jar()?.findFileByPath(file.canonicalPath + URLUtil.JAR_SEPARATOR)
!exists() -> null
isDirectory -> StandardFileSystems.local()?.findFileByPath(this.canonicalPath)
isFile -> StandardFileSystems.jar()?.findFileByPath(this.canonicalPath + URLUtil.JAR_SEPARATOR)
else -> null
}
// TODO: report this somewhere, but do not throw: assert(res != null, { "Invalid classpath entry '$this': exists: ${exists()}, is directory: $isDirectory, is file: $isFile" })
@@ -137,7 +150,7 @@ interface ScriptConfigurationManager {
@TestOnly
fun clearCaches(project: Project) {
(getInstance(project) as CompositeScriptConfigurationManager).default
.updateScriptDefinitions()
.clearCaches()
}
fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) {
@@ -27,7 +27,6 @@ import com.intellij.util.containers.SLRUMap
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.project.SdkInfo
import org.jetbrains.kotlin.idea.caches.project.getScriptRelatedModuleInfo
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.util.application.getServiceSafe
import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
@@ -65,15 +64,9 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
private val scriptDefinitionsCacheLock = ReentrantLock()
private val scriptDefinitionsCache = SLRUMap<String, ScriptDefinition>(10, 10)
val configurations = (ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager)
override fun findDefinition(script: SourceCode): ScriptDefinition? {
val locationId = script.locationId ?: return null
if (nonScriptId(locationId)) return null
val fastPath = configurations.tryGetScriptDefinitionFast(locationId)
if (fastPath != null) return fastPath
if (!isReady()) return null
val cached = scriptDefinitionsCacheLock.withLock { scriptDefinitionsCache.get(locationId) }
@@ -197,7 +190,7 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
scriptDefinitionsCacheLock.withLock { scriptDefinitionsCache.clear() }
// TODO: clear by script type/definition
ScriptConfigurationManager.getInstance(project).updateScriptDefinitionReferences()
ScriptConfigurationManager.getInstance(project).clearConfigurationCachesAndRehighlight()
}
private fun ScriptDefinitionsSource.safeGetDefinitions(): List<ScriptDefinition> {
@@ -6,9 +6,6 @@
package org.jetbrains.kotlin.idea.core.script.configuration
import com.intellij.ProjectTopics
import com.intellij.codeInsight.daemon.OutsidersPsiFileSupport
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootEvent
@@ -18,106 +15,69 @@ import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
/**
* The [CompositeScriptConfigurationManager] will provide redirection of [ScriptConfigurationManager] calls to the
* custom [ScriptingSupport] or [DefaultScriptingSupport] if that script lack of custom [ScriptingSupport].
*
* The [ScriptConfigurationManager] is implemented by caching all scripts using the [ScriptClassRootsCache].
* The [ScriptClassRootsCache] is always available and never blacked by the writer, as all writes occurs
* using the copy-on-write strategy.
*
* This cache are loaded on start and will be updating asynchronously using the [updater].
* Sync updates still my be occurred from the [DefaultScriptingSupport].
*
* We are also watching all script documents:
* [notifier] will call first applicable [ScriptChangesNotifier.listeners] when editor is activated or document changed.
* Listener should do something to invalidate configuration and schedule reloading.
*/
class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigurationManager {
@Suppress("unused")
private val notifier = ScriptChangesNotifier(project)
private val notifier = ScriptChangesNotifier(project, updater)
val updater = ScriptClassRootsUpdater(project, this)
private val providers = ScriptingSupport.Provider.EPN.getPoint(project).extensionList
private val plugins = ScriptingSupport.EPN.getPoint(project).extensionList
val default = DefaultScriptingSupport(project)
val default = DefaultScriptingSupport(this)
private val managers
get() = mutableListOf<ScriptingSupport>().also { managers ->
managers.add(default)
providers.forEach { managers.addAll(it.all) }
}
fun tryGetScriptDefinitionFast(locationId: String): ScriptDefinition? {
return classpathRoots.getLightScriptInfo(locationId)?.definition
}
private fun getRelatedManager(file: VirtualFile): ScriptingSupport =
providers.firstNotNullResult { it.getSupport(file) } ?: default
private fun getRelatedManager(file: KtFile): ScriptingSupport =
getRelatedManager(file.originalFile.virtualFile)
private fun getOrLoadConfiguration(
virtualFile: VirtualFile,
preloadedKtFile: KtFile? = null
): ScriptCompilationConfigurationWrapper? {
val scriptConfiguration = classpathRoots.getScriptConfiguration(virtualFile)
if (scriptConfiguration != null) return scriptConfiguration
): ScriptCompilationConfigurationWrapper? =
getRelatedManager(virtualFile).getOrLoadConfiguration(virtualFile, preloadedKtFile)
// check that this script should be loaded later in special way (e.g. gradle project import)
// (and not for syntactic diff files)
if (!OutsidersPsiFileSupport.isOutsiderFile(virtualFile)) {
if (plugins.any { it.isApplicable(virtualFile) }) return null
}
return default.getOrLoadConfiguration(virtualFile, preloadedKtFile)
}
override fun getConfiguration(file: KtFile) =
getOrLoadConfiguration(file.originalFile.virtualFile, file)
override fun getConfiguration(file: KtFile) = getOrLoadConfiguration(file.originalFile.virtualFile, file)
override fun hasConfiguration(file: KtFile): Boolean =
classpathRoots.contains(file.originalFile.virtualFile)
getRelatedManager(file).hasCachedConfiguration(file)
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean =
plugins.firstOrNull { it.isApplicable(file.originalFile.virtualFile) }?.isConfigurationLoadingInProgress(file)
?: default.isConfigurationLoadingInProgress(file)
getRelatedManager(file).isConfigurationLoadingInProgress(file)
@Volatile
private var classpathRoots: ScriptClassRootsCache = recreateRootsCache()
override val updater: ScriptConfigurationUpdater
get() = object : ScriptConfigurationUpdater {
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) =
getRelatedManager(file).updater.ensureUpToDatedConfigurationSuggested(file)
fun getLightScriptInfo(file: String): ScriptClassRootsCache.LightScriptInfo? =
classpathRoots.getLightScriptInfo(file)
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean =
files.groupBy { getRelatedManager(it) }.all { (manager, files) ->
manager.updater.ensureConfigurationUpToDate(files)
}
private fun recreateRootsCache(): ScriptClassRootsCache {
val builder = ScriptClassRootsCache.Builder(project)
default.collectConfigurations(builder)
plugins.forEach { it.collectConfigurations(builder) }
return builder.build()
}
fun collectRootsAndCheckNew(): ScriptClassRootsCache.Updates {
val old = classpathRoots
val new = recreateRootsCache()
classpathRoots = new
return new.diff(old)
}
override fun updateScriptDefinitionReferences() {
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
default.updateScriptDefinitions()
if (classpathRoots.customDefinitionsUsed) {
updater.ensureUpdateScheduled()
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) {
if (event.isCausedByFileTypesChange) return
if (classpathRoots.hasInvalidSdk(project)) {
updater.ensureUpdateScheduled()
managers.forEach {
it.clearClassRootsCaches(project)
}
}
})
@@ -128,39 +88,49 @@ class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigur
* Loads script configuration if classpath roots don't contain [file] yet
*/
private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache {
try {
// we should run default loader if this [file] is not cached in [classpathRoots]
// and it is not supported by any of [plugins]
// getOrLoadConfiguration will do this
// (despite that it's result becomes unused, it still may populate [classpathRoots])
getOrLoadConfiguration(file, null)
} catch (cancelled: ProcessCanceledException) {
// read actions may be cancelled if we are called by impatient reader
val manager = getRelatedManager(file)
val classpathRoots = manager.classpathRoots
if (classpathRoots.contains(file)) {
return classpathRoots
}
return this.classpathRoots
getOrLoadConfiguration(file)
return manager.classpathRoots
}
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 =
classpathRoots.getScriptDependenciesClassFilesScope(file)
getActualClasspathRoots(file).getScriptDependenciesClassFilesScope(file)
override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope =
classpathRoots.allDependenciesClassFilesScope
GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesClassFilesScope })
override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope =
classpathRoots.allDependenciesSourcesScope
GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesSourcesScope })
override fun getAllScriptsDependenciesClassFiles(): List<VirtualFile> =
classpathRoots.allDependenciesClassFiles
managers.flatMap { it.classpathRoots.allDependenciesClassFiles }
override fun getAllScriptDependenciesSources(): List<VirtualFile> =
classpathRoots.allDependenciesSources
managers.flatMap { it.classpathRoots.allDependenciesSources }
override fun forceReloadConfiguration(file: VirtualFile, loader: ScriptConfigurationLoader): ScriptCompilationConfigurationWrapper? {
val ktFile = project.getKtFile(file, null) ?: return null
return default.forceReloadConfiguration(ktFile, loader)
}
///////////////////
// Adapters for deprecated API
@@ -174,4 +144,20 @@ class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigur
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()
}
}
}
@@ -20,6 +20,7 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions.LOADER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.*
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
@@ -36,73 +37,21 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.script.experimental.api.ScriptDiagnostic
/**
* Standard implementation of scripts configuration loading and caching.
*
* ## Loading initiation
*
* [getOrLoadConfiguration] will be called when we need to show or analyze some script file.
*
* As described in [DefaultScriptingSupportBase], 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.
*
* Also, [ensureLoadedFromCache] 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.
*
* ## 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.
*/
class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : DefaultScriptingSupportBase(manager) {
class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(project) {
// TODO public for tests
val backgroundExecutor: BackgroundExecutor =
if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(manager.updater)
else DefaultBackgroundExecutor(project, manager.updater)
if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer)
else DefaultBackgroundExecutor(project, rootsIndexer)
private val outsiderLoader = ScriptOutsiderFileConfigurationLoader(project)
private val fileAttributeCache = ScriptConfigurationFileAttributeCache(project)
private val defaultLoader = DefaultScriptConfigurationLoader(project)
private val loaders: List<ScriptConfigurationLoader>
get() = mutableListOf<ScriptConfigurationLoader>().apply {
add(outsiderLoader)
add(fileAttributeCache)
addAll(LOADER.getPoint(project).extensionList)
add(defaultLoader)
private val loaders: Sequence<ScriptConfigurationLoader>
get() = sequence {
yield(outsiderLoader)
yield(fileAttributeCache)
yieldAll(LOADER.getPoint(project).extensionList)
yield(defaultLoader)
}
private val saveLock = ReentrantLock()
@@ -121,7 +70,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
*
* Each files may be in on of the states described below:
* - scriptDefinition is not ready. `ScriptDefinitionsManager.getInstance(project).isReady() == false`.
* [updateScriptDefinitions] will be called when [ScriptDefinitionsManager] will be ready
* [clearConfigurationCachesAndRehighlight] will be called when [ScriptDefinitionsManager] will be ready
* which will call [reloadOutOfDateConfiguration] for opened editors.
* - unknown. When [isFirstLoad] true (`cache[file] == null`).
* - up-to-date. `cache[file]?.upToDate == true`.
@@ -155,39 +104,36 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
override fun reloadOutOfDateConfiguration(
file: KtFile,
isFirstLoad: Boolean,
loadEvenWillNotBeApplied: Boolean,
forceSync: Boolean,
isPostponedLoad: Boolean,
fromCacheOnly: Boolean
): Boolean {
val virtualFile = file.originalFile.virtualFile ?: return false
isPostponedLoad: Boolean
) {
val virtualFile = file.originalFile.virtualFile ?: return
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
val scriptDefinition = file.findScriptDefinition() ?: return false
val autoReloadEnabled = KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
val shouldLoad = isFirstLoad || loadEvenWillNotBeApplied || autoReloadEnabled
if (!shouldLoad) return
val postponeLoading = isPostponedLoad && !autoReloadEnabled && !isFirstLoad
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return
val scriptDefinition = file.findScriptDefinition() ?: return
val (async, sync) = loaders.partition { it.shouldRunInBackground(scriptDefinition) }
val syncLoader = sync.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) }
if (syncLoader == null) {
if (!fromCacheOnly) {
if (forceSync) {
loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) }
} else {
val autoReloadEnabled = KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled
val postponeLoading = isPostponedLoad && !autoReloadEnabled
if (postponeLoading) {
LoadScriptConfigurationNotificationFactory.showNotification(virtualFile, project) {
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true)
}
} else {
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = false)
if (forceSync) {
loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) }
} else {
if (postponeLoading) {
LoadScriptConfigurationNotificationFactory.showNotification(virtualFile, project) {
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = true)
}
} else {
runAsyncLoaders(file, virtualFile, scriptDefinition, async, isLoadingPostponed = false)
}
}
return false
} else {
return true
}
}
@@ -204,7 +150,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
val applied = cached?.applied
if (applied != null && applied.inputs.isUpToDate(project, virtualFile)) {
// in case user reverted to applied configuration
suggestOrSaveConfiguration(virtualFile, applied, skipNotification = isLoadingPostponed)
suggestOrSaveConfiguration(virtualFile, applied, isLoadingPostponed)
} else if (cached == null || !cached.isUpToDate(project, virtualFile)) {
// don't start loading if nothing was changed
// (in case we checking for up-to-date and loading concurrently)
@@ -220,7 +166,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return null
val scriptDefinition = file.findScriptDefinition() ?: return null
manager.updater.update {
rootsIndexer.transaction {
if (!loader.shouldRunInBackground(scriptDefinition)) {
loader.loadDependencies(false, file, scriptDefinition, loadingContext)
} else {
@@ -234,16 +180,8 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
}
private val loadingContext = object : ScriptConfigurationLoadingContext {
/**
* Used from [ScriptOutsiderFileConfigurationLoader] only.
*/
override fun getCachedConfiguration(file: VirtualFile): ScriptConfigurationSnapshot? =
getAppliedConfiguration(file) ?: getFromGlobalCache(file)
private fun getFromGlobalCache(file: VirtualFile): ScriptConfigurationSnapshot? {
val info = manager.getLightScriptInfo(file.path) ?: return null
return ScriptConfigurationSnapshot(CachedConfigurationInputs.UpToDate, listOf(), info.buildConfiguration())
}
getAppliedConfiguration(file)
override fun suggestNewConfiguration(file: VirtualFile, newResult: ScriptConfigurationSnapshot) {
suggestOrSaveConfiguration(file, newResult, false)
@@ -286,7 +224,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
file.removeScriptDependenciesNotificationPanel(project)
}
saveReports(file, newResult.reports)
setAppliedConfiguration(file, newResult, syncUpdate = true)
setAppliedConfiguration(file, newResult)
} else {
debug(file) {
"configuration changed, notification is shown: old = $oldConfiguration, new = $newConfiguration"
@@ -302,7 +240,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
onClick = {
saveReports(file, newResult.reports)
file.removeScriptDependenciesNotificationPanel(project)
manager.updater.update {
rootsIndexer.transaction {
setAppliedConfiguration(file, newResult)
}
}
@@ -334,28 +272,10 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
}
}
}
companion object {
fun getInstance(project: Project) =
(ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager).default
}
}
/**
* Abstraction for [DefaultScriptingSupportBase] based [cache] and [reloadOutOfDateConfiguration].
* Among this two methods concrete implementation should provide script changes listening.
*
* 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).
*
* [ScriptClassRootsCache] will be calculated based on [cache]d configurations.
* Every change in [cache] will invalidate [ScriptClassRootsCache] cache.
*/
abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigurationManager) {
val project: Project
get() = manager.project
abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupport() {
protected val rootsIndexer = ScriptClassRootsIndexer(project)
@Suppress("LeakingThis")
protected val cache: ScriptConfigurationCache = createCache()
@@ -369,18 +289,19 @@ abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigura
* (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
* @param fromCacheOnly load only when builtin fast synchronous loaders are available
* @return true, if configuration loaded in sync
*/
protected abstract fun reloadOutOfDateConfiguration(
file: KtFile,
isFirstLoad: Boolean = getAppliedConfiguration(file.originalFile.virtualFile) == null,
loadEvenWillNotBeApplied: Boolean = false,
forceSync: Boolean = false,
isPostponedLoad: Boolean = false,
fromCacheOnly: Boolean = false
): Boolean
isPostponedLoad: Boolean = false
)
fun getCachedConfigurationState(file: VirtualFile?): ScriptConfigurationState? {
if (file == null) return null
@@ -390,14 +311,14 @@ abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigura
fun getAppliedConfiguration(file: VirtualFile?): ScriptConfigurationSnapshot? =
getCachedConfigurationState(file)?.applied
private fun hasCachedConfiguration(file: KtFile): Boolean =
override fun hasCachedConfiguration(file: KtFile): Boolean =
getAppliedConfiguration(file.originalFile.virtualFile) != null
fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return !hasCachedConfiguration(file) && !ScriptConfigurationManager.isManualConfigurationLoading(file.originalFile.virtualFile)
}
fun getOrLoadConfiguration(
override fun getOrLoadConfiguration(
virtualFile: VirtualFile,
preloadedKtFile: KtFile?
): ScriptCompilationConfigurationWrapper? {
@@ -405,86 +326,73 @@ abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigura
if (cached != null) return cached.configuration
val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null
manager.updater.update {
rootsIndexer.transaction {
reloadOutOfDateConfiguration(ktFile, isFirstLoad = true)
}
return getAppliedConfiguration(virtualFile)?.configuration
}
/**
* Load new configuration and suggest to apply it (only if it is changed)
*/
fun ensureUpToDatedConfigurationSuggested(file: KtFile) {
reloadIfOutOfDate(file)
}
override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater {
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) {
reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false)
}
/**
* Show notification about changed script configuration with action to start loading it
*/
fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) {
reloadIfOutOfDate(file, isPostponedLoad = true)
}
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean {
return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false)
}
private fun reloadIfOutOfDate(file: KtFile, isPostponedLoad: Boolean = false) {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return
manager.updater.update {
val virtualFile = file.originalFile.virtualFile
if (virtualFile != null) {
val state = cache[virtualFile]
if (state == null || !state.isUpToDate(project, virtualFile, file)) {
reloadOutOfDateConfiguration(
file,
isFirstLoad = state == null,
isPostponedLoad = isPostponedLoad
)
}
}
override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) {
reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true)
}
}
/**
* Ensure that any configuration for [files] is loaded from cache
*/
fun ensureLoadedFromCache(files: List<KtFile>): Boolean {
private fun reloadIfOutOfDate(files: List<KtFile>, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: Boolean): Boolean {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
var allLoaded = true
manager.updater.update {
var upToDate = true
rootsIndexer.transaction {
files.forEach { file ->
val virtualFile = file.originalFile.virtualFile
if (virtualFile != null) {
val state = cache[virtualFile]
if (state == null) {
if (!reloadOutOfDateConfiguration(
file,
isFirstLoad = true,
fromCacheOnly = true
)
) {
allLoaded = false
}
if (state == null || !state.isUpToDate(project, virtualFile, file)) {
upToDate = false
reloadOutOfDateConfiguration(
file,
isFirstLoad = state == null,
loadEvenWillNotBeApplied = loadEvenWillNotBeApplied,
isPostponedLoad = isPostponedLoad
)
}
}
}
}
return allLoaded
return upToDate
}
protected open fun setAppliedConfiguration(
file: VirtualFile,
newConfigurationSnapshot: ScriptConfigurationSnapshot?,
syncUpdate: Boolean = false
newConfigurationSnapshot: ScriptConfigurationSnapshot?
) {
manager.updater.checkInTransaction()
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)
manager.updater.invalidate(file, synchronous = syncUpdate)
clearClassRootsCaches(project)
}
ScriptingSupportHelper.updateHighlighting(project) {
it == file
}
}
@@ -495,6 +403,10 @@ abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigura
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
@@ -504,29 +416,20 @@ abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigura
val virtualFile = file.virtualFile
if (cache[virtualFile]?.isUpToDate(project, virtualFile, file) == true) return
manager.updater.update {
reloadOutOfDateConfiguration(file, forceSync = true)
rootsIndexer.transaction {
reloadOutOfDateConfiguration(file, forceSync = true, loadEvenWillNotBeApplied = true)
}
}
fun updateScriptDefinitions() {
override fun clearCaches() {
cache.clear()
}
fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
// todo: drop the hell below
// keep this one only:
// cache.allApplied().forEach { (vFile, configuration) -> builder.add(vFile, configuration) }
// own builder for saving to storage
val ownBuilder = ScriptClassRootsCache.Builder(project)
val rootsStorage = ScriptClassRootsStorage.getInstance(project)
rootsStorage.load(ownBuilder)
cache.allApplied().forEach { (vFile, configuration) -> ownBuilder.add(vFile, configuration) }
rootsStorage.save(ownBuilder)
builder.add(ownBuilder)
override fun recreateRootsCache(): ScriptClassRootsCache {
return DefaultClassRootsCache(
project,
cache.allApplied()
)
}
}
@@ -6,37 +6,76 @@
package org.jetbrains.kotlin.idea.core.script.configuration
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder
import org.jetbrains.kotlin.idea.core.script.KotlinScriptDependenciesClassFinder
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
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.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Extension point for overriding default Kotlin scripting support.
*
* Implementation should store script configuration internally (in memory and/or fs),
* and provide it inside [collectConfigurations] using the [ScriptClassRootsCache.LightScriptInfo].
* Custom data can be attached to [ScriptClassRootsCache.LightScriptInfo] and retrieved
* by calling [ScriptClassRootsCache.getLightScriptInfo].
*
* [ScriptChangeListener] can be used to listen script changes.
* [CompositeScriptConfigurationManager.updater] should be used to schedule configuration reloading.
*
* [isApplicable] should return true for files that is covered by that support.
*
* [isConfigurationLoadingInProgress] is used to pause analyzing.
*
* Long read: [idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/README.md].
*
* @sample GradleBuildRootsManager
*/
abstract class ScriptingSupport {
abstract fun isApplicable(file: VirtualFile): Boolean
abstract fun isConfigurationLoadingInProgress(file: KtFile): Boolean
abstract fun collectConfigurations(builder: ScriptClassRootsCache.Builder)
abstract class Provider {
abstract val all: Collection<ScriptingSupport>
companion object {
val EPN: ExtensionPointName<ScriptingSupport> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.scriptingSupport")
abstract fun getSupport(file: VirtualFile): ScriptingSupport?
companion object {
val EPN: ExtensionPointName<Provider> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.scriptingSupportProvider")
}
}
abstract fun clearCaches()
abstract fun hasCachedConfiguration(file: KtFile): Boolean
abstract fun isConfigurationLoadingInProgress(file: KtFile): Boolean
abstract fun getOrLoadConfiguration(virtualFile: VirtualFile, preloadedKtFile: KtFile? = null): ScriptCompilationConfigurationWrapper?
abstract val updater: ScriptConfigurationUpdater
private val classpathRootsLock = ReentrantLock()
@Volatile
private var _classpathRoots: ScriptClassRootsCache? = null
val classpathRoots: ScriptClassRootsCache
get() {
val value1 = _classpathRoots
if (value1 != null) return value1
classpathRootsLock.withLock {
val value2 = _classpathRoots
if (value2 != null) return value2
val value3 = recreateRootsCache()
value3.saveClassRootsToStorage()
_classpathRoots = value3
return value3
}
}
protected abstract fun recreateRootsCache(): ScriptClassRootsCache
fun clearClassRootsCaches(project: Project) {
debug { "class roots caches cleared" }
classpathRootsLock.withLock {
_classpathRoots = null
}
val kotlinScriptDependenciesClassFinder =
Extensions.getArea(project)
.getExtensionPoint(PsiElementFinder.EP_NAME).extensions
.filterIsInstance<KotlinScriptDependenciesClassFinder>()
.single()
kotlinScriptDependenciesClassFinder.clearCache()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
@@ -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)
}
}
}
}
}
@@ -31,7 +31,7 @@ interface ScriptConfigurationCache {
fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun setLoaded(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun allApplied(): List<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>
fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper>
fun clear()
fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper?
@@ -63,10 +63,6 @@ interface CachedConfigurationInputs: Serializable {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean = false
}
object UpToDate: CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean = true
}
data class PsiModificationStamp(
val fileModificationStamp: Long,
val psiModificationStamp: Long
@@ -41,11 +41,11 @@ open class ScriptConfigurationMemoryCache(
@Synchronized
@Suppress("UNCHECKED_CAST")
override fun allApplied(): List<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>> {
val result = mutableListOf<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>()
override fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper> {
val result = hashMapOf<VirtualFile, ScriptCompilationConfigurationWrapper>()
for ((file, configuration) in memoryCache.entrySet()) {
if (configuration.applied?.configuration != null) {
result.add(Pair(file, configuration.applied.configuration))
result[file] = configuration.applied.configuration
}
}
return result
@@ -9,14 +9,14 @@ import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
open class DefaultScriptChangeListener(project: Project) : ScriptChangeListener(project) {
override fun editorActivated(vFile: VirtualFile) {
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile) ?: return
default.ensureUpToDatedConfigurationSuggested(file)
updater.ensureUpToDatedConfigurationSuggested(file)
}
override fun documentChanged(vFile: VirtualFile) {
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile) ?: return
default.ensureUpToDatedConfigurationSuggested(file)
updater.ensureUpToDatedConfigurationSuggested(file)
}
override fun isApplicable(vFile: VirtualFile): Boolean {
@@ -9,21 +9,20 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile
/**
* [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed.
* Listener should do something to invalidate configuration and schedule reloading.
*
* Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
*
* @see DefaultScriptConfigurationManager for more details.
*/
abstract class ScriptChangeListener(val project: Project) {
val default: DefaultScriptingSupport
get() = DefaultScriptingSupport.getInstance(project)
abstract fun editorActivated(vFile: VirtualFile)
abstract fun documentChanged(vFile: VirtualFile)
abstract class ScriptChangeListener(protected val project: Project) {
abstract fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater)
abstract fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater)
abstract fun isApplicable(vFile: VirtualFile): Boolean
@@ -22,7 +22,8 @@ import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChange
import org.jetbrains.kotlin.idea.core.script.isScriptChangesNotifierDisabled
internal class ScriptChangesNotifier(
private val project: Project
private val project: Project,
private val updater: ScriptConfigurationUpdater,
) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project)
private val scriptChangesListenerDelay = 1400
@@ -45,10 +46,10 @@ internal class ScriptChangesNotifier(
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
if (ApplicationManager.getApplication().isUnitTestMode) {
getListener(project, file)?.editorActivated(file)
getListener(project, file)?.editorActivated(file, updater)
} else {
AppExecutorUtil.getAppExecutorService().submit {
getListener(project, file)?.editorActivated(file)
getListener(project, file)?.editorActivated(file, updater)
}
}
}
@@ -67,11 +68,11 @@ internal class ScriptChangesNotifier(
}
if (ApplicationManager.getApplication().isUnitTestMode) {
getListener(project, file)?.documentChanged(file)
getListener(project, file)?.documentChanged(file, updater)
} else {
scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest(
{ getListener(project, file)?.documentChanged(file) },
{ getListener(project, file)?.documentChanged(file, updater) },
scriptChangesListenerDelay,
true,
)
@@ -83,10 +84,11 @@ internal class ScriptChangesNotifier(
}
private val defaultListener = DefaultScriptChangeListener(project)
private val listeners: Collection<ScriptChangeListener> = mutableListOf<ScriptChangeListener>().apply {
addAll(LISTENER.getPoint(project).extensionList)
add(defaultListener)
}
private val listeners: Sequence<ScriptChangeListener>
get() = sequence {
yieldAll(LISTENER.getPoint(project).extensionList)
yield(defaultListener)
}
private fun getListener(project: Project, file: VirtualFile): ScriptChangeListener? {
if (project.isDisposed || areListenersDisabled()) return null
@@ -0,0 +1,31 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.configuration.listener
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCacheScope
import org.jetbrains.kotlin.psi.KtFile
interface ScriptConfigurationUpdater {
/**
* Ensure that configuration for [file] is up-to-date,
* or new configuration loading is started
*/
fun ensureUpToDatedConfigurationSuggested(file: KtFile)
/**
* Check that configurations for given [files] is up-to-date or try synchronously update
* them. Synchronous update will be occurred only if auto apply is enabled and
* configuration for given file will not be loaded in background.
*
* @return true, if all files are already up-to-date or updated during this call
*/
fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean
/**
* Suggest to update script configuration if configuration for [file] is out of date
*/
fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile)
}
@@ -15,7 +15,6 @@ import com.intellij.util.containers.HashSetQueue
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle
import java.util.*
import javax.swing.SwingUtilities
/**
* Sequentially loads script configuration in background.
@@ -33,7 +32,7 @@ import javax.swing.SwingUtilities
*/
internal class DefaultBackgroundExecutor(
val project: Project,
val rootsManager: ScriptClassRootsUpdater
val rootsManager: ScriptClassRootsIndexer
) : BackgroundExecutor {
companion object {
const val PROGRESS_INDICATOR_DELAY = 1000
@@ -138,7 +137,7 @@ internal class DefaultBackgroundExecutor(
private fun ensureInTransaction() {
if (inTransaction) return
inTransaction = true
rootsManager.beginUpdating()
rootsManager.startTransaction()
}
@Synchronized
@@ -228,12 +227,9 @@ internal class DefaultBackgroundExecutor(
override fun start() {
super.start()
// executeOnPooledThread requires read lock, and we may fail to acquire it
SwingUtilities.invokeLater {
BackgroundTaskUtil.executeOnPooledThread(project, {
run()
})
}
BackgroundTaskUtil.executeOnPooledThread(project, Runnable {
run()
})
}
override fun close() {
@@ -0,0 +1,86 @@
/*
* 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,
ScriptClassRootsStorage.Companion.Key("default"),
extractRoots(all, project)
) {
override fun getConfiguration(file: VirtualFile) = all[file]
override fun contains(file: VirtualFile): Boolean = file in all
private val scriptsSdksCache: Map<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
return@createWeakMap getScriptSdkOrDefault(all[file]?.javaHome, project)
}
override fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file]
override val firstScriptSdk: Sdk? by lazy {
getScriptSdkOrDefault(all.values.firstOrNull()?.javaHome, project)
}
companion object {
fun extractRoots(
project: Project,
configuration: ScriptCompilationConfigurationWrapper
): ScriptClassRootsStorage.Companion.ScriptClassRoots {
val scriptSdk = getScriptSdkOrDefault(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 = getScriptSdkOrDefault(configuration.javaHome, project)
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
)
}
}
}
@@ -12,101 +12,100 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.io.FileUtil.toSystemIndependentName
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.*
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.NonClasspathDirectoriesScope.compose
import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.LOG
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.classpathEntryToVfs
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.toVfsRoots
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File
import java.lang.ref.Reference
import java.lang.ref.SoftReference
class ScriptClassRootsCache(
val scripts: Map<String, LightScriptInfo>,
val classes: Set<String>,
val sources: Set<String>,
val sdks: Map<String?, Sdk?>,
private val nonModulesSdks: Collection<Sdk>,
val customDefinitionsUsed: Boolean
abstract class ScriptClassRootsCache(
private val project: Project,
private val rootsCacheKey: ScriptClassRootsStorage.Companion.Key,
private val roots: ScriptClassRootsStorage.Companion.ScriptClassRoots
) {
abstract class LightScriptInfo(val definition: ScriptDefinition?) {
@Volatile
var heavyCache: Reference<HeavyScriptInfo>? = null
protected abstract fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper?
abstract fun buildConfiguration(): ScriptCompilationConfigurationWrapper?
}
abstract val firstScriptSdk: Sdk?
class DirectScriptInfo(val result: ScriptCompilationConfigurationWrapper) : LightScriptInfo(null) {
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper = result
}
abstract fun getScriptSdk(file: VirtualFile): Sdk?
class HeavyScriptInfo(
abstract fun contains(file: VirtualFile): Boolean
private class Fat(
val scriptConfiguration: ScriptCompilationConfigurationWrapper,
val classFilesScope: GlobalSearchScope,
val sdk: Sdk?
val classFilesScope: GlobalSearchScope
)
class Builder(val project: Project) {
val scripts = mutableMapOf<String, LightScriptInfo>()
val allDependenciesClassFiles by lazy {
ScriptClassRootsStorage.getInstance(project).loadClasspathRoots(rootsCacheKey)
}
private val defaultSdk = getScriptDefaultSdk()
val sdks = mutableMapOf<String?, Sdk?>(null to defaultSdk)
val allDependenciesSources by lazy {
ScriptClassRootsStorage.getInstance(project).loadSourcesRoots(rootsCacheKey)
}
val classes = mutableSetOf<String>()
val sources = mutableSetOf<String>()
private var customDefinitionsUsed: Boolean = false
val allDependenciesClassFilesScope by lazy {
NonClasspathDirectoriesScope.compose(allDependenciesClassFiles)
}
fun build(): ScriptClassRootsCache {
val nonIndexedSdks = collectNonIndexedSdks()
return ScriptClassRootsCache(scripts, classes, sources, sdks, nonIndexedSdks, customDefinitionsUsed)
}
val allDependenciesSourcesScope by lazy {
NonClasspathDirectoriesScope.compose(allDependenciesSources)
}
fun collectNonIndexedSdks(): MutableSet<Sdk> {
val nonIndexedSdks = sdks.values.filterNotNullTo(mutableSetOf())
ModuleManager.getInstance(project).modules.map {
nonIndexedSdks.remove(ModuleRootManager.getInstance(it).sdk)
private val scriptsDependenciesCache: MutableMap<VirtualFile, Fat> =
ConcurrentFactoryMap.createWeakMap { file ->
val configuration = getConfiguration(file) ?: return@createWeakMap null
val roots = configuration.dependenciesClassPath
val sdk = getScriptSdk(file)
@Suppress("FoldInitializerAndIfToElvis")
if (sdk == null) {
return@createWeakMap Fat(
configuration,
NonClasspathDirectoriesScope.compose(ScriptConfigurationManager.toVfsRoots(roots))
)
}
return nonIndexedSdks
return@createWeakMap Fat(
configuration,
NonClasspathDirectoriesScope.compose(
sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList() + ScriptConfigurationManager.toVfsRoots(roots)
)
)
}
fun useCustomScriptDefinition() {
customDefinitionsUsed = true
fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope {
return scriptsDependenciesCache[file]?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE
}
fun getScriptConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? {
return scriptsDependenciesCache[file]?.scriptConfiguration
}
fun hasNotCachedRoots(roots: ScriptClassRootsStorage.Companion.ScriptClassRoots): Boolean {
return !ScriptClassRootsStorage.getInstance(project).containsAll(rootsCacheKey, roots)
}
fun saveClassRootsToStorage() {
val rootsStorage = ScriptClassRootsStorage.getInstance(project)
if (roots.classpathFiles.isNotEmpty() || roots.sourcesFiles.isNotEmpty() || roots.sdks.isNotEmpty()) {
rootsStorage.save(rootsCacheKey, roots)
}
}
companion object {
fun toStringValues(prop: Collection<File>): Set<String> {
return prop.mapNotNull { it.absolutePath }.toSet()
}
fun add(
vFile: VirtualFile,
configuration: ScriptCompilationConfigurationWrapper
) {
addSdk(configuration.javaHome)
configuration.dependenciesClassPath.forEach { classes.add(it.absolutePath) }
configuration.dependenciesSources.forEach { sources.add(it.absolutePath) }
scripts[vFile.path] = DirectScriptInfo(configuration)
useCustomScriptDefinition()
}
fun add(other: Builder) {
classes.addAll(other.classes)
sources.addAll(other.sources)
sdks.putAll(other.sdks)
scripts.putAll(other.scripts)
}
fun addSdk(javaHome: File?): Sdk? {
if (javaHome == null) return defaultSdk
val canonicalPath = toSystemIndependentName(javaHome.canonicalPath)
return sdks.getOrPut(canonicalPath) {
getScriptSdkByJavaHome(javaHome) ?: defaultSdk
}
fun getScriptSdkOrDefault(javaHome: File?, project: Project): Sdk? {
return javaHome?.let { getScriptSdkByJavaHome(it) } ?: getScriptDefaultSdk(project)
}
private fun getScriptSdkByJavaHome(javaHome: File): Sdk? {
@@ -120,13 +119,7 @@ class ScriptClassRootsCache(
return getProjectJdkTableSafe().allJdks.find { it.homeDirectory == javaHomeVF }
}
fun addSdkByName(sdkName: String) {
val sdk = getProjectJdkTableSafe().allJdks.find { it.name == sdkName } ?: defaultSdk ?: return
val homePath = sdk.homePath ?: return
sdks[homePath] = sdk
}
private fun getScriptDefaultSdk(): Sdk? {
private fun getScriptDefaultSdk(project: Project): Sdk? {
val projectSdk = ProjectRootManager.getInstance(project).projectSdk?.takeIf { it.canBeUsedForScript() }
if (projectSdk != null) return projectSdk
@@ -140,118 +133,32 @@ class ScriptClassRootsCache(
"projectSdk = ${ProjectRootManager.getInstance(project).projectSdk}, " +
"all sdks = ${getAllProjectSdks().joinToString("\n")}"
)
return null
}
private fun Sdk.canBeUsedForScript() = sdkType is JavaSdkType
}
fun getLightScriptInfo(file: String) = scripts[file]
fun contains(file: VirtualFile): Boolean = file.path in scripts
private fun getHeavyScriptInfo(file: String): HeavyScriptInfo? {
val lightScriptInfo = getLightScriptInfo(file) ?: return null
val heavy0 = lightScriptInfo.heavyCache?.get()
if (heavy0 != null) return heavy0
synchronized(lightScriptInfo) {
val heavy1 = lightScriptInfo.heavyCache?.get()
if (heavy1 != null) return heavy1
val heavy2 = computeHeavy(lightScriptInfo)
lightScriptInfo.heavyCache = SoftReference(heavy2)
return heavy2
}
}
private fun computeHeavy(lightScriptInfo: LightScriptInfo): HeavyScriptInfo? {
val configuration = lightScriptInfo.buildConfiguration() ?: return null
val roots = configuration.dependenciesClassPath
val sdk = sdks[configuration.javaHome?.canonicalPath]
return if (sdk == null) {
HeavyScriptInfo(configuration, compose(toVfsRoots(roots)), null)
} else {
val sdkClasses = sdk.rootProvider.getFiles(OrderRootType.CLASSES).toList()
HeavyScriptInfo(configuration, compose(sdkClasses + toVfsRoots(roots)), sdk)
}
}
val firstScriptSdk: Sdk? = sdks.values.firstOrNull()
val allDependenciesClassFiles: List<VirtualFile>
val allDependenciesSources: List<VirtualFile>
init {
allDependenciesClassFiles = mutableSetOf<VirtualFile>().also { result ->
nonModulesSdks.forEach { result.addAll(it.rootProvider.getFiles(OrderRootType.CLASSES)) }
classes.mapNotNullTo(result) { classpathEntryToVfs(File(it)) }
}.toList()
allDependenciesSources = mutableSetOf<VirtualFile>().also { result ->
nonModulesSdks.forEach { result.addAll(it.rootProvider.getFiles(OrderRootType.SOURCES)) }
sources.mapNotNullTo(result) { classpathEntryToVfs(File(it)) }
}.toList()
}
val allDependenciesClassFilesScope = compose(allDependenciesClassFiles)
val allDependenciesSourcesScope = compose(allDependenciesSources)
fun getScriptConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? =
getHeavyScriptInfo(file.path)?.scriptConfiguration
fun getScriptSdk(file: VirtualFile): Sdk? =
getHeavyScriptInfo(file.path)?.sdk
fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope =
getHeavyScriptInfo(file.path)?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE
fun hasInvalidSdk(project: Project): Boolean {
val builder = Builder(project)
if (sdks.any { (home, sdk) -> builder.addSdk(home?.let(::File)) != sdk }) return true
if (builder.collectNonIndexedSdks() != nonModulesSdks) return true
return false
}
fun diff(old: ScriptClassRootsCache): Updates =
Updates(
hasNewRoots(old),
getChangedScripts(old)
)
private fun hasNewRoots(old: ScriptClassRootsCache): Boolean {
return classes.any { it !in old.classes }
|| sources.any { it !in old.sources }
|| sdks.any { it.key !in old.sdks }
}
private fun getChangedScripts(old: ScriptClassRootsCache): Set<String> {
val changed = mutableSetOf<String>()
scripts.forEach {
if (old.scripts[it.key] != it.value) {
changed.add(it.key)
}
fun Sdk.isAlreadyIndexed(project: Project): Boolean {
return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this }
}
old.scripts.forEach {
if (it.key !in scripts) {
changed.add(it.key)
}
fun empty(project: Project) = object : ScriptClassRootsCache(
project,
ScriptClassRootsStorage.Companion.Key("empty"),
ScriptClassRootsStorage.Companion.ScriptClassRoots(
setOf(),
setOf(),
setOf()
)
) {
override fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? = null
override val firstScriptSdk: Sdk? = null
override fun getScriptSdk(file: VirtualFile): Sdk? = null
override fun contains(file: VirtualFile): Boolean = true
}
return changed
}
class Updates(
val hasNewRoots: Boolean,
val updatedScripts: Set<String>
) {
val changed: Boolean
get() = hasNewRoots || updatedScripts.isNotEmpty()
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.atomic.AtomicInteger
/**
* Utility for postponing indexing of new roots to the end of some bulk operation.
*/
class ScriptClassRootsIndexer(val project: Project) {
private var newRootsPresent: Boolean = false
private val concurrentTransactions = AtomicInteger()
@Synchronized
fun markNewRoot() {
checkInTransaction()
newRootsPresent = true
}
fun checkInTransaction() {
check(concurrentTransactions.get() > 0)
}
inline fun <T> transaction(body: () -> T): T {
startTransaction()
return try {
body()
} finally {
commit()
}
}
fun startTransaction() {
concurrentTransactions.incrementAndGet()
}
fun commit() {
concurrentTransactions.decrementAndGet()
// run indexing even in inner transaction
// (outer transaction may be async, so it would be better to not wait it)
startIndexingIfNeeded()
}
@Synchronized
private fun startIndexingIfNeeded() {
if (!newRootsPresent) return
newRootsPresent = false
val doNotifyRootsChanged = Runnable {
runWriteAction {
if (project.isDisposed) return@runWriteAction
debug { "roots change event" }
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
TransactionGuard.submitTransaction(project, doNotifyRootsChanged)
} else {
TransactionGuard.getInstance().submitTransactionLater(project, doNotifyRootsChanged)
}
}
}
@@ -7,44 +7,144 @@ package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.JarFileSystem
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import org.jdom.Element
import org.jetbrains.kotlin.idea.core.script.debug
/**
* This cache is used by [org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport] only.
* @see org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport.collectConfigurations
*
*/
@State(
name = "ScriptClassRootsStorage",
storages = [Storage(StoragePathMacros.CACHE_FILE)]
)
class ScriptClassRootsStorage(val project: Project) : PersistentStateComponent<ScriptClassRootsStorage> {
private var classpath: Set<String> = hashSetOf()
private var sources: Set<String> = hashSetOf()
private var sdks: Set<String> = hashSetOf()
class ScriptClassRootsStorage : PersistentStateComponent<Element> {
override fun getState(): ScriptClassRootsStorage? {
return this
private var classpath: MutableMap<String, Set<String>> = hashMapOf()
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) {
XmlSerializerUtil.copyBean(state, this)
private fun storeCollection(root: Element, col: Map<String, Set<String>>, name: String) {
for ((key, paths) in col) {
for (path in paths) {
val element = Element(name)
element.setAttribute("path", path)
element.setAttribute("key", key)
root.addContent(element)
}
}
}
fun save(roots: ScriptClassRootsCache.Builder) {
classpath = roots.classes
sources = roots.sources
sdks = roots.sdks.values.mapNotNullTo(mutableSetOf()) { it?.name }
override fun loadState(state: Element) {
classpath = readCollection(state, "classpath")
sources = readCollection(state, "sources")
sdks = readCollection(state, "sdks")
}
fun load(builder: ScriptClassRootsCache.Builder) {
builder.sources.addAll(sources)
builder.classes.addAll(classpath)
sdks.forEach(builder::addSdkByName)
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> {
return sdks.map { it.name }.toSet()
}
private fun toVirtualFiles(prop: Set<String>?, sources: Boolean): List<VirtualFile> {
if (prop == null) return emptyList()
val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES
return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) }
.flatMap { it.rootProvider.getFiles(rootType).toList() }
}
private fun toVirtualFiles(prop: Collection<String>?): List<VirtualFile> {
if (prop == null) return emptyList()
return prop.mapNotNull {
if (it.endsWith(JarFileSystem.PROTOCOL)) {
StandardFileSystems.jar()?.findFileByPath(it + JarFileSystem.JAR_SEPARATOR)?.let {
return@mapNotNull it
}
}
StandardFileSystems.local()?.findFileByPath(it)?.let {
return@mapNotNull it
}
// TODO: report this somewhere, but do not throw: assert(res != null, { "Invalid classpath entry '$this': exists: ${exists()}, is directory: $isDirectory, is file: $isFile" })
null
}.distinct()
}
fun containsAll(key: Key, configuration: ScriptClassRoots): Boolean {
if (configuration.classpathFiles.isNotEmpty() && classpath[key.value]?.containsAll(configuration.classpathFiles) != true) {
debug { "class roots were changed: old = $classpath, new = ${configuration.classpathFiles}" }
return false
}
if (configuration.sourcesFiles.isNotEmpty() && sources[key.value]?.containsAll(configuration.sourcesFiles) != true) {
debug { "source roots were changed: old = $sources, new = ${configuration.sourcesFiles}" }
return false
}
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 }}" }
return false
}
return true
}
fun save(key: Key, configuration: ScriptClassRoots) {
// TODO: do not drop all storage on save: KT-34444
classpath.getOrPut(key.value, { emptySet() })
classpath.replace(key.value, configuration.classpathFiles)
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(key: Key): List<VirtualFile> {
return toVirtualFiles(sdks[key.value], false) + toVirtualFiles(classpath[key.value])
}
fun loadSourcesRoots(key: Key): List<VirtualFile> {
return toVirtualFiles(sdks[key.value], true) + toVirtualFiles(sources[key.value])
}
companion object {
fun getInstance(project: Project): ScriptClassRootsStorage =
ServiceManager.getService(project, ScriptClassRootsStorage::class.java)
data class ScriptClassRoots(
val classpathFiles: Set<String>,
val sourcesFiles: Set<String>,
val sdks: Set<Sdk>
)
val EMPTY = ScriptClassRoots(emptySet(), emptySet(), emptySet())
data class Key(val value: String)
}
}
@@ -1,194 +0,0 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.TransactionGuard
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElementFinder
import com.intellij.psi.PsiManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.KotlinScriptDependenciesClassFinder
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Utility for postponing indexing of new roots to the end of some bulk operation.
*/
class ScriptClassRootsUpdater(
val project: Project,
val manager: CompositeScriptConfigurationManager
) {
private var invalidated: Boolean = false
private var syncUpdateRequired: Boolean = false
private val concurrentUpdates = AtomicInteger()
/**
* @param synchronous Used from legacy FS cache only, don't use
*/
@Synchronized
@Suppress("UNUSED_PARAMETER")
fun invalidate(file: VirtualFile, synchronous: Boolean = false) {
// todo: record invalided files for some optimisations in update
invalidate(synchronous)
}
/**
* @param synchronous Used from legacy FS cache only, don't use
*/
@Synchronized
fun invalidate(synchronous: Boolean = false) {
checkInTransaction()
invalidated = true
if (synchronous) {
syncUpdateRequired = true
}
}
fun checkInTransaction() {
check(concurrentUpdates.get() > 0)
}
inline fun <T> update(body: () -> T): T {
beginUpdating()
return try {
body()
} finally {
commit()
}
}
fun beginUpdating() {
concurrentUpdates.incrementAndGet()
}
fun commit() {
concurrentUpdates.decrementAndGet()
// run update even in inner transaction
// (outer transaction may be async, so it would be better to not wait it)
scheduleUpdateIfInvalid()
}
@Synchronized
private fun scheduleUpdateIfInvalid() {
if (!invalidated) return
invalidated = false
if (syncUpdateRequired || ApplicationManager.getApplication().isUnitTestMode) {
syncUpdateRequired = false
updateSynchronously()
} else {
ensureUpdateScheduled()
}
}
private val syncLock = ReentrantLock()
private var scheduledUpdate: ProgressIndicator? = null
@Synchronized
fun ensureUpdateScheduled() {
scheduledUpdate?.cancel()
scheduledUpdate = BackgroundTaskUtil.executeOnPooledThread(project) {
doUpdate()
}
}
@Synchronized
private fun updateSynchronously() {
scheduledUpdate?.cancel()
doUpdate(false)
}
private fun doUpdate(underProgressManager: Boolean = true) {
syncLock.withLock {
try {
val updates = manager.collectRootsAndCheckNew()
if (!updates.changed) return
if (underProgressManager) {
ProgressManager.checkCanceled()
}
if (updates.hasNewRoots) {
notifyRootsChanged()
}
PsiElementFinder.EP.findExtensionOrFail(KotlinScriptDependenciesClassFinder::class.java, project)
.clearCache()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
if (updates.updatedScripts.isNotEmpty()) {
updateHighlighting(project) {
it.path in updates.updatedScripts
}
}
} catch (cancel: ProcessCanceledException) {
if (underProgressManager) throw cancel
} finally {
synchronized(this) {
scheduledUpdate = null
}
}
}
}
private fun notifyRootsChanged() {
val doNotifyRootsChanged = Runnable {
runWriteAction {
if (project.isDisposed) return@runWriteAction
debug { "roots change event" }
ProjectRootManagerEx.getInstanceEx(project)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
TransactionGuard.submitTransaction(project, doNotifyRootsChanged)
} else {
TransactionGuard.getInstance().submitTransactionLater(project, doNotifyRootsChanged)
}
}
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)
}
}
}
}
}
@@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.HashSetQueue
class TestingBackgroundExecutor internal constructor(
private val rootsManager: ScriptClassRootsUpdater
private val rootsManager: ScriptClassRootsIndexer
) : BackgroundExecutor {
val backgroundQueue = HashSetQueue<BackgroundTask>()
@@ -42,7 +42,7 @@ class TestingBackgroundExecutor internal constructor(
actions()
rootsManager.update {
rootsManager.transaction {
copy.forEach {
it.actions()
}
@@ -9,16 +9,12 @@ import com.intellij.openapi.externalSystem.service.project.autoimport.AsyncFileC
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
fun addVfsListener(
watcher: GradleScriptListener,
buildRootsManager: GradleBuildRootsManager
) {
fun addVfsListener(watcher: GradleScriptInputsWatcher) {
VirtualFileManager.getInstance().addAsyncFileListener(
object : AsyncFileChangeListenerBase() {
override fun isRelevant(path: String): Boolean {
return buildRootsManager.maybeAffectedGradleProjectFile(path)
return isInAffectedGradleProjectFiles(watcher.project, path)
}
override fun updateFile(file: VirtualFile, event: VFileEvent) {
@@ -9,12 +9,8 @@ import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.concurrency.SequentialTaskExecutor
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
fun addVfsListener(
watcher: GradleScriptListener,
buildRootsManager: GradleBuildRootsManager
) {
fun addVfsListener(watcher: GradleScriptInputsWatcher) {
watcher.project.messageBus.connect().subscribe(
VirtualFileManager.VFS_CHANGES,
object : BulkFileListener {
@@ -26,7 +22,7 @@ fun addVfsListener(
if (watcher.project.isDisposed) return@Runnable
for (event in events) {
val file = event.file ?: continue
if (buildRootsManager.maybeAffectedGradleProjectFile(event.path)) {
if (isInAffectedGradleProjectFiles(watcher.project, event.path)) {
watcher.fileChanged(event.path, file.timeStamp)
}
}
@@ -9,16 +9,12 @@ import com.intellij.openapi.externalSystem.autoimport.AsyncFileChangeListenerBas
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
fun addVfsListener(
watcher: GradleScriptListener,
buildRootsManager: GradleBuildRootsManager
) {
fun addVfsListener(watcher: GradleScriptInputsWatcher) {
VirtualFileManager.getInstance().addAsyncFileListener(
object : AsyncFileChangeListenerBase() {
override fun isRelevant(path: String): Boolean {
return buildRootsManager.maybeAffectedGradleProjectFile(path)
return isInAffectedGradleProjectFiles(watcher.project, path)
}
override fun updateFile(file: VirtualFile, event: VFileEvent) {
@@ -0,0 +1,37 @@
/*
* 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.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
data class ConfigurationData(
val templateClasspath: List<String>,
val models: List<KotlinDslScriptModel>
)
class Configuration(val data: ConfigurationData) {
private val scripts: Map<String, KotlinDslScriptModel>
val sourcePath: MutableSet<String>
val classFilePath: MutableSet<String> = mutableSetOf()
init {
val allModels = data.models
scripts = allModels.associateBy { it.file }
sourcePath = allModels.flatMapTo(mutableSetOf()) { it.sourcePath }
classFilePath.addAll(data.templateClasspath)
allModels.flatMapTo(classFilePath) { it.classPath }
}
fun scriptModel(file: VirtualFile): KotlinDslScriptModel? {
return scripts[FileUtil.toSystemDependentName(file.path)]
}
}
@@ -0,0 +1,88 @@
/*
* 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.VfsUtil
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.ScriptClassRootsCache.Companion.getScriptSdkOrDefault
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.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.adjustByDefinition
import java.io.File
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.jdkHome
import kotlin.script.experimental.jvm.jvm
class GradleClassRootsCache(
val project: Project,
val context: GradleKtsContext,
val configuration: Configuration
) : ScriptClassRootsCache(
project,
ScriptClassRootsStorage.Companion.Key("gradle"),
extractRoots(context, configuration, project)
) {
override fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? =
configuration.scriptModel(file)?.toScriptConfiguration()
private fun KotlinDslScriptModel.toScriptConfiguration(): ScriptCompilationConfigurationWrapper? {
val scriptFile = File(file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
val definition = virtualFile.findScriptDefinition(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)
)
}
override fun getScriptSdk(file: VirtualFile): Sdk? {
return firstScriptSdk
}
override val firstScriptSdk: Sdk? = getScriptSdkOrDefault(context.javaHome, project)
// called to ensure that configuration for file is loaded
// as we cannot force loading, we always return true
override fun contains(file: VirtualFile): Boolean = true
companion object {
fun extractRoots(context: GradleKtsContext, configuration: Configuration?, project: Project): ScriptClassRoots {
if (configuration == null) {
return ScriptClassRootsStorage.EMPTY
}
val scriptSdk = getScriptSdkOrDefault(context.javaHome, project)
if (scriptSdk != null && !scriptSdk.isAlreadyIndexed(project)) {
return ScriptClassRootsStorage.Companion.ScriptClassRoots(
configuration.classFilePath,
configuration.sourcePath,
setOf(scriptSdk)
)
}
return ScriptClassRoots(
configuration.classFilePath,
configuration.sourcePath,
getScriptSdkOrDefault(context.javaHome, project)?.let { setOf(it) } ?: setOf()
)
}
}
}
@@ -38,6 +38,7 @@ private const val kotlinDslNotificationGroupId = "Gradle Kotlin DSL Scripts"
private var Project.notificationPanel: ScriptConfigurationChangedNotification?
by UserDataProperty<Project, ScriptConfigurationChangedNotification>(Key.create("load.script.configuration.panel"))
fun showNotificationForProjectImport(project: Project) {
val existingPanel = project.notificationPanel
if (existingPanel != null) {
@@ -21,17 +21,19 @@ import com.intellij.testFramework.LightVirtualFileBase
import org.jetbrains.kotlin.idea.KotlinIcons
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModelResolver
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.plugins.gradle.service.project.GradlePartialResolverPolicy
import org.jetbrains.plugins.gradle.service.project.GradleProjectResolverExtension
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.util.function.Predicate
fun runPartialGradleImport(project: Project) {
getGradleProjectSettings(project).forEach { gradleProjectSettings ->
getGradleProjectSettings(project).forEach {
ExternalSystemUtil.refreshProject(
gradleProjectSettings.externalProjectPath,
it.externalProjectPath,
ImportSpecBuilder(project, GradleConstants.SYSTEM_ID)
.projectResolverPolicy(
GradlePartialResolverPolicy { it is KotlinDslScriptModelResolver }
GradlePartialResolverPolicy(Predicate<GradleProjectResolverExtension?> { it is KotlinDslScriptModelResolver })
)
)
}
@@ -79,6 +81,6 @@ class LoadConfigurationAction : AnAction(
if (virtualFile is LightVirtualFileBase) return false
if (virtualFile == null || !virtualFile.isValid) return false
return GradleBuildRootsManager.getInstance(project).isConfigurationOutOfDate(virtualFile)
return GradleScriptingSupportProvider.getInstance(project).isConfigurationOutOfDate(virtualFile)
}
}
@@ -5,12 +5,10 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.psi.KtFile
/**
@@ -18,27 +16,19 @@ import org.jetbrains.kotlin.psi.KtFile
* 1. It is out of date when essential [sections] are changed
* @see getGradleScriptInputsStamp
* 2. When some related file is changed (other gradle script, gradle.properties file)
* @see GradleBuildRoot.Linked.areRelatedFilesChangedBefore
* @see GradleScriptInputsWatcher.areRelatedFilesUpToDate
*
* [lastModifiedTs] is needed to check if some related file was changed since last update
* [inputsTS] is needed to check if some related file was changed since last update
*/
data class GradleKotlinScriptConfigurationInputs(
val sections: String,
val lastModifiedTs: Long,
val buildRoot: String? = null
val inputsTS: Long
) : CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean {
try {
val actualStamp = getGradleScriptInputsStamp(project, file, ktFile) ?: return false
val actualStamp = getGradleScriptInputsStamp(project, file, ktFile) ?: return false
if (actualStamp.sections != this.sections) return false
if (actualStamp.sections != this.sections) return false
return buildRoot == null ||
GradleBuildRootsManager.getInstance(project)
.getBuildRoot(buildRoot)
?.areRelatedFilesChangedBefore(file, lastModifiedTs) ?: false
} catch (cancel: ProcessCanceledException) {
return false
}
return project.service<GradleScriptInputsWatcher>().areRelatedFilesUpToDate(file, inputsTS)
}
}
@@ -0,0 +1,10 @@
/*
* 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 java.io.File
class GradleKtsContext(val javaHome: File?)
@@ -1,9 +1,9 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.legacy
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
@@ -15,20 +15,11 @@ import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigura
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp
import org.jetbrains.kotlin.idea.scripting.gradle.isGradleKotlinScript
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.useScriptConfigurationFromImportOnly
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
/**
* Loader that performs loading for .gralde.kts scripts configuration through the [DefaultScriptingSupport]
*
* TODO(gradle6): remove
*/
class GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(project: Project) : DefaultScriptConfigurationLoader(project) {
class GradleScriptConfigurationLoaderForOutOfProjectScripts(project: Project) : DefaultScriptConfigurationLoader(project) {
override fun loadDependencies(
isFirstLoad: Boolean,
ktFile: KtFile,
@@ -52,14 +43,7 @@ class GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(project: Proje
}
}
/**
* Loader that performs loading for .gralde.kts scripts configuration through the [DefaultScriptingSupport]
*
* TODO(gradle6): remove
*/
class GradleLegacyScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
private val buildRootsManager = GradleBuildRootsManager.getInstance(project)
class GradleScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean {
return if (useScriptConfigurationFromImportOnly()) false else super.shouldRunInBackground(scriptDefinition)
}
@@ -80,7 +64,7 @@ class GradleLegacyScriptConfigurationLoader(project: Project) : DefaultScriptCon
return true
}
if (!buildRootsManager.isAffectedGradleProjectFile(vFile.path)) {
if (!isInAffectedGradleProjectFiles(ktFile.project, vFile.path)) {
ScriptConfigurationManager.markFileWithManualConfigurationLoading(vFile)
return true
}
@@ -11,7 +11,6 @@ import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.util.EnvironmentUtil
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionSourceAsContributor
import org.jetbrains.kotlin.idea.core.script.ScriptDefinitionsManager
import org.jetbrains.kotlin.idea.core.script.loadDefinitionsFromTemplates
@@ -41,21 +40,6 @@ import kotlin.script.experimental.location.ScriptExpectedLocation
import kotlin.script.templates.standard.ScriptTemplateWithArgs
class GradleScriptDefinitionsContributor(private val project: Project) : ScriptDefinitionSourceAsContributor {
companion object {
fun getDefinitions(project: Project) =
ScriptDefinitionContributor.EP_NAME.getExtensions(project)
.filterIsInstance<GradleScriptDefinitionsContributor>()
.single().definitions.toList()
fun getDefinitionsTemplateClasspath(project: Project): MutableSet<String> =
mutableSetOf<String>().also { result ->
getDefinitions(project).forEach { definition ->
definition.asLegacyOrNull<KotlinScriptDefinitionFromAnnotatedTemplate>()
?.templateClasspath
?.mapNotNullTo(result) { it.path }
}
}
}
override val id: String = "Gradle Kotlin DSL"
private val failedToLoad = AtomicBoolean(false)
@@ -144,8 +128,8 @@ class GradleScriptDefinitionsContributor(private val project: Project) : ScriptD
private fun kotlinStdlibAndCompiler(gradleLibDir: File): List<File> {
// additionally need compiler jar to load gradle resolver
return gradleLibDir.listFiles { file ->
file.name.startsWith("kotlin-compiler-embeddable") || file.name.startsWith("kotlin-stdlib")
}
file.name.startsWith("kotlin-compiler-embeddable") || file.name.startsWith("kotlin-stdlib")
}
.firstOrNull()?.let(::listOf).orEmpty()
}
@@ -0,0 +1,91 @@
/*
* 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.components.*
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
@State(
name = "KotlinBuildScriptsModificationInfo",
storages = [Storage(StoragePathMacros.CACHE_FILE)]
)
class GradleScriptInputsWatcher(val project: Project) : PersistentStateComponent<GradleScriptInputsWatcher.Storage> {
private var storage = Storage()
private var cachedGradleProjectsRoots: Set<String>? = null
fun getGradleProjectsRoots(): Set<String> {
if (cachedGradleProjectsRoots == null) {
cachedGradleProjectsRoots = computeGradleProjectRoots(project)
}
return cachedGradleProjectsRoots ?: emptySet()
}
fun saveGradleProjectRootsAfterImport(roots: Set<String>) {
val oldRoots = cachedGradleProjectsRoots
if (oldRoots != null && oldRoots.isNotEmpty()) {
cachedGradleProjectsRoots = oldRoots + roots
} else {
cachedGradleProjectsRoots = roots
}
}
private fun computeGradleProjectRoots(project: Project): Set<String> {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
if (gradleSettings.getLinkedProjectsSettings().isEmpty()) return setOf()
val projectSettings = gradleSettings.getLinkedProjectsSettings().filterIsInstance<GradleProjectSettings>().firstOrNull()
?: return setOf()
return projectSettings.modules.takeIf { it.isNotEmpty() } ?: setOf(projectSettings.externalProjectPath)
}
fun startWatching() {
addVfsListener(this)
}
fun areRelatedFilesUpToDate(file: VirtualFile, timeStamp: Long): Boolean {
return storage.lastModifiedTimeStampExcept(file.path) < timeStamp
}
class Storage {
private val lastModifiedFiles = LastModifiedFiles()
fun lastModifiedTimeStampExcept(filePath: String): Long {
return lastModifiedFiles.lastModifiedTimeStampExcept(filePath)
}
fun fileChanged(filePath: String, ts: Long) {
lastModifiedFiles.fileChanged(ts, filePath)
}
}
override fun getState(): Storage {
return storage
}
override fun loadState(state: Storage) {
this.storage = state
}
fun fileChanged(filePath: String, ts: Long) {
storage.fileChanged(filePath, ts)
}
fun clearState() {
storage = Storage()
}
@TestOnly
fun clearAndRefillState() {
loadState(project.service<GradleScriptInputsWatcher>().state)
}
}
@@ -1,62 +1,44 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptListener
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
// todo(gradle6): remove
private val legacy = GradleLegacyScriptListener(project)
private val buildRootsManager = GradleBuildRootsManager.getInstance(project)
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
open class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
init {
// listen changes using VFS events, including gradle-configuration related files
addVfsListener(this, buildRootsManager)
// start GradleScriptInputsWatcher to track changes in gradle-configuration related files
project.service<GradleScriptInputsWatcher>().startWatching()
}
fun fileChanged(filePath: String, ts: Long) =
buildRootsManager.fileChanged(filePath, ts)
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
override fun isApplicable(vFile: VirtualFile) =
// todo(gradle6): replace with `isCustomScriptingSupport(vFile)`
legacy.isApplicable(vFile)
private fun isCustomScriptingSupport(vFile: VirtualFile) =
buildRootsManager.isApplicable(vFile)
override fun editorActivated(vFile: VirtualFile) {
if (isCustomScriptingSupport(vFile)) {
checkUpToDate(vFile)
if (useScriptConfigurationFromImportOnly()) {
// do nothing
} else {
legacy.editorActivated(vFile)
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
override fun documentChanged(vFile: VirtualFile) {
fileChanged(vFile.path, System.currentTimeMillis())
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
if (isCustomScriptingSupport(vFile)) {
checkUpToDate(vFile)
} else {
legacy.documentChanged(vFile)
val file = getAnalyzableKtFileForScript(vFile)
if (file != null) {
// *.gradle.kts file was changed
updater.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
private fun checkUpToDate(vFile: VirtualFile) {
val upToDate = GradleBuildRootsManager.getInstance(project)
.getScriptInfo(vFile)?.model?.inputs?.isUpToDate(project, vFile) ?: return
if (upToDate) {
hideNotificationForProjectImport(project)
} else {
showNotificationForProjectImport(project)
}
override fun isApplicable(vFile: VirtualFile): Boolean {
return isGradleKotlinScript(vFile)
}
}
@@ -0,0 +1,62 @@
/*
* 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.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
class GradleScriptingSupport(
val project: Project,
val buildRoot: VirtualFile,
val context: GradleKtsContext,
val configuration: Configuration
) : ScriptingSupport() {
override fun recreateRootsCache() = GradleClassRootsCache(project, context, configuration)
fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val scriptModel = configuration.scriptModel(file) ?: return false
return !scriptModel.inputs.isUpToDate(project, file)
}
override fun clearCaches() {
// todo: should clear up to date
}
override fun hasCachedConfiguration(file: KtFile): Boolean =
configuration.scriptModel(file.originalFile.virtualFile) != null
// TODO: can be true during import
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean = false
override fun getOrLoadConfiguration(virtualFile: VirtualFile, preloadedKtFile: KtFile?): ScriptCompilationConfigurationWrapper? {
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) {
val vFile = file.originalFile.virtualFile
if (isConfigurationOutOfDate(vFile)) {
showNotificationForProjectImport(project)
} else {
hideNotificationForProjectImport(project)
}
}
}
}
@@ -0,0 +1,208 @@
/*
* 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.application.ApplicationManager
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import org.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.KotlinDslGradleBuildSync
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 org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.settings.GradleSettingsListener
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import java.nio.file.Paths
/**
* Creates [GradleScriptingSupport] per each linked Gradle build.
* Gradle project sync should call [update].
*/
class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Provider() {
private val rootsIndexer = ScriptClassRootsIndexer(project)
val roots = RootsIndex<GradleScriptingSupport>()
override val all get() = roots.values
private val VirtualFile.localPath
get() = path
private fun findRoot(file: VirtualFile) = roots.findRoot(file.localPath)
override fun getSupport(file: VirtualFile): ScriptingSupport? {
if (isGradleKotlinScript(file)) {
findRoot(file)?.let { return it }
val externalProjectSettings = findExternalProjectSettings(file) ?: return null
if (kotlinDslScriptsModelImportSupported(getGradleVersion(project, externalProjectSettings))) {
return unlinkedFilesSupport
}
}
return null
}
private fun findExternalProjectSettings(file: VirtualFile): GradleProjectSettings? {
return getGradleProjectSettings(project)
.filter { file.localPath.startsWith(it.externalProjectPath) }
.maxBy { it.externalProjectPath.length }
}
init {
ApplicationManager.getApplication().executeOnPooledThread {
roots.update { set ->
getGradleProjectSettings(project).forEach { gradleProjectSettings ->
if (kotlinDslScriptsModelImportSupported(getGradleVersion(project, gradleProjectSettings))) {
val support = createSupport(gradleProjectSettings.externalProjectPath) {
KotlinDslScriptModels.read(it)
}
if (support != null) {
set(support.buildRoot.localPath, support)
}
}
}
}
}
// subscribe to gradle build unlink
val listener = object : GradleSettingsListenerAdapter() {
override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) {
linkedProjectPaths.forEach {
val buildRoot = VfsUtil.findFile(Paths.get(it), false)
if (buildRoot != null) {
if (roots.remove(buildRoot.localPath) != null) {
KotlinDslScriptModels.remove(buildRoot)
}
}
}
}
}
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
}
fun update(build: KotlinDslGradleBuildSync) {
// fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) {
val root = roots.findRoot(build.workingDir) ?: return
if (root.configuration.data.models.isEmpty()) return
}
val templateClasspath = findTemplateClasspath(build) ?: return
val data = ConfigurationData(templateClasspath, build.models)
val newSupport = createSupport(build.workingDir) { data } ?: return
KotlinDslScriptModels.write(newSupport.buildRoot, data)
roots[newSupport.buildRoot.localPath] = newSupport
}
private fun createSupport(
externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> ConfigurationData?
): GradleScriptingSupport? {
val gradleExeSettings =
ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>(
project,
externalProjectPath,
GradleConstants.SYSTEM_ID
)
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null
val newSupport = GradleScriptingSupport(
project,
buildRoot,
GradleKtsContext(gradleExeSettings.javaHome?.let { File(it) }),
Configuration(data)
)
val oldSupport = roots.findRoot(externalProjectPath)
if (oldSupport != null) {
rootsIndexer.transaction {
val newRoots = GradleClassRootsCache.extractRoots(newSupport.context, newSupport.configuration, project)
if (oldSupport.classpathRoots.hasNotCachedRoots(newRoots)) {
rootsIndexer.markNewRoot()
}
newSupport.clearClassRootsCaches(project)
ScriptingSupportHelper.updateHighlighting(project) {
newSupport.configuration.scriptModel(it) != null
}
}
hideNotificationForProjectImport(project)
}
return newSupport
}
private fun findTemplateClasspath(build: KotlinDslGradleBuildSync): List<String>? {
val anyScript = VfsUtil.findFile(Paths.get(build.models.first().file), true)!!
// todo: find definition according to build.workingDir
val definition = anyScript.findScriptDefinition(project) ?: return null
return definition.asLegacyOrNull<KotlinScriptDefinitionFromAnnotatedTemplate>()
?.templateClasspath?.map { it.path }
}
private val unlinkedFilesSupport = object : ScriptingSupport() {
override fun clearCaches() {
}
override fun hasCachedConfiguration(file: KtFile): Boolean = false
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean = false
override fun getOrLoadConfiguration(
virtualFile: VirtualFile,
preloadedKtFile: KtFile?
): ScriptCompilationConfigurationWrapper? = null
override val updater: ScriptConfigurationUpdater
get() = object : ScriptConfigurationUpdater {
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) {
}
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean = false
override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) {
}
}
override fun recreateRootsCache(): ScriptClassRootsCache = ScriptClassRootsCache.empty(project)
}
fun isMissingConfigurationCanBeLoadedDuringImport(file: VirtualFile): Boolean {
findRoot(file)?.let { return false }
val externalProjectSettings = findExternalProjectSettings(file) ?: return false
return kotlinDslScriptsModelImportSupported(getGradleVersion(project, externalProjectSettings))
}
// used in 201
@Suppress("UNUSED")
fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val support = findRoot(file) ?: return false
return support.isConfigurationOutOfDate(file)
}
companion object {
fun getInstance(project: Project): GradleScriptingSupportProvider =
EPN.getPoint(project).extensionList.firstIsInstance()
}
}
@@ -5,15 +5,6 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.FileAttribute
import org.jetbrains.kotlin.idea.core.util.readNullable
import org.jetbrains.kotlin.idea.core.util.readStringList
import org.jetbrains.kotlin.idea.core.util.writeNullable
import org.jetbrains.kotlin.idea.core.util.writeStringList
import java.io.DataInputStream
import java.io.DataOutput
/**
* Optimized collection for storing last modified files with ability to
* get time of last modified file expect given one ([lastModifiedTimeStampExcept]).
@@ -23,14 +14,9 @@ import java.io.DataOutput
*
* Actually works by storing two last timestamps with the set of files modified at this times.
*/
class LastModifiedFiles(
private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles(),
class LastModifiedFiles {
private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles()
private var previous: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles()
) {
init {
previous.fileIds.removeAll(last.fileIds)
if (previous.fileIds.isEmpty()) previous = SimultaneouslyChangedFiles()
}
class SimultaneouslyChangedFiles(
val ts: Long = Long.MIN_VALUE,
@@ -41,10 +27,7 @@ class LastModifiedFiles(
fun fileChanged(ts: Long, fileId: String) {
when {
ts > last.ts -> {
val prevPrev = previous
previous = last
previous.fileIds.remove(fileId)
if (previous.fileIds.isEmpty()) previous = prevPrev
last = SimultaneouslyChangedFiles(ts, hashSetOf(fileId))
}
ts == last.ts -> last.fileIds.add(fileId)
@@ -57,36 +40,4 @@ class LastModifiedFiles(
last.fileIds.size == 1 && last.fileIds.contains(fileId) -> previous.ts
else -> last.ts
}
companion object {
private val fileAttribute = FileAttribute("last-modified-files", 1, false)
fun read(buildRoot: VirtualFile): LastModifiedFiles? {
return fileAttribute.readAttribute(buildRoot)?.use {
it.readNullable {
LastModifiedFiles(readSCF(it), readSCF(it))
}
}
}
fun write(buildRoot: VirtualFile, data: LastModifiedFiles?) {
fileAttribute.writeAttribute(buildRoot).use {
it.writeNullable(data) { data ->
writeSCF(data.last)
writeSCF(data.previous)
}
}
}
fun remove(buildRoot: VirtualFile) {
write(buildRoot, null)
}
private fun readSCF(it: DataInputStream) = SimultaneouslyChangedFiles(it.readLong(), it.readStringList().toMutableSet())
private fun DataOutput.writeSCF(last: SimultaneouslyChangedFiles) {
writeLong(last.ts)
writeStringList(last.fileIds)
}
}
}
@@ -16,24 +16,18 @@ import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
class MissingGradleScriptConfigurationNotificationProvider(private val project: Project) :
EditorNotifications.Provider<EditorNotificationPanel>() {
class MissingGradleScriptConfigurationNotificationProvider(private val project: Project) : EditorNotifications.Provider<EditorNotificationPanel>() {
override fun getKey(): Key<EditorNotificationPanel> = KEY
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
if (!isGradleKotlinScript(file)) return null
if (file.fileType != KotlinFileType.INSTANCE) return null
val scriptUnderRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)
val root = scriptUnderRoot?.root
return when {
root is GradleBuildRoot.Unlinked -> EditorNotificationPanel().apply {
if (!isInAffectedGradleProjectFiles(project, file.path)) {
return EditorNotificationPanel().apply {
text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported"))
val linkProjectText = KotlinIdeaGradleBundle.message("action.label.text.load.script.configuration")
@@ -59,20 +53,22 @@ class MissingGradleScriptConfigurationNotificationProvider(private val project:
linkProjectText
)
}
root is GradleBuildRoot.New && !root.importing -> EditorNotificationPanel().apply {
}
if (GradleScriptingSupportProvider.getInstance(project).isMissingConfigurationCanBeLoadedDuringImport(file)) {
return EditorNotificationPanel().apply {
text(getMissingConfigurationNotificationText())
createActionLabel(getMissingConfigurationActionText()) {
runPartialGradleImport(project)
}
}
else -> null
}
return null
}
private val loaderForOutOfProjectScripts by lazy {
GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(
project
)
GradleScriptConfigurationLoaderForOutOfProjectScripts(project)
}
companion object {
@@ -1,75 +0,0 @@
# Kotlin Gradle build scripts support.
## Why custom Scripting support
The scripting support for Gradle becomes tricky since the default one was designed for a little bit another case: each script becomes configured individually on first opening/usage and loading supposes to be instant.
Unfortunately, this is not working well for the Gradle case: the script configuration gathering is long operation since the Gradle configuration phase required to be executed to get it. For large projects, it may take from 5 seconds for hot daemon and to minutes for initial project loading. It may download dependencies, compile and build buildSrc project and all these things will be done on first script usage, even when we need just to know is such word in reference to our symbol when we are calling "Find usages". Among that, Gradle is not able to gather individual script configuration and always provides configuration for all scripts.
Taking this in mind, it becomes better to have this strategy for Gradle scripts:
- The user performing Gradle project importing/reimporting by executing Gradle configuration phase
- We have all Gradle scripts configured
- We can save this configuration to FS cache which can be persisted between IntelliJ restarts.
## More details about what we already have
[ScriptConfigurationManager] service already existed for providing and updating scripts configuration. It was working by loading scripts configuration on the first opening, storing in-memory cache, and FS cache.
It also reloads it after each typing (with some throttling) and notifies the user if the configuration is changed with a suggestion to apply a new configuration. We may apply this configuration automatically, but it may introduce indexing which breaks UX (some action becomes unavailable during indexing). To leverage this situation, we are showing notification instead and indexing will occur only after an explicit user action. We can do this automatically if no new source and classes are introduced in dependencies, but this is not implemented yet.
Note: as we don't know in general when some script change causes script configuration change, we always run configuration reloading and comparing new configuration with the old one. If we can know that some particular change will affect script configuration, we can show notification before starting expensive loading, but this requires some knowledge of the script, so should be implemented by the script definition author. [ScriptConfigurationManager] provides ability to implement up-to-date stamp by extension point. It is implemented for the older Gradle versions, see "DefaultScriptingSupport extensions for older Gradle versions" below.
The manager also creates a list of all classes and sources of script dependencies to properly support analyzing of scripts. All these calculations are cached and invalidated on each change in script configurations.
Sometimes this loading may occur in the background, sometimes in sync. We also may apply configuration automatically in some cases (first loading, loading in sync, etc). All that introduces complex states intersection which is tricky to support. Adding another layer of complexity to that implementation is not a way to go.
## Custom scripting support
After some experiments and several iterations, the final idea is to provide the ability to create custom mechanics for managing the scripts configurations without breaking default scripting support.
As we already need a cache of all known scripts, the union of its classpath, and source path which is rebuilt from the scratch on any change, we may just provide the ability to populate this cache with custom content on each invalidation. That the main idea behind custom scripting support.
The other idea is to don't make any blocking work synchronously while getting some configuration. The cache should be always ready for unblocking reading. All updating operation is asynchronous and non-blocking thanks to copy-on-write strategy.
So, it should work in that way:
- Custom scripting support should manage the scripts configurations on its own (with own in-memory/fs cache).
- It should implement a method that will gather all script configuration providers and populate the classpath/sourcepath union that is used to get files for indexing.
- It may implement ScriptChangeListener extension point to do some invalidation on document change events.
- It should call `updater.update { ... updater.invalidate() ... }` when the new script configurations are required. The manager will schedule an asynchronous update in the background.
- It is not able to load new configurations lazily on the first request (it is still possible to calculate script configuration lazily, but file name should be already registered)
The [CompositeScriptConfigurationManager] will provide redirection of [ScriptConfigurationManager] calls to the custom [ScriptingSupport] or [DefaultScriptingSupport].
## Gradle CustomScriptingSupport
`GradleBuildRootsManager` implementing `ScriptingSupport` described above.
There are many Gradle builds can be linked with a single IntelliJ project (don't confuse with the Gradle project and included build). This complicates implementation a bit, as we should manage this builds separately: each of them may have its own script definitions, Gradle version and java home. Typically, the IntelliJ project has no more than one [GradleBuildRoot].
`GradleBuildRootsManager` actually managing linked Gradle builds. See it's KDoc for more details.
The script configuration is stored in FS by using the IntelliJ VFS file attributes.
Note that we can provide custom scripting support only for projects that using Gradle 6.0 and later, as gathering script models unavailable in older Gradle versions. `GradleBuildRootsManager` falling back to default scripting support for such linked Gradle builds. It is extended through: see "DefaultScriptingSupport extensions for older Gradle versions" for more details.
## Watching files states across IntelliJ restarts
To have consistent sate of scripts, we should also be aware of external script changes. This is achieved by watching files using the IntelliJ VFS events. [GradleScriptInputsWatcher] is responsible for that.
The first tricky part is that scripts are depending on each other: so, when one script is changed, we actually should invalidate all other scripts as we don't know dependencies between them (Gradle will provide this information later, but it is not yet implemented). Actually, we should know the last modified timestamp of all scripts excepting a particular one. This can be achieved by storing timestamps of two last modified files. [LastModifiedFiles] utility is responsible for that.
Another tricky part is that we should track only scripts belong the Gradle project and should ignore all other `*.gradle.kts` files (in `testData` for example). This is achieved by storing Gradle project roots, as scripts can be exactly near Gradle project roots (excepting included and precompiled scripts which are not fully supported yet). This can be gathered from the Gradle project import information or from GradleProjectSettings when the import has not occurred yet. `GradleBuildRootsManager` does it.
## Out of project Gradle scripts
There are scripts that are not linked to any import Gradle project. In `testData` for example. Currently, we are suggesting to import Gradle project for such scripts, but we also may implement standalone Gradle scripts supporting using the `DefaultScriptingSupport` for explicitly listed scripts. We can implement this in the future if it will be valuable for users.
## DefaultScriptingSupport extensions for older Gradle versions
For Gradle versions before 6.0, we are still falling back to the [DefaultScriptingSupport] with some extensions.
We are showing notification before loading as we knew what changes will cause it. This is done by:
- implementing `org.jetbrains.kotlin.scripting.idea.listener` extension point and calling `suggestToUpdateConfigurationIfOutOfDate` instead of `ensureUpToDatedConfigurationSuggested` on document changes.
- implementing `org.jetbrains.kotlin.scripting.idea.loader` extension point and overriding `getInputsStamp`
We are also managing out of project scripts the same way as it is done in `GradleBuildRootsManager`.
@@ -5,17 +5,19 @@
package org.jetbrains.kotlin.idea.scripting.gradle.importing
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.io.FileUtil.toSystemIndependentName
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VfsUtil
import org.gradle.tooling.model.kotlin.dsl.EditorReportSeverity
import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel
import org.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptInputsWatcher
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider
import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import java.io.File
@@ -90,19 +92,18 @@ private fun KotlinDslScriptsModel.toListOfScriptModels(project: Project): List<K
// todo(KT-34440): take inputs snapshot before starting import
KotlinDslScriptModel(
toSystemIndependentName(file.absolutePath),
getGradleScriptInputsStamp(project, virtualFile)!!, // TODO: NPE
model.classPath.map { toSystemIndependentName(it.absolutePath) },
model.sourcePath.map { toSystemIndependentName(it.absolutePath) },
file.absolutePath,
// TODO: NPE
getGradleScriptInputsStamp(project, virtualFile)!!,
model.classPath.map { it.absolutePath },
model.sourcePath.map { it.absolutePath },
model.implicitImports,
messages
)
}
class KotlinDslGradleBuildSync(val workingDir: String, val taskId: ExternalSystemTaskId) {
val projectRoots = mutableSetOf<String>()
val models = mutableListOf<KotlinDslScriptModel>()
var failed = false
}
fun saveScriptModels(project: Project, build: KotlinDslGradleBuildSync) {
@@ -112,8 +113,11 @@ fun saveScriptModels(project: Project, build: KotlinDslGradleBuildSync) {
errorReporter.reportError(File(model.file), model)
}
// todo: use real info about projects
build.projectRoots.addAll(build.models.map { toSystemIndependentName(File(it.file).parent) })
project.service<GradleScriptInputsWatcher>().saveGradleProjectRootsAfterImport(
build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }.toSet()
)
GradleBuildRootsManager.getInstance(project).update(build)
GradleScriptingSupportProvider.getInstance(project).update(build)
project.service<GradleScriptInputsWatcher>().clearState()
}
@@ -9,8 +9,6 @@ import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType
import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import java.lang.Exception
class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
override fun onStart(id: ExternalSystemTaskId, workingDir: String?) {
@@ -20,7 +18,6 @@ class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
if (workingDir == null) return
val project = id.findProject() ?: return
GradleBuildRootsManager.getInstance(project).markImportingInProgress(workingDir)
project.kotlinGradleDslSync[id] = KotlinDslGradleBuildSync(workingDir, id)
}
@@ -33,26 +30,4 @@ class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
saveScriptModels(project, sync)
}
override fun onFailure(id: ExternalSystemTaskId, e: Exception) {
if (id.type != ExternalSystemTaskType.RESOLVE_PROJECT) return
if (id.projectSystemId != GRADLE_SYSTEM_ID) return
val project = id.findProject() ?: return
val sync = project.kotlinGradleDslSync[id] ?: return
sync.failed = true
}
override fun onCancel(id: ExternalSystemTaskId) {
if (id.type != ExternalSystemTaskType.RESOLVE_PROJECT) return
if (id.projectSystemId != GRADLE_SYSTEM_ID) return
val project = id.findProject() ?: return
val cancelled = project.kotlinGradleDslSync.remove(id)
if (cancelled != null) {
GradleBuildRootsManager.getInstance(project).markImportingInProgress(cancelled.workingDir, false)
}
}
}
@@ -1,37 +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.legacy
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.scripting.gradle.isGradleKotlinScript
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
// called from GradleScriptListener
// todo(gradle6): remove
class GradleLegacyScriptListener(project: Project) : ScriptChangeListener(project) {
private val buildRootsManager = GradleBuildRootsManager.getInstance(project)
override fun isApplicable(vFile: VirtualFile) =
isGradleKotlinScript(vFile)
override fun editorActivated(vFile: VirtualFile) =
checkUpToDate(vFile)
override fun documentChanged(vFile: VirtualFile) =
checkUpToDate(vFile)
private fun checkUpToDate(vFile: VirtualFile) {
if (!buildRootsManager.isAffectedGradleProjectFile(vFile.path)) return
val file = getAnalyzableKtFileForScript(vFile)
if (file != null) {
// *.gradle.kts file was changed
default.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
}
@@ -1,153 +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.roots
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptDefinitionsContributor
import org.jetbrains.kotlin.idea.scripting.gradle.LastModifiedFiles
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import java.io.File
/**
* [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build).
* Each [GradleBuildRoot] may have it's own Gradle version, Java home and other settings.
*
* Typically, IntelliJ project have no more than one [GradleBuildRoot].
*
* See [GradleBuildRootsManager] for more details.
*/
sealed class GradleBuildRoot {
/**
* The script not related to any Gradle build that is linked to IntelliJ Project,
* or we cannot known what is it
*/
@Suppress("CanSealedSubClassBeObject")
class Unlinked : GradleBuildRoot()
/**
* Linked project, that may be itself: [Legacy], [New] or [Imported].
*/
abstract class Linked : GradleBuildRoot() {
@Volatile
var importing = false
abstract val manager: GradleBuildRootsManager
abstract val pathPrefix: String
abstract val projectRoots: Collection<String>
private val dir: VirtualFile?
get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix)
private lateinit var lastModifiedFiles: LastModifiedFiles
fun loadLastModifiedFiles() {
lastModifiedFiles = dir?.let { LastModifiedFiles.read(it) } ?: LastModifiedFiles()
}
fun saveLastModifiedFiles() {
LastModifiedFiles.write(dir ?: return, lastModifiedFiles)
}
fun areRelatedFilesChangedBefore(file: VirtualFile, lastModified: Long): Boolean =
lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified
fun fileChanged(filePath: String, ts: Long) {
lastModifiedFiles.fileChanged(ts, filePath)
manager.scheduleLastModifiedFilesSave()
}
}
abstract class WithoutScriptModels(settings: GradleProjectSettings) : Linked() {
final override val pathPrefix = settings.externalProjectPath!!
final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix)
init {
loadLastModifiedFiles()
}
}
/**
* Gradle build with old Gradle version (<6.0)
*/
class Legacy(
override val manager: GradleBuildRootsManager,
settings: GradleProjectSettings
) : WithoutScriptModels(settings) {
init {
loadLastModifiedFiles()
}
}
/**
* Linked but not yet imported Gradle build.
*/
class New(
override val manager: GradleBuildRootsManager,
settings: GradleProjectSettings
) : WithoutScriptModels(settings) {
init {
loadLastModifiedFiles()
}
}
/**
* Imported Gradle build.
* Each imported build have info about all of it's Kotlin Build Scripts.
*/
class Imported(
override val manager: GradleBuildRootsManager,
val dir: VirtualFile,
val javaHome: File?,
val data: GradleBuildRootData
) : Linked() {
val project: Project
get() = manager.project
override val pathPrefix: String = dir.path
override val projectRoots: Collection<String>
get() = data.projectRoots
init {
loadLastModifiedFiles()
}
fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
if (javaHome != null) {
builder.addSdk(javaHome)
}
val definitions = GradleScriptDefinitionsContributor.getDefinitions(project)
builder.classes.addAll(data.templateClasspath)
data.models.forEach { script ->
val definition = selectScriptDefinition(script, definitions)
builder.scripts[script.file] = GradleScriptInfo(this, definition, script)
builder.classes.addAll(script.classPath)
builder.sources.addAll(script.sourcePath)
}
}
private fun selectScriptDefinition(
script: KotlinDslScriptModel,
definitions: List<ScriptDefinition>
): ScriptDefinition? {
val file = LocalFileSystem.getInstance().findFileByPath(script.file) ?: return null
val scriptSource = VirtualFileScriptSource(file)
return definitions.firstOrNull { it.isScript(scriptSource) }
}
}
}
@@ -1,14 +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.roots
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
data class GradleBuildRootData(
val projectRoots: Collection<String>,
val templateClasspath: Collection<String>,
val models: Collection<KotlinDslScriptModel>
)
@@ -1,60 +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.roots
import com.intellij.openapi.diagnostic.logger
class GradleBuildRootIndex {
private val log = logger<GradleBuildRootIndex>()
private val byWorkingDir = HashMap<String, GradleBuildRoot.Linked>()
private val byProjectDir = HashMap<String, GradleBuildRoot.Linked>()
val list: Collection<GradleBuildRoot.Linked>
get() = byWorkingDir.values
@Synchronized
fun rebuildProjectRoots() {
byProjectDir.clear()
byWorkingDir.values.forEach { buildRoot ->
buildRoot.projectRoots.forEach {
byProjectDir[it] = buildRoot
}
}
}
@Synchronized
fun getBuildByRootDir(dir: String) = byWorkingDir[dir]
@Synchronized
fun findNearestRoot(path: String): GradleBuildRoot.Linked? {
var max: Pair<String, GradleBuildRoot.Linked>? = null
byWorkingDir.entries.forEach {
if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) {
max = it.key to it.value
}
}
return max?.second
}
@Synchronized
fun getBuildByProjectDir(projectDir: String) = byProjectDir[projectDir]
@Synchronized
fun add(value: GradleBuildRoot.Linked): GradleBuildRoot.Linked? {
val prefix = value.pathPrefix
val old = byWorkingDir.put(prefix, value)
rebuildProjectRoots()
log.info("$prefix: $old -> $value")
return old
}
@Synchronized
fun remove(prefix: String) = byWorkingDir.remove(prefix)?.also {
rebuildProjectRoots()
log.info("$prefix: removed")
}
}
@@ -1,393 +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.roots
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.scripting.gradle.*
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.kotlin.utils.addToStdlib.lastIndexOfOrNull
import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
import org.jetbrains.plugins.gradle.settings.*
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File
import java.nio.file.Paths
import java.util.concurrent.atomic.AtomicBoolean
/**
* [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build).
* Each [GradleBuildRoot] may have it's own Gradle version, Java home and other settings.
*
* Typically, IntelliJ project have no more than one [GradleBuildRoot].
*
* This manager allows to find related Gradle build by the Gradle Kotlin script file path.
* Each imported build have info about all of it's Kotlin Build Scripts.
* It is populated by calling [update], stored in FS and will be loaded from FS on next project opening
*
* [CompositeScriptConfigurationManager] may ask about known scripts by calling [collectConfigurations].
*
* It also used to show related notification and floating actions depending on root kind, state and script state itself.
*
* Roots may be:
* - [GradleBuildRoot.Unlinked] - The script not related to any Gradle build that is linked to IntelliJ Project,
* or we cannot known what is it
* - [GradleBuildRoot.Linked] - Linked project, that may be itself:
* - [GradleBuildRoot.Legacy] - Gradle build with old Gradle version (<6.0)
* - [GradleBuildRoot.New] - not yet imported
* - [GradleBuildRoot.Imported] - imported
*/
class GradleBuildRootsManager(val project: Project) : ScriptingSupport() {
private val manager: CompositeScriptConfigurationManager
get() = ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager
private val updater
get() = manager.updater
private val roots = GradleBuildRootIndex()
////////////
/// ScriptingSupport.Provider implementation:
override fun isApplicable(file: VirtualFile): Boolean {
val scriptUnderRoot = findScriptBuildRoot(file) ?: return false
if (scriptUnderRoot.root is GradleBuildRoot.Legacy) return false
return true
}
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)?.root) {
is GradleBuildRoot.Linked -> root.importing
else -> false
}
}
// used in 201
@Suppress("UNUSED")
fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val script = getScriptInfo(file) ?: return false
return !script.model.inputs.isUpToDate(project, file)
}
override fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
roots.list.forEach { root ->
if (root is GradleBuildRoot.Imported) {
root.collectConfigurations(builder)
}
}
}
//////////////////
private val VirtualFile.localPath
get() = path
fun getScriptInfo(file: VirtualFile): GradleScriptInfo? =
getScriptInfo(file.localPath)
fun getScriptInfo(localPath: String): GradleScriptInfo? =
manager.getLightScriptInfo(localPath) as? GradleScriptInfo
class ScriptUnderRoot(
val root: GradleBuildRoot?,
val script: GradleScriptInfo? = null
)
fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? =
findScriptBuildRoot(gradleKtsFile.path)
/**
* Fast method that can return false positive
*/
fun maybeAffectedGradleProjectFile(filePath: String): Boolean =
filePath.endsWith("/gradle.properties") ||
filePath.endsWith("/gradle.local") ||
filePath.endsWith("/gradle-wrapper.properties") ||
filePath.endsWith("/build.gradle.kts") ||
filePath.endsWith("/settings.gradle.kts") ||
filePath.endsWith("/init.gradle.kts")
fun isAffectedGradleProjectFile(filePath: String): Boolean =
findAffectedFileRoot(filePath) != null
fun fileChanged(filePath: String, ts: Long = System.currentTimeMillis()) {
findAffectedFileRoot(filePath)?.fileChanged(filePath, ts)
}
private fun findAffectedFileRoot(filePath: String): GradleBuildRoot.Linked? {
if (filePath.endsWith("/gradle.properties") ||
filePath.endsWith("/gradle.local")
) {
return roots.getBuildByProjectDir(filePath.substringBeforeLast("/"))
} else if (filePath.endsWith("/gradle-wrapper.properties")) {
val gradleWrapperDirIndex = filePath.lastIndexOfOrNull('/') ?: return null
val gradleDirIndex = filePath.lastIndexOfOrNull('/', gradleWrapperDirIndex - 1) ?: return null
val buildDirIndex = filePath.lastIndexOfOrNull('/', gradleDirIndex - 1) ?: return null
return roots.getBuildByRootDir(filePath.substring(0, buildDirIndex))
}
return findScriptBuildRoot(filePath, searchNearestLegacy = false)?.root as? GradleBuildRoot.Linked
}
private fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true): ScriptUnderRoot? {
if (!filePath.endsWith(".gradle.kts")) return null
val scriptInfo = getScriptInfo(filePath)
val imported = scriptInfo?.buildRoot
if (imported != null) return ScriptUnderRoot(imported, scriptInfo)
if (filePath.endsWith("/build.gradle.kts") ||
filePath.endsWith("/settings.gradle.kts") ||
filePath.endsWith("/init.gradle.kts")
) {
// build|settings|init.gradle.kts scripts should be located near gradle project root only
val gradleBuild = roots.getBuildByProjectDir(filePath.substringBeforeLast("/"))
if (gradleBuild != null) return ScriptUnderRoot(gradleBuild)
}
// other scripts: "included", "precompiled" scripts, scripts in unlinked projects,
// or just random files with ".gradle.kts" ending
// todo(gradle6): remove, it is required only for projects with old gradle
if (searchNearestLegacy) {
val found = roots.findNearestRoot(filePath)
if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found)
}
return ScriptUnderRoot(GradleBuildRoot.Unlinked())
}
fun getBuildRoot(gradleWorkingDir: String) =
roots.getBuildByRootDir(gradleWorkingDir)
fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) {
actualizeBuildRoot(workingDir)?.importing = inProgress
updateNotifications(workingDir)
hideNotificationForProjectImport(project)
}
fun update(build: KotlinDslGradleBuildSync) {
// fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) {
val root = roots.getBuildByRootDir(build.workingDir) ?: return
if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return
}
val root = actualizeBuildRoot(build.workingDir) ?: return
root.importing = false
if (root is GradleBuildRoot.Legacy) return
val templateClasspath = GradleScriptDefinitionsContributor.getDefinitionsTemplateClasspath(project)
val newData = GradleBuildRootData(build.projectRoots, templateClasspath, build.models)
val mergedData = if (build.failed && root is GradleBuildRoot.Imported) merge(root.data, newData) else newData
val newSupport = tryCreateImportedRoot(build.workingDir) { mergedData } ?: return
GradleBuildRootDataSerializer.write(newSupport.dir, mergedData)
add(newSupport)
hideNotificationForProjectImport(project)
}
private fun merge(old: GradleBuildRootData, new: GradleBuildRootData): GradleBuildRootData {
val roots = old.projectRoots.toMutableSet()
roots.addAll(new.projectRoots)
val models = old.models.associateByTo(mutableMapOf()) { it.file }
new.models.associateByTo(models) { it.file }
return GradleBuildRootData(roots, new.templateClasspath, models.values)
}
private val lastModifiedFilesSaveScheduled = AtomicBoolean()
fun scheduleLastModifiedFilesSave() {
if (lastModifiedFilesSaveScheduled.compareAndSet(false, true)) {
BackgroundTaskUtil.executeOnPooledThread(project) {
if (lastModifiedFilesSaveScheduled.compareAndSet(true, false)) {
roots.list.forEach {
it.saveLastModifiedFiles()
}
}
}
}
}
init {
getGradleProjectSettings(project).forEach {
// don't call this.add, as we are inside scripting manager initialization
roots.add(loadLinkedRoot(it))
}
// subscribe to linked gradle project modification
val listener = object : GradleSettingsListenerAdapter() {
override fun onProjectsLinked(settings: MutableCollection<GradleProjectSettings>) {
settings.forEach {
add(loadLinkedRoot(it))
}
}
override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) {
linkedProjectPaths.forEach {
remove(it)
}
}
override fun onGradleHomeChange(oldPath: String?, newPath: String?, linkedProjectPath: String) {
reloadBuildRoot(linkedProjectPath)
}
override fun onGradleDistributionTypeChange(currentValue: DistributionType?, linkedProjectPath: String) {
reloadBuildRoot(linkedProjectPath)
}
}
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
}
private fun getGradleProjectSettings(workingDir: String): GradleProjectSettings? {
return (ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings)
.getLinkedProjectSettings(workingDir)
}
/**
* Check that root under [workingDir] in sync with it's [GradleProjectSettings].
* Actually this should be true, but we may miss some change events.
* For that cases we are rechecking this on each Gradle Project sync (importing/reimporting)
*/
private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot.Linked? {
val actualSettings = getGradleProjectSettings(workingDir)
val buildRoot = roots.getBuildByRootDir(workingDir)
return when {
buildRoot != null -> when {
!buildRoot.checkActual(actualSettings) -> reloadBuildRoot(workingDir)
else -> buildRoot
}
actualSettings != null -> loadLinkedRoot(actualSettings)
else -> null
}
}
private fun GradleBuildRoot.Linked.checkActual(actualSettings: GradleProjectSettings?): Boolean {
if (actualSettings == null) return false
val knownAsSupported = this !is GradleBuildRoot.Legacy
val shouldBeSupported = kotlinDslScriptsModelImportSupported(actualSettings.resolveGradleVersion().version)
return knownAsSupported == shouldBeSupported
}
private fun reloadBuildRoot(rootPath: String): GradleBuildRoot.Linked? {
val settings = getGradleProjectSettings(rootPath)
if (settings == null) {
remove(rootPath)
return null
} else {
val newRoot = loadLinkedRoot(settings)
add(newRoot)
return newRoot
}
}
private fun loadLinkedRoot(settings: GradleProjectSettings) =
tryLoadFromFsCache(settings) ?: createOtherLinkedRoot(settings)
private fun tryLoadFromFsCache(settings: GradleProjectSettings) =
tryCreateImportedRoot(settings.externalProjectPath) {
GradleBuildRootDataSerializer.read(it)
}
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when {
supported -> GradleBuildRoot.New(this, settings)
else -> GradleBuildRoot.Legacy(this, settings)
}
}
private fun tryCreateImportedRoot(
externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> GradleBuildRootData?
): GradleBuildRoot.Imported? {
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null
val javaHome = ExternalSystemApiUtil
.getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID)
.javaHome?.let { File(it) }
return GradleBuildRoot.Imported(this, buildRoot, javaHome, data)
}
private fun add(newRoot: GradleBuildRoot.Linked) {
val old = roots.add(newRoot)
if (old is GradleBuildRoot.Imported) removeData(old.pathPrefix)
if (old is GradleBuildRoot.Imported || newRoot is GradleBuildRoot.Imported) {
updater.ensureUpdateScheduled()
}
updateNotifications(newRoot.pathPrefix)
}
private fun remove(rootPath: String) {
val removed = roots.remove(rootPath)
if (removed is GradleBuildRoot.Imported) {
removeData(rootPath)
updater.ensureUpdateScheduled()
}
updateNotifications(rootPath)
}
private fun removeData(rootPath: String) {
val buildRoot = LocalFileSystem.getInstance().findFileByPath(rootPath)
if (buildRoot != null) {
GradleBuildRootDataSerializer.remove(buildRoot)
LastModifiedFiles.remove(buildRoot)
}
}
private fun updateNotifications(dir1: String) {
if (!project.isOpen) return
val openedScripts = FileEditorManager.getInstance(project).openFiles.filter {
it.path.startsWith(dir1) && isGradleKotlinScript(it)
}
if (openedScripts.isEmpty()) return
GlobalScope.launch(EDT(project)) {
if (project.isDisposed) return@launch
openedScripts.forEach {
val ktFile = PsiManager.getInstance(project).findFile(it)
if (ktFile != null) DaemonCodeAnalyzer.getInstance(project).restart(ktFile)
EditorNotifications.getInstance(project).updateAllNotifications()
}
}
}
companion object {
fun getInstance(project: Project): GradleBuildRootsManager =
EPN.getPoint(project).extensionList.firstIsInstance()
}
}
@@ -1,66 +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.roots
import com.intellij.openapi.vfs.VfsUtil
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.kotlin.scripting.resolve.adjustByDefinition
import java.io.File
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.JvmDependency
import kotlin.script.experimental.jvm.jdkHome
import kotlin.script.experimental.jvm.jvm
class GradleScriptInfo(
val buildRoot: GradleBuildRoot.Imported,
scriptDefinition: ScriptDefinition?,
val model: KotlinDslScriptModel
) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) {
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper? {
val javaHome = buildRoot.javaHome
val scriptFile = File(model.file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
if (definition == null) return null
return ScriptCompilationConfigurationWrapper.FromCompilationConfiguration(
VirtualFileScriptSource(virtualFile),
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)
)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as GradleScriptInfo
if (buildRoot.pathPrefix != other.buildRoot.pathPrefix) return false
if (model != other.model) return false
if (definition != other.definition) return false
return true
}
override fun hashCode(): Int {
var result = buildRoot.pathPrefix.hashCode()
result = 31 * result + model.hashCode()
result = 31 * result + definition.hashCode()
return result
}
}
@@ -3,7 +3,7 @@
* 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.roots
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.FileAttribute
@@ -11,27 +11,25 @@ import org.jetbrains.kotlin.idea.core.util.readNullable
import org.jetbrains.kotlin.idea.core.util.readString
import org.jetbrains.kotlin.idea.core.util.writeNullable
import org.jetbrains.kotlin.idea.core.util.writeString
import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.LastModifiedFiles
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import java.io.DataInput
import java.io.DataInputStream
import java.io.DataOutput
internal object GradleBuildRootDataSerializer {
private val attribute = FileAttribute("kotlin-dsl-script-models", 5, false)
internal object KotlinDslScriptModels {
private val attribute = FileAttribute("kotlin-dsl-script-models", 3, false)
fun read(buildRoot: VirtualFile): GradleBuildRootData? {
fun read(buildRoot: VirtualFile): ConfigurationData? {
return attribute.readAttribute(buildRoot)?.use {
it.readNullable {
readKotlinDslScriptModels(it, buildRoot.path)
readKotlinDslScriptModels(it)
}
}
}
fun write(buildRoot: VirtualFile, data: GradleBuildRootData?) {
fun write(buildRoot: VirtualFile, configuration: ConfigurationData?) {
attribute.writeAttribute(buildRoot).use {
it.writeNullable(data) {
it.writeNullable(configuration) {
writeKotlinDslScriptModels(this, it)
}
}
@@ -39,13 +37,11 @@ internal object GradleBuildRootDataSerializer {
fun remove(buildRoot: VirtualFile) {
write(buildRoot, null)
LastModifiedFiles.remove(buildRoot)
}
}
internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleBuildRootData) {
internal fun writeKotlinDslScriptModels(output: DataOutput, data: ConfigurationData) {
val strings = StringsPool.writer(output)
strings.addStrings(data.projectRoots)
strings.addStrings(data.templateClasspath)
data.models.forEach {
strings.addString(it.file)
@@ -54,28 +50,26 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleBuildRoo
strings.addStrings(it.imports)
}
strings.writeHeader()
strings.writeStringIds(data.projectRoots)
strings.writeStringIds(data.templateClasspath)
output.writeList(data.models) {
strings.writeStringId(it.file)
output.writeString(it.inputs.sections)
output.writeLong(it.inputs.lastModifiedTs)
output.writeLong(it.inputs.inputsTS)
strings.writeStringIds(it.classPath)
strings.writeStringIds(it.sourcePath)
strings.writeStringIds(it.imports)
}
}
internal fun readKotlinDslScriptModels(input: DataInputStream, buildRoot: String): GradleBuildRootData {
internal fun readKotlinDslScriptModels(input: DataInputStream): ConfigurationData {
val strings = StringsPool.reader(input)
val projectRoots = strings.readStrings()
val templateClasspath = strings.readStrings()
val models = input.readList {
KotlinDslScriptModel(
strings.readString(),
GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong(), buildRoot),
GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong()),
strings.readStrings(),
strings.readStrings(),
strings.readStrings(),
@@ -83,7 +77,7 @@ internal fun readKotlinDslScriptModels(input: DataInputStream, buildRoot: String
)
}
return GradleBuildRootData(projectRoots, templateClasspath, models)
return ConfigurationData(templateClasspath, models)
}
private object StringsPool {
@@ -5,6 +5,8 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
@@ -12,21 +14,36 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.*
import com.intellij.psi.impl.source.tree.LeafPsiElement
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.plugins.gradle.settings.GradleLocalSettings
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.util.*
private val sections = arrayListOf("buildscript", "plugins", "initscript", "pluginManagement")
fun isGradleKotlinScript(virtualFile: VirtualFile) = virtualFile.name.endsWith(".gradle.kts")
fun isInAffectedGradleProjectFiles(project: Project, filePath: String): Boolean {
if (filePath.endsWith("/gradle.properties")) return true
if (filePath.endsWith("/gradle-wrapper.properties")) return true
if (filePath.endsWith(".gradle") || filePath.endsWith(".gradle.kts")) {
if (ApplicationManager.getApplication().isUnitTestModeWithoutAffectedGradleProjectFilesCheck) {
return true
}
return filePath.substringBeforeLast("/") in project.service<GradleScriptInputsWatcher>().getGradleProjectsRoots()
}
return false
}
fun getGradleScriptInputsStamp(
project: Project,
file: VirtualFile,
@@ -63,8 +80,7 @@ fun getGradleScriptInputsStamp(
}
}
val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot.Linked
GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp, buildRoot?.pathPrefix)
GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp)
} else null
}
}
@@ -79,5 +95,50 @@ fun useScriptConfigurationFromImportOnly(): Boolean {
return Registry.`is`("kotlin.gradle.scripts.useIdeaProjectImport", false)
}
fun getGradleProjectSettings(project: Project): Collection<GradleProjectSettings> =
(ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings).linkedProjectsSettings
fun getGradleVersion(project: Project, settings: GradleProjectSettings): String {
val localVersion = GradleLocalSettings.getInstance(project).getGradleVersion(settings.externalProjectPath)
if (localVersion != null) return localVersion
return settings.resolveGradleVersion().version
}
fun getGradleProjectSettings(project: Project): Collection<GradleProjectSettings> {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings
return gradleSettings.getLinkedProjectsSettings()
}
class RootsIndex<T : Any> {
internal val tree = TreeMap<String, T>()
var values: Collection<T> = listOf()
internal set
fun findRoot(path: String): T? {
// race condition can be ignored
val values = values
val size = values.size
if (size == 0) return null
if (size == 1) return values.single() // we can omit prefix check
return tree.floorEntry(path).takeIf { path.startsWith(it.key) }?.value
}
internal inline fun update(updater: (insert: (prefix: String, value: T) -> Unit) -> Unit) {
synchronized(this) {
updater { prefix, value -> tree[prefix] = value }
values = tree.values
}
}
@Synchronized
operator fun set(prefix: String, value: T) {
val moreCommon = tree.lowerKey(prefix)
check(moreCommon == null || !prefix.startsWith(moreCommon)) {
"Cannot add root `${prefix}`. More common root already added: `$moreCommon`"
}
tree[prefix] = value
values = tree.values
}
@Synchronized
fun remove(prefix: String) = tree.remove(prefix)
}
@@ -10,7 +10,6 @@ import junit.framework.AssertionFailedError
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.applySuggestedScriptConfiguration
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.script.configuration.utils.areSimilar
@@ -38,7 +37,7 @@ class GradleKtsImportTest : GradleImportingTestCase() {
fun data(): Collection<Array<Any?>> = listOf(arrayOf<Any?>("6.0.1"))
}
val scriptConfigurationManager get() = ScriptConfigurationManager.getInstance(myProject) as CompositeScriptConfigurationManager
val scriptConfigurationManager get() = ScriptConfigurationManager.getInstance(myProject)
val projectDir get() = File(GradleSettings.getInstance(myProject).linkedProjectsSettings.first().externalProjectPath)
override fun testDataDirName(): String {
@@ -99,8 +98,8 @@ class GradleKtsImportTest : GradleImportingTestCase() {
// reload configuration and check this it is not changed
scripts.forEach {
val reloadedConfiguration = scriptConfigurationManager.default.forceReloadConfiguration(
it.psiFile,
val reloadedConfiguration = scriptConfigurationManager.forceReloadConfiguration(
it.virtualFile,
object : DefaultScriptConfigurationLoader(it.psiFile.project) {
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition) = false
override fun loadDependencies(
@@ -5,24 +5,19 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.components.service
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationLoadingTest
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners
import org.jetbrains.plugins.gradle.settings.DistributionType
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.jetbrains.plugins.gradle.util.GradleConstants
import org.junit.runner.RunWith
import java.io.File
@RunWith(JUnit3RunnerWithInners::class)
open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
@RunWith(JUnit3WithIdeaConfigurationRunner::class)
open class GradleScriptInputsWatcherTest : AbstractScriptConfigurationLoadingTest() {
companion object {
internal const val outsidePlaceholder = "// OUTSIDE_SECTIONS"
internal const val insidePlaceholder = "// INSIDE_SECTIONS"
@@ -30,12 +25,14 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
private lateinit var testFiles: TestFiles
data class TestFiles(
val buildKts: KtFile,
val settings: KtFile,
val prop: PsiFile,
val gradleWrapperProperties: VirtualFile
)
data class TestFiles(val buildKts: KtFile, val settings: KtFile, val prop: PsiFile)
override fun setUp() {
super.setUp()
// should be initialized explicitly because we do not have a real Gradle Project in this test
project.service<GradleScriptInputsWatcher>().startWatching()
}
override fun setUpTestProject() {
val rootDir = "idea/testData/script/definition/loading/gradle/"
@@ -43,35 +40,12 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
val settings: KtFile = addFileToProject(rootDir + GradleConstants.KOTLIN_DSL_SETTINGS_FILE_NAME)
val prop: PsiFile = addFileToProject(rootDir + "gradle.properties")
val gradleWrapperProperties = VfsUtil.virtualToIoFile(settings.virtualFile.parent)
.resolve("gradle/wrapper/gradle-wrapper.properties")
gradleWrapperProperties.parentFile.mkdirs()
gradleWrapperProperties.writeText(
"""
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-1.0.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
""".trimIndent()
)
val buildGradleKts = File(rootDir).walkTopDown().find { it.name == GradleConstants.KOTLIN_DSL_SCRIPT_NAME }
?: error("Couldn't find main script")
configureScriptFile(rootDir, buildGradleKts)
val build = (myFile as? KtFile) ?: error("")
val newProjectSettings = GradleProjectSettings()
newProjectSettings.distributionType = DistributionType.DEFAULT_WRAPPED
newProjectSettings.externalProjectPath = settings.virtualFile.parent.path
ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID).linkProject(newProjectSettings)
testFiles = TestFiles(
build,
settings,
prop,
LocalFileSystem.getInstance().findFileByIoFile(gradleWrapperProperties)!!
)
testFiles = TestFiles(build, settings, prop)
}
private inline fun <reified T : Any> addFileToProject(file: String): T {
@@ -211,6 +185,8 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
changeSettingsKtsOutsideSections()
project.service<GradleScriptInputsWatcher>().clearAndRefillState()
assertConfigurationUpToDate(testFiles.settings)
assertConfigurationUpdateWasDone(testFiles.buildKts)
}
@@ -244,7 +220,7 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
}
private fun markFileChanged(virtualFile: VirtualFile, ts: Long) {
GradleBuildRootsManager.getInstance(project).fileChanged(virtualFile.path, ts)
project.service<GradleScriptInputsWatcher>().fileChanged(virtualFile.path, ts)
}
fun testLoadedConfigurationWhenExternalFileChanged() {
@@ -256,23 +232,14 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
assertConfigurationUpToDate(testFiles.buildKts)
}
fun testChangeGradleWrapperPropertiesFile() {
assertAndLoadInitialConfiguration(testFiles.buildKts)
markFileChanged(testFiles.gradleWrapperProperties, System.currentTimeMillis())
assertConfigurationUpdateWasDone(testFiles.buildKts)
assertConfigurationUpToDate(testFiles.buildKts)
}
private fun assertConfigurationUpToDate(file: KtFile) {
scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file)
assertNoBackgroundTasks()
assertNoLoading()
}
private fun assertConfigurationUpdateWasDone(file: KtFile) {
scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file)
assertAndDoAllBackgroundTasks()
assertSingleLoading()
}
@@ -7,7 +7,6 @@ package org.jetbrains.kotlin.idea.scripting.gradle
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationLoadingTest
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.jetbrains.plugins.gradle.util.GradleConstants
@@ -35,15 +34,13 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
}
private val loaderForOutOfProjectScripts by lazy {
GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(
project
)
GradleScriptConfigurationLoaderForOutOfProjectScripts(project)
}
fun testManualLoadingForUpToDate() {
assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded()
}
@@ -56,7 +53,7 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
assertNoBackgroundTasks()
assertNoLoading()
scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded()
}
@@ -64,7 +61,7 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
fun testFileAttributesForManualLoading() {
assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded()
@@ -76,22 +73,23 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
fun testNoNotificationAfterForReload() {
assertConfigurationShouldBeLoadedManually()
scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded()
makeChangesInsideSections()
scriptConfigurationManager.default.forceReloadConfiguration(myFile as KtFile, loaderForOutOfProjectScripts)
scriptConfigurationManager.forceReloadConfiguration(myFile.virtualFile, loaderForOutOfProjectScripts)
assertConfigurationWasLoaded()
}
private fun makeChangesInsideSections() {
changeContents(myFile.text.replace(GradleScriptListenerTest.insidePlaceholder, "application"), myFile)
changeContents(myFile.text.replace(GradleScriptInputsWatcherTest.insidePlaceholder, "application"), myFile)
}
private fun assertConfigurationShouldBeLoadedManually() {
assertNull(getConfiguration())
assertAndDoAllBackgroundTasks()
assertNoLoading()
}
@@ -6,9 +6,6 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootData
import org.jetbrains.kotlin.idea.scripting.gradle.roots.readKotlinDslScriptModels
import org.jetbrains.kotlin.idea.scripting.gradle.roots.writeKotlinDslScriptModels
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -16,16 +13,15 @@ import java.io.DataInputStream
import java.io.DataOutputStream
import kotlin.test.assertEquals
class GradleBuildRootDataSerializerTest {
class KotlinDslScriptModelsTest {
@Test
fun write() {
val data = GradleBuildRootData(
val data = ConfigurationData(
listOf("a", "b", "c"),
listOf("a"),
listOf(
KotlinDslScriptModel(
"a",
GradleKotlinScriptConfigurationInputs("b", 1, "a"),
GradleKotlinScriptConfigurationInputs("b", 1),
listOf("c", "a", "b"),
listOf("b", "c", "a"),
listOf("i", "c", "b"),
@@ -33,7 +29,7 @@ class GradleBuildRootDataSerializerTest {
),
KotlinDslScriptModel(
"a",
GradleKotlinScriptConfigurationInputs("b", 1, "a"),
GradleKotlinScriptConfigurationInputs("b", 1),
listOf("c", "a", "b"),
listOf("b", "c", "a"),
listOf("i", "c", "b"),
@@ -45,7 +41,7 @@ class GradleBuildRootDataSerializerTest {
val buffer = ByteArrayOutputStream()
writeKotlinDslScriptModels(DataOutputStream(buffer), data)
val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray())), "a")
val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray())))
assertEquals(data.toString(), restored.toString())
}
@@ -72,81 +72,4 @@ class LastModifiedFilesTest {
assertEquals(3, files.lastModifiedTimeStampExcept("a"))
assertEquals(3, files.lastModifiedTimeStampExcept("b"))
}
@Test
fun testSameFile() {
files.fileChanged(1, "x")
files.fileChanged(2, "x")
assertEquals(Long.MIN_VALUE, files.lastModifiedTimeStampExcept("x"))
}
@Test
fun testSameFile1() {
files.fileChanged(1, "z")
files.fileChanged(2, "x")
files.fileChanged(3, "x")
assertEquals(1, files.lastModifiedTimeStampExcept("x"))
assertEquals(3, files.lastModifiedTimeStampExcept("z"))
}
@Test
fun testSameFile2() {
files.fileChanged(1, "z")
files.fileChanged(1, "x")
files.fileChanged(2, "x")
assertEquals(1, files.lastModifiedTimeStampExcept("x"))
assertEquals(2, files.lastModifiedTimeStampExcept("z"))
}
@Test
fun testSameFile3() {
files.fileChanged(1, "x")
files.fileChanged(1, "y")
files.fileChanged(2, "x")
assertEquals(1, files.lastModifiedTimeStampExcept("x"))
assertEquals(2, files.lastModifiedTimeStampExcept("y"))
}
@Test
fun testSameFile4() {
files.fileChanged(1, "z")
files.fileChanged(2, "x")
files.fileChanged(2, "y")
files.fileChanged(3, "x")
assertEquals(2, files.lastModifiedTimeStampExcept("x"))
assertEquals(3, files.lastModifiedTimeStampExcept("y"))
assertEquals(3, files.lastModifiedTimeStampExcept("z"))
}
@Test
fun testLoadingAfterSameFileBug() {
val restored = LastModifiedFiles(
LastModifiedFiles.SimultaneouslyChangedFiles(2, mutableSetOf("x")),
LastModifiedFiles.SimultaneouslyChangedFiles(1, mutableSetOf("x"))
)
assertEquals(Long.MIN_VALUE, restored.lastModifiedTimeStampExcept("x"))
}
@Test
fun testLoadingAfterSameFileBug2() {
val restored = LastModifiedFiles(
LastModifiedFiles.SimultaneouslyChangedFiles(2, mutableSetOf("x", "y")),
LastModifiedFiles.SimultaneouslyChangedFiles(1, mutableSetOf("x"))
)
assertEquals(2, restored.lastModifiedTimeStampExcept("x"))
assertEquals(2, restored.lastModifiedTimeStampExcept("y"))
}
@Test
fun testLoadingAfterSameFileBug3() {
val restored = LastModifiedFiles(
LastModifiedFiles.SimultaneouslyChangedFiles(2, mutableSetOf("x", "y")),
LastModifiedFiles.SimultaneouslyChangedFiles(1, mutableSetOf("x", "z"))
)
assertEquals(2, restored.lastModifiedTimeStampExcept("x"))
assertEquals(2, restored.lastModifiedTimeStampExcept("y"))
}
}
@@ -0,0 +1,33 @@
/*
* 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.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
class RootsIndexTest {
@Test
fun findRoot() {
val roots = RootsIndex<Int>()
roots["a/b/c"] = 1
assertFails { roots["a/b/c/d"] = 2 }
roots["a/c"] = 3
roots["d"] = 4
assertEquals(1, roots.findRoot("a/b/c/x"))
assertEquals(1, roots.findRoot("a/b/c"))
assertEquals(3, roots.findRoot("a/c/x"))
assertEquals(3, roots.findRoot("a/c"))
assertEquals(4, roots.findRoot("d"))
assertEquals(4, roots.findRoot("d/x"))
assertEquals(null, roots.findRoot("e"))
assertEquals(null, roots.findRoot("e/f"))
roots["d"] = 5
assertEquals(5, roots.findRoot("d"))
}
}
+4 -4
View File
@@ -14,10 +14,10 @@
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/>
<scriptingSupport implementation="org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/>
<scriptingSupportProvider
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider"
/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
+4 -5
View File
@@ -14,11 +14,10 @@
<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.legacy.GradleLegacyScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/>
<scriptingSupport implementation="org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/>
<scriptingSupportProvider
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider"
/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
@@ -1,7 +1,7 @@
<idea-plugin>
<extensionPoints>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.scriptingSupport"
interface="org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport"
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.scriptingSupportProvider"
interface="org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport$Provider"
area="IDEA_PROJECT"/>
</extensionPoints>
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.inspections
import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInsight.daemon.QuickFixBundle
import com.intellij.codeInsight.daemon.impl.HighlightInfoType
import com.intellij.codeInsight.daemon.impl.analysis.HighlightUtil
import com.intellij.codeInsight.daemon.impl.analysis.JavaHighlightUtil
import com.intellij.codeInspection.*
@@ -44,7 +45,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.findModuleDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.core.isInheritable
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.toDescriptor
import org.jetbrains.kotlin.idea.findUsages.KotlinFindUsagesHandlerFactory
import org.jetbrains.kotlin.idea.findUsages.handlers.KotlinFindClassUsagesHandler
@@ -137,8 +138,7 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
val usedScripts = findScriptsWithUsages(declaration)
if (usedScripts.isNotEmpty()) {
if (!DefaultScriptingSupport.getInstance(declaration.project).ensureLoadedFromCache(usedScripts)) {
// Not all script configuration are loaded; behave like it is used
if (!ScriptConfigurationManager.getInstance(declaration.project).updater.ensureConfigurationUpToDate(usedScripts)) {
return TOO_MANY_OCCURRENCES
}
}
@@ -13,7 +13,6 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions
import org.jetbrains.kotlin.idea.core.script.configuration.loader.FileContentsDependentConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.testingBackgroundExecutor
@@ -22,7 +21,7 @@ import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile
abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurationTest() {
lateinit var scriptConfigurationManager: CompositeScriptConfigurationManager
lateinit var scriptConfigurationManager: ScriptConfigurationManager
companion object {
private var occurredLoadings = 0
@@ -42,7 +41,7 @@ abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurat
testScriptConfigurationNotification = true
ApplicationManager.getApplication().isScriptChangesNotifierDisabled = false
scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java) as CompositeScriptConfigurationManager
scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java)
}
override fun tearDown() {
@@ -104,7 +103,7 @@ abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurat
protected fun makeChanges(contents: String, file: KtFile = myFile as KtFile) {
changeContents(contents)
scriptConfigurationManager.default.ensureUpToDatedConfigurationSuggested(file)
scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(file)
}
protected fun changeContents(contents: String, file: PsiFile = myFile) {
@@ -6,10 +6,10 @@
package org.jetbrains.kotlin.idea.script
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.junit.runner.RunWith
@RunWith(JUnit3RunnerWithInners::class)
@RunWith(JUnit3WithIdeaConfigurationRunner::class)
class ScriptConfigurationLoadingTest : AbstractScriptConfigurationLoadingTest() {
fun testSimple() {
assertAndLoadInitialConfiguration()
@@ -163,10 +163,10 @@ class ScriptConfigurationLoadingTest : AbstractScriptConfigurationLoadingTest()
fun testLoadingForUsagesSearch() {
assertAndLoadInitialConfiguration()
assertTrue(scriptConfigurationManager.default.ensureLoadedFromCache(listOf(myFile as KtFile)))
assertTrue(scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(myFile as KtFile)))
changeContents("A")
assertTrue(scriptConfigurationManager.default.ensureLoadedFromCache(listOf(myFile as KtFile)))
assertFalse(scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(myFile as KtFile)))
}
fun testReportsOnAutoApply() {
@@ -135,7 +135,6 @@ abstract class ScriptCompilationConfigurationWrapper(val script: SourceCode) {
override val javaHome: File?
get() = configuration?.get(ScriptCompilationConfiguration.hostConfiguration)?.get(ScriptingHostConfiguration.jvm.jdkHome)
?: configuration?.get(ScriptingHostConfiguration.jvm.jdkHome)
override val defaultImports: List<String>
get() = configuration?.get(ScriptCompilationConfiguration.defaultImports).orEmpty()