CompositeScriptConfigurationManager: unified caching

This commit is contained in:
Sergey Rostov
2020-04-25 00:37:58 +03:00
parent d21a7f54e4
commit 4e61758188
34 changed files with 899 additions and 978 deletions
@@ -27,8 +27,6 @@ import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.io.URLUtil import com.intellij.util.io.URLUtil
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager 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.KtFile
import org.jetbrains.kotlin.psi.UserDataProperty import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider import org.jetbrains.kotlin.scripting.definitions.ScriptDependenciesProvider
@@ -69,12 +67,6 @@ interface ScriptConfigurationManager {
*/ */
fun getConfiguration(file: KtFile): ScriptCompilationConfigurationWrapper? 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") @Deprecated("Use getScriptClasspath(KtFile) instead")
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> fun getScriptClasspath(file: VirtualFile): List<VirtualFile>
@@ -97,15 +89,10 @@ interface ScriptConfigurationManager {
*/ */
fun isConfigurationLoadingInProgress(file: KtFile): Boolean fun isConfigurationLoadingInProgress(file: KtFile): Boolean
/**
* See [ScriptConfigurationUpdater].
*/
val updater: ScriptConfigurationUpdater
/** /**
* Clear all caches and re-highlighting opened scripts * Clear all caches and re-highlighting opened scripts
*/ */
fun clearConfigurationCachesAndRehighlight() fun updateScriptDefinitions()
/////////////// ///////////////
// classpath roots info: // classpath roots info:
@@ -126,14 +113,14 @@ interface ScriptConfigurationManager {
ServiceManager.getService(project, ScriptConfigurationManager::class.java) ServiceManager.getService(project, ScriptConfigurationManager::class.java)
fun toVfsRoots(roots: Iterable<File>): List<VirtualFile> { fun toVfsRoots(roots: Iterable<File>): List<VirtualFile> {
return roots.mapNotNull { it.classpathEntryToVfs() } return roots.mapNotNull { classpathEntryToVfs(it) }
} }
private fun File.classpathEntryToVfs(): VirtualFile? { fun classpathEntryToVfs(file: File): VirtualFile? {
val res = when { val res = when {
!exists() -> null !file.exists() -> null
isDirectory -> StandardFileSystems.local()?.findFileByPath(this.canonicalPath) file.isDirectory -> StandardFileSystems.local()?.findFileByPath(file.canonicalPath)
isFile -> StandardFileSystems.jar()?.findFileByPath(this.canonicalPath + URLUtil.JAR_SEPARATOR) file.isFile -> StandardFileSystems.jar()?.findFileByPath(file.canonicalPath + URLUtil.JAR_SEPARATOR)
else -> null 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" }) // TODO: report this somewhere, but do not throw: assert(res != null, { "Invalid classpath entry '$this': exists: ${exists()}, is directory: $isDirectory, is file: $isFile" })
@@ -150,7 +137,7 @@ interface ScriptConfigurationManager {
@TestOnly @TestOnly
fun clearCaches(project: Project) { fun clearCaches(project: Project) {
(getInstance(project) as CompositeScriptConfigurationManager).default (getInstance(project) as CompositeScriptConfigurationManager).default
.clearCaches() .updateScriptDefinitions()
} }
fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) { fun clearManualConfigurationLoadingIfNeeded(file: VirtualFile) {
@@ -215,7 +215,7 @@ class ScriptDefinitionsManager(private val project: Project) : LazyScriptDefinit
scriptDefinitionsCacheLock.withLock { scriptDefinitionsCache.clear() } scriptDefinitionsCacheLock.withLock { scriptDefinitionsCache.clear() }
// TODO: clear by script type/definition // TODO: clear by script type/definition
ScriptConfigurationManager.getInstance(project).clearConfigurationCachesAndRehighlight() ScriptConfigurationManager.getInstance(project).updateScriptDefinitions()
} }
private fun ScriptDefinitionsSource.safeGetDefinitions(): List<ScriptDefinition> { private fun ScriptDefinitionsSource.safeGetDefinitions(): List<ScriptDefinition> {
@@ -15,69 +15,87 @@ import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.ScriptDependenciesModificationTracker 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.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.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.idea.core.script.configuration.utils.getKtFile
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult
class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigurationManager { class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigurationManager {
@Suppress("unused") @Suppress("unused")
private val notifier = ScriptChangesNotifier(project, updater) private val notifier = ScriptChangesNotifier(project)
private val providers = ScriptingSupport.Provider.EPN.getPoint(project).extensionList val updater = ScriptClassRootsUpdater(project, this)
val default = DefaultScriptingSupport(project) private val plugins = ScriptingSupport.Provider.EPN.getPoint(project).extensionList
private val managers val default = DefaultScriptingSupport(this)
get() = mutableListOf<ScriptingSupport>().also { managers ->
managers.add(default)
providers.forEach { managers.addAll(it.all) }
}
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( private fun getOrLoadConfiguration(
virtualFile: VirtualFile, virtualFile: VirtualFile,
preloadedKtFile: KtFile? = null preloadedKtFile: KtFile? = null
): ScriptCompilationConfigurationWrapper? = ): ScriptCompilationConfigurationWrapper? {
getRelatedManager(virtualFile).getOrLoadConfiguration(virtualFile, preloadedKtFile) val scriptConfiguration = classpathRoots.getScriptConfiguration(virtualFile)
if (scriptConfiguration != null) return scriptConfiguration
override fun getConfiguration(file: KtFile) = getOrLoadConfiguration(file.originalFile.virtualFile, file) // check that this script should be loaded later in special way (e.g. gradle project import)
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 hasConfiguration(file: KtFile): Boolean = override fun hasConfiguration(file: KtFile): Boolean =
getRelatedManager(file).hasCachedConfiguration(file) classpathRoots.contains(file.originalFile.virtualFile)
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean = override fun isConfigurationLoadingInProgress(file: KtFile): Boolean =
getRelatedManager(file).isConfigurationLoadingInProgress(file) plugins.firstOrNull { it.isApplicable(file.originalFile.virtualFile) }?.isConfigurationLoadingInProgress(file)
?: default.isConfigurationLoadingInProgress(file)
override val updater: ScriptConfigurationUpdater @Volatile
get() = object : ScriptConfigurationUpdater { private var classpathRoots: ScriptClassRootsCache = recreateRootsCache()
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) =
getRelatedManager(file).updater.ensureUpToDatedConfigurationSuggested(file)
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean = fun getLightScriptInfo(file: String): ScriptClassRootsCache.LightScriptInfo? =
files.groupBy { getRelatedManager(it) }.all { (manager, files) -> classpathRoots.getLightScriptInfo(file)
manager.updater.ensureConfigurationUpToDate(files)
}
override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) = private fun recreateRootsCache(): ScriptClassRootsCache {
getRelatedManager(file).updater.suggestToUpdateConfigurationIfOutOfDate(file) 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 updateScriptDefinitions() {
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
default.updateScriptDefinitions()
if (classpathRoots.customDefinitionsUsed) {
plugins.forEach {
it.updateScriptDefinitions()
}
updater.ensureUpdateScheduled()
} }
}
init { init {
val connection = project.messageBus.connect(project) val connection = project.messageBus.connect(project)
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener { connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootListener {
override fun rootsChanged(event: ModuleRootEvent) { override fun rootsChanged(event: ModuleRootEvent) {
managers.forEach { if (event.isCausedByFileTypesChange) return
it.clearClassRootsCaches(project)
if (classpathRoots.hasInvalidSdk(project)) {
updater.ensureUpdateScheduled()
} }
} }
}) })
@@ -88,49 +106,36 @@ class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigur
* Loads script configuration if classpath roots don't contain [file] yet * Loads script configuration if classpath roots don't contain [file] yet
*/ */
private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache { private fun getActualClasspathRoots(file: VirtualFile): ScriptClassRootsCache {
val manager = getRelatedManager(file) val classpathRoots = classpathRoots
val classpathRoots = manager.classpathRoots
if (classpathRoots.contains(file)) { if (classpathRoots.contains(file)) {
return classpathRoots return classpathRoots
} }
getOrLoadConfiguration(file) getOrLoadConfiguration(file)
return manager.classpathRoots return this.classpathRoots
} }
override fun getScriptSdk(file: VirtualFile): Sdk? = override fun getScriptSdk(file: VirtualFile): Sdk? =
getActualClasspathRoots(file).getScriptSdk(file) getActualClasspathRoots(file).getScriptSdk(file)
override fun getFirstScriptsSdk(): Sdk? { override fun getFirstScriptsSdk(): Sdk? =
managers.forEach { classpathRoots.firstScriptSdk
it.classpathRoots.firstScriptSdk?.let { sdk -> return sdk }
}
return null
}
override fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope = override fun getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope =
getActualClasspathRoots(file).getScriptDependenciesClassFilesScope(file) classpathRoots.getScriptDependenciesClassFilesScope(file)
override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope = override fun getAllScriptsDependenciesClassFilesScope(): GlobalSearchScope =
GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesClassFilesScope }) classpathRoots.allDependenciesClassFilesScope
override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope = override fun getAllScriptDependenciesSourcesScope(): GlobalSearchScope =
GlobalSearchScope.union(managers.map { it.classpathRoots.allDependenciesSourcesScope }) classpathRoots.allDependenciesSourcesScope
override fun getAllScriptsDependenciesClassFiles(): List<VirtualFile> = override fun getAllScriptsDependenciesClassFiles(): List<VirtualFile> =
managers.flatMap { it.classpathRoots.allDependenciesClassFiles } classpathRoots.allDependenciesClassFiles
override fun getAllScriptDependenciesSources(): List<VirtualFile> = override fun getAllScriptDependenciesSources(): List<VirtualFile> =
managers.flatMap { it.classpathRoots.allDependenciesSources } 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 // Adapters for deprecated API
@@ -144,20 +149,4 @@ class CompositeScriptConfigurationManager(val project: Project) : ScriptConfigur
override fun getScriptClasspath(file: KtFile): List<VirtualFile> = override fun getScriptClasspath(file: KtFile): List<VirtualFile> =
ScriptConfigurationManager.toVfsRoots(getConfiguration(file)?.dependenciesClassPath.orEmpty()) 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,7 +20,6 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.* import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions.LOADER import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions.LOADER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.* 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.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
@@ -37,21 +36,21 @@ import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.script.experimental.api.ScriptDiagnostic import kotlin.script.experimental.api.ScriptDiagnostic
class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(project) { class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : DefaultScriptingSupportBase(manager) {
// TODO public for tests // TODO public for tests
val backgroundExecutor: BackgroundExecutor = val backgroundExecutor: BackgroundExecutor =
if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer) if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(manager.updater)
else DefaultBackgroundExecutor(project, rootsIndexer) else DefaultBackgroundExecutor(project, manager.updater)
private val outsiderLoader = ScriptOutsiderFileConfigurationLoader(project) private val outsiderLoader = ScriptOutsiderFileConfigurationLoader(project)
private val fileAttributeCache = ScriptConfigurationFileAttributeCache(project) private val fileAttributeCache = ScriptConfigurationFileAttributeCache(project)
private val defaultLoader = DefaultScriptConfigurationLoader(project) private val defaultLoader = DefaultScriptConfigurationLoader(project)
private val loaders: Sequence<ScriptConfigurationLoader> private val loaders: List<ScriptConfigurationLoader>
get() = sequence { get() = mutableListOf<ScriptConfigurationLoader>().apply {
yield(outsiderLoader) add(outsiderLoader)
yield(fileAttributeCache) add(fileAttributeCache)
yieldAll(LOADER.getPoint(project).extensionList) addAll(LOADER.getPoint(project).extensionList)
yield(defaultLoader) add(defaultLoader)
} }
private val saveLock = ReentrantLock() private val saveLock = ReentrantLock()
@@ -166,7 +165,7 @@ class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(pr
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return null if (!ScriptDefinitionsManager.getInstance(project).isReady()) return null
val scriptDefinition = file.findScriptDefinition() ?: return null val scriptDefinition = file.findScriptDefinition() ?: return null
rootsIndexer.transaction { manager.updater.update {
if (!loader.shouldRunInBackground(scriptDefinition)) { if (!loader.shouldRunInBackground(scriptDefinition)) {
loader.loadDependencies(false, file, scriptDefinition, loadingContext) loader.loadDependencies(false, file, scriptDefinition, loadingContext)
} else { } else {
@@ -240,7 +239,7 @@ class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(pr
onClick = { onClick = {
saveReports(file, newResult.reports) saveReports(file, newResult.reports)
file.removeScriptDependenciesNotificationPanel(project) file.removeScriptDependenciesNotificationPanel(project)
rootsIndexer.transaction { manager.updater.update {
setAppliedConfiguration(file, newResult) setAppliedConfiguration(file, newResult)
} }
} }
@@ -272,10 +271,16 @@ class DefaultScriptingSupport(project: Project) : DefaultScriptingSupportBase(pr
} }
} }
} }
companion object {
fun getInstance(project: Project) =
(ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager).default
}
} }
abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupport() { abstract class DefaultScriptingSupportBase(val manager: CompositeScriptConfigurationManager) : ScriptingSupport() {
protected val rootsIndexer = ScriptClassRootsIndexer(project) val project: Project
get() = manager.project
@Suppress("LeakingThis") @Suppress("LeakingThis")
protected val cache: ScriptConfigurationCache = createCache() protected val cache: ScriptConfigurationCache = createCache()
@@ -311,14 +316,14 @@ abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupp
fun getAppliedConfiguration(file: VirtualFile?): ScriptConfigurationSnapshot? = fun getAppliedConfiguration(file: VirtualFile?): ScriptConfigurationSnapshot? =
getCachedConfigurationState(file)?.applied getCachedConfigurationState(file)?.applied
override fun hasCachedConfiguration(file: KtFile): Boolean = fun hasCachedConfiguration(file: KtFile): Boolean =
getAppliedConfiguration(file.originalFile.virtualFile) != null getAppliedConfiguration(file.originalFile.virtualFile) != null
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean { fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return !hasCachedConfiguration(file) && !ScriptConfigurationManager.isManualConfigurationLoading(file.originalFile.virtualFile) return !hasCachedConfiguration(file) && !ScriptConfigurationManager.isManualConfigurationLoading(file.originalFile.virtualFile)
} }
override fun getOrLoadConfiguration( fun getOrLoadConfiguration(
virtualFile: VirtualFile, virtualFile: VirtualFile,
preloadedKtFile: KtFile? preloadedKtFile: KtFile?
): ScriptCompilationConfigurationWrapper? { ): ScriptCompilationConfigurationWrapper? {
@@ -326,32 +331,30 @@ abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupp
if (cached != null) return cached.configuration if (cached != null) return cached.configuration
val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null val ktFile = project.getKtFile(virtualFile, preloadedKtFile) ?: return null
rootsIndexer.transaction { manager.updater.update {
reloadOutOfDateConfiguration(ktFile, isFirstLoad = true) reloadOutOfDateConfiguration(ktFile, isFirstLoad = true)
} }
return getAppliedConfiguration(virtualFile)?.configuration return getAppliedConfiguration(virtualFile)?.configuration
} }
override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater { fun ensureUpToDatedConfigurationSuggested(file: KtFile) {
override fun ensureUpToDatedConfigurationSuggested(file: KtFile) { reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false)
reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false) }
}
override fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean { fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean {
return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false) return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false)
} }
override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) { fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) {
reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true) reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true)
}
} }
private fun reloadIfOutOfDate(files: List<KtFile>, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: Boolean): Boolean { private fun reloadIfOutOfDate(files: List<KtFile>, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: Boolean): Boolean {
if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false
var upToDate = true var upToDate = true
rootsIndexer.transaction { manager.updater.update {
files.forEach { file -> files.forEach { file ->
val virtualFile = file.originalFile.virtualFile val virtualFile = file.originalFile.virtualFile
if (virtualFile != null) { if (virtualFile != null) {
@@ -376,23 +379,13 @@ abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupp
file: VirtualFile, file: VirtualFile,
newConfigurationSnapshot: ScriptConfigurationSnapshot? newConfigurationSnapshot: ScriptConfigurationSnapshot?
) { ) {
rootsIndexer.checkInTransaction() manager.updater.checkInTransaction()
val newConfiguration = newConfigurationSnapshot?.configuration val newConfiguration = newConfigurationSnapshot?.configuration
debug(file) { "configuration changed = $newConfiguration" } debug(file) { "configuration changed = $newConfiguration" }
if (newConfiguration != null) { if (newConfiguration != null) {
if (hasNotCachedRoots(newConfiguration)) { manager.updater.invalidate(file)
debug(file) { "new class roots found: $newConfiguration" }
rootsIndexer.markNewRoot()
}
cache.setApplied(file, newConfigurationSnapshot) cache.setApplied(file, newConfigurationSnapshot)
clearClassRootsCaches(project)
}
ScriptingSupportHelper.updateHighlighting(project) {
it == file
} }
} }
@@ -403,10 +396,6 @@ abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupp
cache.setLoaded(file, configurationSnapshot) cache.setLoaded(file, configurationSnapshot)
} }
private fun hasNotCachedRoots(configuration: ScriptCompilationConfigurationWrapper): Boolean {
return classpathRoots.hasNotCachedRoots(DefaultClassRootsCache.extractRoots(project, configuration))
}
@TestOnly @TestOnly
internal fun updateScriptDependenciesSynchronously(file: PsiFile) { internal fun updateScriptDependenciesSynchronously(file: PsiFile) {
file.findScriptDefinition() ?: return file.findScriptDefinition() ?: return
@@ -416,20 +405,29 @@ abstract class DefaultScriptingSupportBase(val project: Project) : ScriptingSupp
val virtualFile = file.virtualFile val virtualFile = file.virtualFile
if (cache[virtualFile]?.isUpToDate(project, virtualFile, file) == true) return if (cache[virtualFile]?.isUpToDate(project, virtualFile, file) == true) return
rootsIndexer.transaction { manager.updater.update {
reloadOutOfDateConfiguration(file, forceSync = true, loadEvenWillNotBeApplied = true) reloadOutOfDateConfiguration(file, forceSync = true, loadEvenWillNotBeApplied = true)
} }
} }
override fun clearCaches() { fun updateScriptDefinitions() {
cache.clear() cache.clear()
} }
override fun recreateRootsCache(): ScriptClassRootsCache { fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
return DefaultClassRootsCache( // todo: drop the hell below
project, // keep this one only:
cache.allApplied() // 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)
} }
} }
@@ -6,76 +6,20 @@
package org.jetbrains.kotlin.idea.core.script.configuration package org.jetbrains.kotlin.idea.core.script.configuration
import com.intellij.openapi.extensions.ExtensionPointName 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.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.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
abstract class ScriptingSupport { abstract class ScriptingSupport {
abstract class Provider { abstract class Provider {
abstract val all: Collection<ScriptingSupport> abstract fun updateScriptDefinitions()
abstract fun isApplicable(file: VirtualFile): Boolean
abstract fun getSupport(file: VirtualFile): ScriptingSupport? abstract fun isConfigurationLoadingInProgress(file: KtFile): Boolean
abstract fun collectConfigurations(builder: ScriptClassRootsCache.Builder)
companion object { companion object {
val EPN: ExtensionPointName<Provider> = val EPN: ExtensionPointName<Provider> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.scriptingSupportProvider") 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()
}
} }
@@ -31,7 +31,7 @@ interface ScriptConfigurationCache {
fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot) fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun setLoaded(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot) fun setLoaded(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper> fun allApplied(): List<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>
fun clear() fun clear()
fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper? fun getAnyLoadedScript(): ScriptCompilationConfigurationWrapper?
@@ -41,11 +41,11 @@ open class ScriptConfigurationMemoryCache(
@Synchronized @Synchronized
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun allApplied(): Map<VirtualFile, ScriptCompilationConfigurationWrapper> { override fun allApplied(): List<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>> {
val result = hashMapOf<VirtualFile, ScriptCompilationConfigurationWrapper>() val result = mutableListOf<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>()
for ((file, configuration) in memoryCache.entrySet()) { for ((file, configuration) in memoryCache.entrySet()) {
if (configuration.applied?.configuration != null) { if (configuration.applied?.configuration != null) {
result[file] = configuration.applied.configuration result.add(Pair(file, configuration.applied.configuration))
} }
} }
return result return result
@@ -9,14 +9,14 @@ import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript import org.jetbrains.kotlin.scripting.definitions.isNonScript
open class DefaultScriptChangeListener(project: Project) : ScriptChangeListener(project) { open class DefaultScriptChangeListener(project: Project) : ScriptChangeListener(project) {
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) { override fun editorActivated(vFile: VirtualFile) {
val file = getAnalyzableKtFileForScript(vFile) ?: return val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file) default.ensureUpToDatedConfigurationSuggested(file)
} }
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) { override fun documentChanged(vFile: VirtualFile) {
val file = getAnalyzableKtFileForScript(vFile) ?: return val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file) default.ensureUpToDatedConfigurationSuggested(file)
} }
override fun isApplicable(vFile: VirtualFile): Boolean { override fun isApplicable(vFile: VirtualFile): Boolean {
@@ -9,6 +9,7 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptingSupport
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
@@ -21,8 +22,11 @@ import org.jetbrains.kotlin.psi.KtFile
* @see DefaultScriptConfigurationManager for more details. * @see DefaultScriptConfigurationManager for more details.
*/ */
abstract class ScriptChangeListener(protected val project: Project) { abstract class ScriptChangeListener(protected val project: Project) {
abstract fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) val default: DefaultScriptingSupport
abstract fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) get() = DefaultScriptingSupport.getInstance(project)
abstract fun editorActivated(vFile: VirtualFile)
abstract fun documentChanged(vFile: VirtualFile)
abstract fun isApplicable(vFile: VirtualFile): Boolean abstract fun isApplicable(vFile: VirtualFile): Boolean
@@ -22,8 +22,7 @@ import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChange
import org.jetbrains.kotlin.idea.core.script.isScriptChangesNotifierDisabled import org.jetbrains.kotlin.idea.core.script.isScriptChangesNotifierDisabled
internal class ScriptChangesNotifier( 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 scriptsQueue = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project)
private val scriptChangesListenerDelay = 1400 private val scriptChangesListenerDelay = 1400
@@ -46,10 +45,10 @@ internal class ScriptChangesNotifier(
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) { private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
if (ApplicationManager.getApplication().isUnitTestMode) { if (ApplicationManager.getApplication().isUnitTestMode) {
getListener(project, file)?.editorActivated(file, updater) getListener(project, file)?.editorActivated(file)
} else { } else {
AppExecutorUtil.getAppExecutorService().submit { AppExecutorUtil.getAppExecutorService().submit {
getListener(project, file)?.editorActivated(file, updater) getListener(project, file)?.editorActivated(file)
} }
} }
} }
@@ -68,11 +67,11 @@ internal class ScriptChangesNotifier(
} }
if (ApplicationManager.getApplication().isUnitTestMode) { if (ApplicationManager.getApplication().isUnitTestMode) {
getListener(project, file)?.documentChanged(file, updater) getListener(project, file)?.documentChanged(file)
} else { } else {
scriptsQueue.cancelAllRequests() scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest( scriptsQueue.addRequest(
{ getListener(project, file)?.documentChanged(file, updater) }, { getListener(project, file)?.documentChanged(file) },
scriptChangesListenerDelay, scriptChangesListenerDelay,
true, true,
) )
@@ -84,11 +83,10 @@ internal class ScriptChangesNotifier(
} }
private val defaultListener = DefaultScriptChangeListener(project) private val defaultListener = DefaultScriptChangeListener(project)
private val listeners: Sequence<ScriptChangeListener> private val listeners: Collection<ScriptChangeListener> = mutableListOf<ScriptChangeListener>().apply {
get() = sequence { addAll(LISTENER.getPoint(project).extensionList)
yieldAll(LISTENER.getPoint(project).extensionList) add(defaultListener)
yield(defaultListener) }
}
private fun getListener(project: Project, file: VirtualFile): ScriptChangeListener? { private fun getListener(project: Project, file: VirtualFile): ScriptChangeListener? {
if (project.isDisposed || areListenersDisabled()) return null if (project.isDisposed || areListenersDisabled()) return null
@@ -32,7 +32,7 @@ import java.util.*
*/ */
internal class DefaultBackgroundExecutor( internal class DefaultBackgroundExecutor(
val project: Project, val project: Project,
val rootsManager: ScriptClassRootsIndexer val rootsManager: ScriptClassRootsUpdater
) : BackgroundExecutor { ) : BackgroundExecutor {
companion object { companion object {
const val PROGRESS_INDICATOR_DELAY = 1000 const val PROGRESS_INDICATOR_DELAY = 1000
@@ -137,7 +137,7 @@ internal class DefaultBackgroundExecutor(
private fun ensureInTransaction() { private fun ensureInTransaction() {
if (inTransaction) return if (inTransaction) return
inTransaction = true inTransaction = true
rootsManager.startTransaction() rootsManager.beginUpdating()
} }
@Synchronized @Synchronized
@@ -1,86 +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.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,100 +12,99 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.OrderRootType import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.* import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.NonClasspathDirectoriesScope import com.intellij.psi.search.NonClasspathDirectoriesScope.compose
import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.LOG import org.jetbrains.kotlin.idea.core.script.LOG
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager 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.util.getProjectJdkTableSafe import org.jetbrains.kotlin.idea.util.getProjectJdkTableSafe
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File import java.io.File
import java.lang.ref.Reference
import java.lang.ref.SoftReference
abstract class ScriptClassRootsCache( class ScriptClassRootsCache(
private val project: Project, val scripts: Map<String, LightScriptInfo>,
private val rootsCacheKey: ScriptClassRootsStorage.Companion.Key, val classes: Set<String>,
private val roots: ScriptClassRootsStorage.Companion.ScriptClassRoots val sources: Set<String>,
val sdks: Map<String?, Sdk?>,
private val nonModulesSdks: Collection<Sdk>,
val customDefinitionsUsed: Boolean
) { ) {
protected abstract fun getConfiguration(file: VirtualFile): ScriptCompilationConfigurationWrapper? abstract class LightScriptInfo {
@Volatile
var heavyCache: Reference<HeavyScriptInfo>? = null
abstract val firstScriptSdk: Sdk? abstract fun buildConfiguration(): ScriptCompilationConfigurationWrapper?
}
abstract fun getScriptSdk(file: VirtualFile): Sdk? class DirectScriptInfo(val result: ScriptCompilationConfigurationWrapper) : LightScriptInfo() {
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper = result
}
abstract fun contains(file: VirtualFile): Boolean class HeavyScriptInfo(
private class Fat(
val scriptConfiguration: ScriptCompilationConfigurationWrapper, val scriptConfiguration: ScriptCompilationConfigurationWrapper,
val classFilesScope: GlobalSearchScope val classFilesScope: GlobalSearchScope,
val sdk: Sdk?
) )
val allDependenciesClassFiles by lazy { class Builder(val project: Project) {
ScriptClassRootsStorage.getInstance(project).loadClasspathRoots(rootsCacheKey) val scripts = mutableMapOf<String, LightScriptInfo>()
}
val allDependenciesSources by lazy { private val defaultSdk = getScriptDefaultSdk()
ScriptClassRootsStorage.getInstance(project).loadSourcesRoots(rootsCacheKey) val sdks = mutableMapOf<String?, Sdk?>(null to defaultSdk)
}
val allDependenciesClassFilesScope by lazy { val classes = mutableSetOf<String>()
NonClasspathDirectoriesScope.compose(allDependenciesClassFiles) val sources = mutableSetOf<String>()
} private var customDefinitionsUsed: Boolean = false
val allDependenciesSourcesScope by lazy { fun build(): ScriptClassRootsCache {
NonClasspathDirectoriesScope.compose(allDependenciesSources) val nonIndexedSdks = collectNonIndexedSdks()
} return ScriptClassRootsCache(scripts, classes, sources, sdks, nonIndexedSdks, customDefinitionsUsed)
}
private val scriptsDependenciesCache: MutableMap<VirtualFile, Fat> = fun collectNonIndexedSdks(): MutableSet<Sdk> {
ConcurrentFactoryMap.createWeakMap { file -> val nonIndexedSdks = sdks.values.filterNotNullTo(mutableSetOf())
val configuration = getConfiguration(file) ?: return@createWeakMap null ModuleManager.getInstance(project).modules.map {
nonIndexedSdks.remove(ModuleRootManager.getInstance(it).sdk)
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 getScriptDependenciesClassFilesScope(file: VirtualFile): GlobalSearchScope { fun useCustomScriptDefinition() {
return scriptsDependenciesCache[file]?.classFilesScope ?: GlobalSearchScope.EMPTY_SCOPE customDefinitionsUsed = true
}
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 getScriptSdkOrDefault(javaHome: File?, project: Project): Sdk? { fun add(
return javaHome?.let { getScriptSdkByJavaHome(it) } ?: getScriptDefaultSdk(project) 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 = javaHome.canonicalPath
return sdks.getOrPut(canonicalPath) {
getScriptSdkByJavaHome(javaHome) ?: defaultSdk
}
} }
private fun getScriptSdkByJavaHome(javaHome: File): Sdk? { private fun getScriptSdkByJavaHome(javaHome: File): Sdk? {
@@ -119,7 +118,13 @@ abstract class ScriptClassRootsCache(
return getProjectJdkTableSafe().allJdks.find { it.homeDirectory == javaHomeVF } return getProjectJdkTableSafe().allJdks.find { it.homeDirectory == javaHomeVF }
} }
private fun getScriptDefaultSdk(project: Project): Sdk? { 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? {
val projectSdk = ProjectRootManager.getInstance(project).projectSdk?.takeIf { it.canBeUsedForScript() } val projectSdk = ProjectRootManager.getInstance(project).projectSdk?.takeIf { it.canBeUsedForScript() }
if (projectSdk != null) return projectSdk if (projectSdk != null) return projectSdk
@@ -133,32 +138,118 @@ abstract class ScriptClassRootsCache(
"projectSdk = ${ProjectRootManager.getInstance(project).projectSdk}, " + "projectSdk = ${ProjectRootManager.getInstance(project).projectSdk}, " +
"all sdks = ${getAllProjectSdks().joinToString("\n")}" "all sdks = ${getAllProjectSdks().joinToString("\n")}"
) )
return null return null
} }
private fun Sdk.canBeUsedForScript() = sdkType is JavaSdkType private fun Sdk.canBeUsedForScript() = sdkType is JavaSdkType
}
fun Sdk.isAlreadyIndexed(project: Project): Boolean { fun getLightScriptInfo(file: String) = scripts[file]
return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this }
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 empty(project: Project) = object : ScriptClassRootsCache( old.scripts.forEach {
project, if (it.key !in scripts) {
ScriptClassRootsStorage.Companion.Key("empty"), changed.add(it.key)
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()
} }
} }
@@ -1,80 +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.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,144 +7,46 @@ package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.components.* import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.util.xmlb.XmlSerializerUtil
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
/**
* Actually, this storage is required only for before Gradle 6.0.
* For projects starting from Gradle 6.0, [org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider]
* will also store all roots in FS, and will overwrite this one shrotly after start.
*
* Btw, this is still useful for projects without custom scripting support.
*/
@State( @State(
name = "ScriptClassRootsStorage", name = "ScriptClassRootsStorage",
storages = [Storage(StoragePathMacros.CACHE_FILE)] storages = [Storage(StoragePathMacros.CACHE_FILE)]
) )
class ScriptClassRootsStorage : PersistentStateComponent<Element> { 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()
private var classpath: MutableMap<String, Set<String>> = hashMapOf() override fun getState(): ScriptClassRootsStorage? {
private var sources: MutableMap<String, Set<String>> = hashMapOf() return this
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
} }
private fun storeCollection(root: Element, col: Map<String, Set<String>>, name: String) { override fun loadState(state: ScriptClassRootsStorage) {
for ((key, paths) in col) { XmlSerializerUtil.copyBean(state, this)
for (path in paths) {
val element = Element(name)
element.setAttribute("path", path)
element.setAttribute("key", key)
root.addContent(element)
}
}
} }
override fun loadState(state: Element) { fun save(roots: ScriptClassRootsCache.Builder) {
classpath = readCollection(state, "classpath") classpath = roots.classes
sources = readCollection(state, "sources") sources = roots.sources
sdks = readCollection(state, "sdks") sdks = roots.sdks.values.mapNotNullTo(mutableSetOf()) { it?.name }
} }
private fun readCollection(root: Element, name: String): MutableMap<String, Set<String>> { fun load(builder: ScriptClassRootsCache.Builder) {
val result: MutableMap<String, HashSet<String>> = hashMapOf() builder.sources.addAll(sources)
for (it in root.getChildren(name)) { builder.classes.addAll(classpath)
result.getOrPut(it.getAttributeValue("key")) { sdks.forEach(builder::addSdkByName)
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 { companion object {
fun getInstance(project: Project): ScriptClassRootsStorage = fun getInstance(project: Project): ScriptClassRootsStorage =
ServiceManager.getService(project, ScriptClassRootsStorage::class.java) 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)
} }
} }
@@ -0,0 +1,136 @@
/*
* 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.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 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.configuration.ScriptingSupportHelper
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import java.util.concurrent.atomic.AtomicInteger
/**
* 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 val concurrentUpdates = AtomicInteger()
@Synchronized
@Suppress("UNUSED_PARAMETER")
fun invalidate(file: VirtualFile) {
// todo: record invalided files for some optimisations in update
invalidate()
}
@Synchronized
fun invalidate() {
checkInTransaction()
invalidated = 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()
}
private fun scheduleUpdateIfInvalid() {
if (!invalidated) return
invalidated = false
ensureUpdateScheduled()
}
private var scheduledUpdate: ProgressIndicator? = null
@Synchronized
fun ensureUpdateScheduled() {
scheduledUpdate?.cancel()
scheduledUpdate = BackgroundTaskUtil.executeOnPooledThread(project) {
doUpdate()
}
}
fun doUpdate() {
val updates = manager.collectRootsAndCheckNew()
if (!updates.changed) return
ProgressManager.checkCanceled()
if (updates.hasNewRoots) {
notifyRootsChanged()
}
PsiElementFinder.EP.findExtensionOrFail(KotlinScriptDependenciesClassFinder::class.java, project)
.clearCache()
ScriptDependenciesModificationTracker.getInstance(project).incModificationCount()
if (updates.updatedScripts.isNotEmpty()) {
ScriptingSupportHelper.updateHighlighting(project) {
it.path in updates.updatedScripts
}
}
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)
}
}
}
@@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.containers.HashSetQueue import com.intellij.util.containers.HashSetQueue
class TestingBackgroundExecutor internal constructor( class TestingBackgroundExecutor internal constructor(
private val rootsManager: ScriptClassRootsIndexer private val rootsManager: ScriptClassRootsUpdater
) : BackgroundExecutor { ) : BackgroundExecutor {
val backgroundQueue = HashSetQueue<BackgroundTask>() val backgroundQueue = HashSetQueue<BackgroundTask>()
@@ -42,7 +42,7 @@ class TestingBackgroundExecutor internal constructor(
actions() actions()
rootsManager.transaction { rootsManager.update {
copy.forEach { copy.forEach {
it.actions() it.actions()
} }
@@ -5,33 +5,9 @@
package org.jetbrains.kotlin.idea.scripting.gradle 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 import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
data class ConfigurationData( data class GradleImportedBuildRootData(
val templateClasspath: List<String>, val templateClasspath: List<String>,
val models: List<KotlinDslScriptModel> 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,114 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
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.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.scripting.resolve.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
sealed class GradleBuildRoot {
abstract val pathPrefix: String
abstract class Unlinked : GradleBuildRoot()
class UnlinkedUnknown(override val pathPrefix: String) : Unlinked()
class UnlinkedLegacy(override val pathPrefix: String) : Unlinked()
class UnlinkedSupported(override val pathPrefix: String) : Unlinked()
abstract class Linked : GradleBuildRoot() {
@Volatile
var importing = false
}
class Legacy(override val pathPrefix: String) : Linked()
class New(override val pathPrefix: String) : Linked()
class Imported(
val project: Project,
val dir: VirtualFile,
val context: GradleKtsContext,
val data: GradleImportedBuildRootData
) : Linked() {
override val pathPrefix: String = dir.path
fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
val javaHome = context.javaHome
javaHome?.let { builder.addSdk(it) }
val anyScript = data.models.firstOrNull()?.let {
LocalFileSystem.getInstance().findFileByPath(it.file)
}
val scriptDefinition: ScriptDefinition? = anyScript?.findScriptDefinition(project)
if (scriptDefinition != null) {
builder.classes.addAll(data.templateClasspath)
}
val root = ScriptInfoRoot(this, scriptDefinition)
data.models.forEach {
builder.scripts[it.file] = ScriptInfo(root, it)
builder.classes.addAll(it.classPath)
builder.sources.addAll(it.sourcePath)
}
}
/**
* Common info between scripts (to save bits inside ScriptInfo)
*/
data class ScriptInfoRoot(
val buildRoot: Imported,
val scriptDefinition: ScriptDefinition?
)
data class ScriptInfo(
val root: ScriptInfoRoot,
val model: KotlinDslScriptModel
) : ScriptClassRootsCache.LightScriptInfo() {
val buildRoot get() = root.buildRoot
override fun buildConfiguration(): ScriptCompilationConfigurationWrapper? {
val javaHome = buildRoot.context.javaHome
val scriptDefinition = root.scriptDefinition
val scriptFile = File(model.file)
val virtualFile = VfsUtil.findFile(scriptFile.toPath(), true)!!
if (scriptDefinition == null) return null
return ScriptCompilationConfigurationWrapper.FromCompilationConfiguration(
VirtualFileScriptSource(virtualFile),
scriptDefinition.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(scriptDefinition)
)
}
}
}
}
@@ -1,88 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.project.Project
import com.intellij.openapi.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()
)
}
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
@@ -9,36 +9,23 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
open class GradleScriptListener(project: Project) : ScriptChangeListener(project) { class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
init { init {
// start GradleScriptInputsWatcher to track changes in gradle-configuration related files // start GradleScriptInputsWatcher to track changes in gradle-configuration related files
project.service<GradleScriptInputsWatcher>().startWatching() project.service<GradleScriptInputsWatcher>().startWatching()
} }
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
if (useScriptConfigurationFromImportOnly()) {
// do nothing
} else {
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
val file = getAnalyzableKtFileForScript(vFile)
if (file != null) {
// *.gradle.kts file was changed
updater.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
override fun isApplicable(vFile: VirtualFile): Boolean { override fun isApplicable(vFile: VirtualFile): Boolean {
return isGradleKotlinScript(vFile) return GradleScriptingSupportProvider.getInstance(project).isApplicable(vFile)
}
override fun editorActivated(vFile: VirtualFile) {
// do nothing
}
override fun documentChanged(vFile: VirtualFile) {
GradleScriptingSupportProvider.getInstance(project).getScriptInfo(vFile)?.model?.inputs
} }
} }
@@ -1,62 +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.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)
}
}
}
}
@@ -5,82 +5,146 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
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.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.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsIndexer
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate import org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance
import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter
import org.jetbrains.plugins.gradle.settings.GradleExecutionSettings import org.jetbrains.plugins.gradle.settings.*
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.settings.GradleSettingsListener
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File import java.io.File
import java.nio.file.Paths import java.nio.file.Paths
/** /**
* Creates [GradleScriptingSupport] per each linked Gradle build. * Manages [roots] gradle build roots:
* Gradle project sync should call [update]. * - populated after Gradle project sync, by calling [update]
* - stored in FS and loaded in [init]
*/ */
class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Provider() { class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Provider() {
private val rootsIndexer = ScriptClassRootsIndexer(project) val manager: CompositeScriptConfigurationManager
get() = ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager
val roots = RootsIndex<GradleScriptingSupport>() private val updater
override val all get() = roots.values get() = manager.updater
val roots = RootsIndex()
////////////
/// ScriptingSupport.Provider implementation:
override fun updateScriptDefinitions() {
// nothing related to script definition and project roots are cached
}
override fun isApplicable(file: VirtualFile): Boolean {
val buildRoot = findScriptBuildRoot(file) ?: return false
if (buildRoot is GradleBuildRoot.Legacy) return false
if (buildRoot is GradleBuildRoot.UnlinkedLegacy) return false
return true
}
override fun isConfigurationLoadingInProgress(file: KtFile): Boolean {
return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)) {
is GradleBuildRoot.Linked -> root.importing
else -> false
}
}
override fun collectConfigurations(builder: ScriptClassRootsCache.Builder) {
roots.values.forEach { root ->
if (root is GradleBuildRoot.Imported) {
root.collectConfigurations(builder)
}
}
}
//////////////////
private val VirtualFile.localPath private val VirtualFile.localPath
get() = path get() = path
private fun findRoot(file: VirtualFile) = roots.findRoot(file.localPath) fun getScriptInfo(file: VirtualFile): GradleBuildRoot.Imported.ScriptInfo? =
manager.getLightScriptInfo(file.localPath) as? GradleBuildRoot.Imported.ScriptInfo
override fun getSupport(file: VirtualFile): ScriptingSupport? { fun findScriptBuildRoot(file: VirtualFile): GradleBuildRoot? {
if (isGradleKotlinScript(file)) { if (!isGradleKotlinScript(file)) return null
findRoot(file)?.let { return it }
val externalProjectSettings = findExternalProjectSettings(file) ?: return null val path = file.localPath
if (kotlinDslScriptsModelImportSupported(getGradleVersion(project, externalProjectSettings))) { val found = roots.findNearestRoot(path)
return unlinkedFilesSupport if (found != null) return found
}
} return findNewScriptBuildRoot(path)
return null
} }
private fun findExternalProjectSettings(file: VirtualFile): GradleProjectSettings? { private fun findNewScriptBuildRoot(scriptPath: String): GradleBuildRoot {
val newRoot = updater.update {
loadScriptBuildRoot(scriptPath)
}
roots[newRoot.pathPrefix] = newRoot
return newRoot
}
private fun updateBuildRoot(rootPath: String) {
val settings = getGradleProjectSettings(rootPath) ?: return
val newRoot = loadLinkedRoot(settings)
roots[newRoot.pathPrefix] = newRoot
if (newRoot is GradleBuildRoot.Imported) {
updater.ensureUpdateScheduled()
}
}
private fun loadScriptBuildRoot(scriptPath: String): GradleBuildRoot {
val settings = findNearestGradleProjectSettings(scriptPath) ?: return detectUnlinkedGradleBuildRoot(scriptPath)
return loadLinkedRoot(settings)
}
private fun loadLinkedRoot(settings: GradleProjectSettings) =
tryLoadFromFsCache(settings) ?: createOtherLinkedRoot(settings)
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when {
supported -> GradleBuildRoot.New(settings.externalProjectPath)
else -> GradleBuildRoot.Legacy(settings.externalProjectPath)
}
}
private fun getGradleProjectSettings(workingDir: String): GradleProjectSettings? {
return (ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) as GradleSettings)
.getLinkedProjectSettings(workingDir)
}
private fun findNearestGradleProjectSettings(localPath: String): GradleProjectSettings? {
return getGradleProjectSettings(project) return getGradleProjectSettings(project)
.filter { file.localPath.startsWith(it.externalProjectPath) } .filter { localPath.startsWith(it.externalProjectPath) }
.maxBy { it.externalProjectPath.length } .maxBy { it.externalProjectPath.length }
} }
init { private fun detectUnlinkedGradleBuildRoot(file: String): GradleBuildRoot.Unlinked =
ApplicationManager.getApplication().executeOnPooledThread { GradleBuildRoot.UnlinkedUnknown(file)
roots.update { set ->
getGradleProjectSettings(project).forEach { gradleProjectSettings ->
if (kotlinDslScriptsModelImportSupported(getGradleVersion(project, gradleProjectSettings))) {
val support = createSupport(gradleProjectSettings.externalProjectPath) {
KotlinDslScriptModels.read(it)
}
if (support != null) { init {
set(support.buildRoot.localPath, support) getGradleProjectSettings(project).forEach {
} roots[it.externalProjectPath] = loadLinkedRoot(it)
}
}
}
} }
// subscribe to gradle build unlink // subscribe to gradle build unlink
val listener = object : GradleSettingsListenerAdapter() { val listener = object : GradleSettingsListenerAdapter() {
override fun onProjectsLinked(settings: MutableCollection<GradleProjectSettings>) {
settings.forEach {
loadLinkedRoot(it)
}
}
override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) { override fun onProjectsUnlinked(linkedProjectPaths: MutableSet<String>) {
linkedProjectPaths.forEach { linkedProjectPaths.forEach {
val buildRoot = VfsUtil.findFile(Paths.get(it), false) val buildRoot = VfsUtil.findFile(Paths.get(it), false)
@@ -91,30 +155,68 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
} }
} }
} }
override fun onGradleHomeChange(oldPath: String?, newPath: String?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
override fun onGradleDistributionTypeChange(currentValue: DistributionType?, linkedProjectPath: String) {
updateBuildRoot(linkedProjectPath)
manager.updater.ensureUpdateScheduled()
}
} }
project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener) project.messageBus.connect(project).subscribe(GradleSettingsListener.TOPIC, listener)
} }
private fun tryLoadFromFsCache(settings: GradleProjectSettings) =
tryCreateImportedRoot(settings.externalProjectPath) {
KotlinDslScriptModels.read(it)
}
fun markImportingInProgress(workingDir: String) {
val linked = roots.getBuildRoot(workingDir) as? GradleBuildRoot.Linked
if (linked != null) {
linked.importing = true
} else {
val settings = getGradleProjectSettings(workingDir)
if (settings != null) {
val root = createOtherLinkedRoot(settings)
root.importing = true
roots[workingDir] = root
}
}
}
fun update(build: KotlinDslGradleBuildSync) { fun update(build: KotlinDslGradleBuildSync) {
val settings = getGradleProjectSettings(build.workingDir) ?: return
(roots.getBuildRoot(build.workingDir) as? GradleBuildRoot.Linked)?.importing = false
if (!kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)) {
roots[build.workingDir] = GradleBuildRoot.Legacy(build.workingDir)
return
}
// fast path for linked gradle builds without .gradle.kts support // fast path for linked gradle builds without .gradle.kts support
if (build.models.isEmpty()) { if (build.models.isEmpty()) {
val root = roots.findRoot(build.workingDir) ?: return val root = roots.findNearestRoot(build.workingDir) ?: return
if (root.configuration.data.models.isEmpty()) return if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return
} }
val templateClasspath = findTemplateClasspath(build) ?: return val templateClasspath = findTemplateClasspath(build) ?: return
val data = ConfigurationData(templateClasspath, build.models) val data = GradleImportedBuildRootData(templateClasspath, build.models)
val newSupport = createSupport(build.workingDir) { data } ?: return val newSupport = tryCreateImportedRoot(build.workingDir) { data } ?: return
KotlinDslScriptModels.write(newSupport.buildRoot, data) KotlinDslScriptModels.write(newSupport.dir, data)
roots[build.workingDir] = newSupport
roots[newSupport.buildRoot.localPath] = newSupport manager.updater.ensureUpdateScheduled()
} }
private fun createSupport( private fun tryCreateImportedRoot(
externalProjectPath: String, externalProjectPath: String,
dataProvider: (buildRoot: VirtualFile) -> ConfigurationData? dataProvider: (buildRoot: VirtualFile) -> GradleImportedBuildRootData?
): GradleScriptingSupport? { ): GradleBuildRoot.Imported? {
val gradleExeSettings = val gradleExeSettings =
ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>( ExternalSystemApiUtil.getExecutionSettings<GradleExecutionSettings>(
project, project,
@@ -125,28 +227,15 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null
val data = dataProvider(buildRoot) ?: return null val data = dataProvider(buildRoot) ?: return null
val newSupport = GradleScriptingSupport( val newSupport = GradleBuildRoot.Imported(
project, project,
buildRoot, buildRoot,
GradleKtsContext(gradleExeSettings.javaHome?.let { File(it) }), GradleKtsContext(gradleExeSettings.javaHome?.let { File(it) }),
Configuration(data) data
) )
val oldSupport = roots.findRoot(externalProjectPath) val oldSupport = roots.findNearestRoot(externalProjectPath)
if (oldSupport != null) { 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) hideNotificationForProjectImport(project)
} }
@@ -161,44 +250,11 @@ class GradleScriptingSupportProvider(val project: Project) : ScriptingSupport.Pr
?.templateClasspath?.map { it.path } ?.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 // used in 201
@Suppress("UNUSED") @Suppress("UNUSED")
fun isConfigurationOutOfDate(file: VirtualFile): Boolean { fun isConfigurationOutOfDate(file: VirtualFile): Boolean {
val support = findRoot(file) ?: return false val script = getScriptInfo(file) ?: return false
return support.isConfigurationOutOfDate(file) return script.model.inputs.isUpToDate(project, file)
} }
companion object { companion object {
@@ -16,18 +16,20 @@ import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotifications import com.intellij.ui.EditorNotifications
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants 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 getKey(): Key<EditorNotificationPanel> = KEY
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? { override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
if (!isGradleKotlinScript(file)) return null if (!isGradleKotlinScript(file)) return null
if (file.fileType != KotlinFileType.INSTANCE) return null if (file.fileType != KotlinFileType.INSTANCE) return null
if (!isInAffectedGradleProjectFiles(project, file.path)) { return when (GradleScriptingSupportProvider.getInstance(project).findScriptBuildRoot(file)) {
return EditorNotificationPanel().apply { is GradleBuildRoot.UnlinkedSupported -> EditorNotificationPanel().apply {
text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported")) text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported"))
val linkProjectText = KotlinIdeaGradleBundle.message("action.label.text.load.script.configuration") val linkProjectText = KotlinIdeaGradleBundle.message("action.label.text.load.script.configuration")
@@ -53,22 +55,20 @@ class MissingGradleScriptConfigurationNotificationProvider(private val project:
linkProjectText linkProjectText
) )
} }
} is GradleBuildRoot.Linked -> EditorNotificationPanel().apply {
if (GradleScriptingSupportProvider.getInstance(project).isMissingConfigurationCanBeLoadedDuringImport(file)) {
return EditorNotificationPanel().apply {
text(getMissingConfigurationNotificationText()) text(getMissingConfigurationNotificationText())
createActionLabel(getMissingConfigurationActionText()) { createActionLabel(getMissingConfigurationActionText()) {
runPartialGradleImport(project) runPartialGradleImport(project)
} }
} }
else -> null
} }
return null
} }
private val loaderForOutOfProjectScripts by lazy { private val loaderForOutOfProjectScripts by lazy {
GradleScriptConfigurationLoaderForOutOfProjectScripts(project) GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(
project
)
} }
companion object { companion object {
@@ -9,6 +9,7 @@ import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType
import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider
class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() { class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
override fun onStart(id: ExternalSystemTaskId, workingDir: String?) { override fun onStart(id: ExternalSystemTaskId, workingDir: String?) {
@@ -18,6 +19,7 @@ class KotlinDslSyncListener : ExternalSystemTaskNotificationListenerAdapter() {
if (workingDir == null) return if (workingDir == null) return
val project = id.findProject() ?: return val project = id.findProject() ?: return
GradleScriptingSupportProvider.getInstance(project).markImportingInProgress(workingDir)
project.kotlinGradleDslSync[id] = KotlinDslGradleBuildSync(workingDir, id) project.kotlinGradleDslSync[id] = KotlinDslGradleBuildSync(workingDir, id)
} }
@@ -1,9 +1,9 @@
/* /*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/ */
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle.legacy
import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@@ -15,6 +15,10 @@ 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.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.util.EDT 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.isInAffectedGradleProjectFiles
import org.jetbrains.kotlin.idea.scripting.gradle.useScriptConfigurationFromImportOnly
import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
@@ -24,7 +28,7 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
* *
* TODO(gradle6): remove * TODO(gradle6): remove
*/ */
class GradleScriptConfigurationLoaderForOutOfProjectScripts(project: Project) : DefaultScriptConfigurationLoader(project) { class GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(project: Project) : DefaultScriptConfigurationLoader(project) {
override fun loadDependencies( override fun loadDependencies(
isFirstLoad: Boolean, isFirstLoad: Boolean,
ktFile: KtFile, ktFile: KtFile,
@@ -53,7 +57,7 @@ class GradleScriptConfigurationLoaderForOutOfProjectScripts(project: Project) :
* *
* TODO(gradle6): remove * TODO(gradle6): remove
*/ */
class GradleScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) { class GradleLegacyScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean { override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean {
return if (useScriptConfigurationFromImportOnly()) false else super.shouldRunInBackground(scriptDefinition) return if (useScriptConfigurationFromImportOnly()) false else super.shouldRunInBackground(scriptDefinition)
} }
@@ -0,0 +1,39 @@
/*
* 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.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.*
class GradleLegacyScriptListener(project: Project) : ScriptChangeListener(project) {
override fun isApplicable(vFile: VirtualFile): Boolean {
return isGradleKotlinScript(vFile)
}
override fun editorActivated(vFile: VirtualFile) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
if (useScriptConfigurationFromImportOnly()) {
// do nothing
} else {
val file = getAnalyzableKtFileForScript(vFile) ?: return
default.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
override fun documentChanged(vFile: VirtualFile) {
if (!isInAffectedGradleProjectFiles(project, vFile.path)) return
val file = getAnalyzableKtFileForScript(vFile)
if (file != null) {
// *.gradle.kts file was changed
default.suggestToUpdateConfigurationIfOutOfDate(file)
}
}
}
@@ -19,7 +19,7 @@ import java.io.DataOutput
internal object KotlinDslScriptModels { internal object KotlinDslScriptModels {
private val attribute = FileAttribute("kotlin-dsl-script-models", 3, false) private val attribute = FileAttribute("kotlin-dsl-script-models", 3, false)
fun read(buildRoot: VirtualFile): ConfigurationData? { fun read(buildRoot: VirtualFile): GradleImportedBuildRootData? {
return attribute.readAttribute(buildRoot)?.use { return attribute.readAttribute(buildRoot)?.use {
it.readNullable { it.readNullable {
readKotlinDslScriptModels(it) readKotlinDslScriptModels(it)
@@ -27,9 +27,9 @@ internal object KotlinDslScriptModels {
} }
} }
fun write(buildRoot: VirtualFile, configuration: ConfigurationData?) { fun write(buildRoot: VirtualFile, data: GradleImportedBuildRootData?) {
attribute.writeAttribute(buildRoot).use { attribute.writeAttribute(buildRoot).use {
it.writeNullable(configuration) { it.writeNullable(data) {
writeKotlinDslScriptModels(this, it) writeKotlinDslScriptModels(this, it)
} }
} }
@@ -40,7 +40,7 @@ internal object KotlinDslScriptModels {
} }
} }
internal fun writeKotlinDslScriptModels(output: DataOutput, data: ConfigurationData) { internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleImportedBuildRootData) {
val strings = StringsPool.writer(output) val strings = StringsPool.writer(output)
strings.addStrings(data.templateClasspath) strings.addStrings(data.templateClasspath)
data.models.forEach { data.models.forEach {
@@ -61,7 +61,7 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: ConfigurationD
} }
} }
internal fun readKotlinDslScriptModels(input: DataInputStream): ConfigurationData { internal fun readKotlinDslScriptModels(input: DataInputStream): GradleImportedBuildRootData {
val strings = StringsPool.reader(input) val strings = StringsPool.reader(input)
val templateClasspath = strings.readStrings() val templateClasspath = strings.readStrings()
@@ -77,7 +77,7 @@ internal fun readKotlinDslScriptModels(input: DataInputStream): ConfigurationDat
) )
} }
return ConfigurationData(templateClasspath, models) return GradleImportedBuildRootData(templateClasspath, models)
} }
private object StringsPool { private object StringsPool {
@@ -24,7 +24,8 @@ import org.jetbrains.plugins.gradle.settings.GradleLocalSettings
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.settings.GradleSettings import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
import java.util.* import org.slf4j.LoggerFactory
import kotlin.collections.HashMap
private val sections = arrayListOf("buildscript", "plugins", "initscript", "pluginManagement") private val sections = arrayListOf("buildscript", "plugins", "initscript", "pluginManagement")
@@ -108,42 +109,48 @@ fun getGradleProjectSettings(project: Project): Collection<GradleProjectSettings
return gradleSettings.getLinkedProjectsSettings() return gradleSettings.getLinkedProjectsSettings()
} }
class RootsIndex<T : Any> { class RootsIndex {
internal val tree = TreeMap<String, T>() private val log = LoggerFactory.getLogger(RootsIndex::class.java)
var values: Collection<T> = listOf()
internal set
fun findRoot(path: String): T? { private val buildRoots = HashMap<String, GradleBuildRoot>()
// race condition can be ignored private val scripts = HashMap<String, GradleBuildRoot>()
val values = values val values: Collection<GradleBuildRoot>
val size = values.size get() = buildRoots.values
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
synchronized(this) { fun rebuildProjectRoots() {
updater { prefix, value -> tree[prefix] = value } // values.forEach {
values = tree.values // it.
} // }
} }
@Synchronized @Synchronized
operator fun set(prefix: String, value: T) { fun getBuildRoot(dir: String) = buildRoots[dir]
val moreCommon = tree.lowerKey(prefix)
check(moreCommon == null || !prefix.startsWith(moreCommon)) {
"Cannot add root `${prefix}`. More common root already added: `$moreCommon`"
}
tree[prefix] = value @Synchronized
values = tree.values fun findNearestRoot(path: String): GradleBuildRoot? {
var max: Pair<String, GradleBuildRoot>? = null
buildRoots.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 @Synchronized
fun remove(prefix: String) = tree.remove(prefix) operator fun set(prefix: String, value: GradleBuildRoot) {
val old = buildRoots.put(prefix, value)
rebuildProjectRoots()
log.info("{}: {} -> {}", prefix, old, value)
}
@Synchronized
fun remove(prefix: String) = buildRoots.remove(prefix)?.also {
rebuildProjectRoots()
log.info("{}: removed", prefix)
}
} }
private val logger = Logger.getInstance("#org.jetbrains.kotlin.idea.scripting.gradle") private val logger = Logger.getInstance("#org.jetbrains.kotlin.idea.scripting.gradle")
fun scriptingDebugLog(message: () -> String) { fun scriptingDebugLog(message: () -> String) {
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.idea.scripting.gradle
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationLoadingTest 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.psi.KtFile
import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner import org.jetbrains.kotlin.test.JUnit3WithIdeaConfigurationRunner
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
@@ -34,7 +35,9 @@ class GradleScriptOutOfProjectTest : AbstractScriptConfigurationLoadingTest() {
} }
private val loaderForOutOfProjectScripts by lazy { private val loaderForOutOfProjectScripts by lazy {
GradleScriptConfigurationLoaderForOutOfProjectScripts(project) GradleLegacyScriptConfigurationLoaderForOutOfProjectScripts(
project
)
} }
fun testManualLoadingForUpToDate() { fun testManualLoadingForUpToDate() {
@@ -16,7 +16,7 @@ import kotlin.test.assertEquals
class KotlinDslScriptModelsTest { class KotlinDslScriptModelsTest {
@Test @Test
fun write() { fun write() {
val data = ConfigurationData( val data = GradleImportedBuildRootData(
listOf("a", "b", "c"), listOf("a", "b", "c"),
listOf( listOf(
KotlinDslScriptModel( KotlinDslScriptModel(
@@ -18,16 +18,16 @@ class RootsIndexTest {
roots["a/c"] = 3 roots["a/c"] = 3
roots["d"] = 4 roots["d"] = 4
assertEquals(1, roots.findRoot("a/b/c/x")) assertEquals(1, roots.findNearestRoot("a/b/c/x"))
assertEquals(1, roots.findRoot("a/b/c")) assertEquals(1, roots.findNearestRoot("a/b/c"))
assertEquals(3, roots.findRoot("a/c/x")) assertEquals(3, roots.findNearestRoot("a/c/x"))
assertEquals(3, roots.findRoot("a/c")) assertEquals(3, roots.findNearestRoot("a/c"))
assertEquals(4, roots.findRoot("d")) assertEquals(4, roots.findNearestRoot("d"))
assertEquals(4, roots.findRoot("d/x")) assertEquals(4, roots.findNearestRoot("d/x"))
assertEquals(null, roots.findRoot("e")) assertEquals(null, roots.findNearestRoot("e"))
assertEquals(null, roots.findRoot("e/f")) assertEquals(null, roots.findNearestRoot("e/f"))
roots["d"] = 5 roots["d"] = 5
assertEquals(5, roots.findRoot("d")) assertEquals(5, roots.findNearestRoot("d"))
} }
} }
+4 -4
View File
@@ -14,10 +14,10 @@
<extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea"> <extensions defaultExtensionNs="org.jetbrains.kotlin.scripting.idea">
<listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptListener"/>
<loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptConfigurationLoader"/> <listener order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptListener"/>
<scriptingSupportProvider
implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider" <loader order="first" implementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleLegacyScriptConfigurationLoader"/>
/> <scriptingSupportProvider implementation="org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptingSupportProvider"/>
</extensions> </extensions>
<extensions defaultExtensionNs="org.jetbrains.plugins.gradle"> <extensions defaultExtensionNs="org.jetbrains.plugins.gradle">
@@ -145,12 +145,12 @@ class UnusedSymbolInspection : AbstractKotlinInspection() {
val project = declaration.project val project = declaration.project
val psiSearchHelper = PsiSearchHelper.getInstance(project) val psiSearchHelper = PsiSearchHelper.getInstance(project)
val usedScripts = findScriptsWithUsages(declaration) // val usedScripts = findScriptsWithUsages(declaration)
if (usedScripts.isNotEmpty()) { // if (usedScripts.isNotEmpty()) {
if (!ScriptConfigurationManager.getInstance(declaration.project).updater.ensureConfigurationUpToDate(usedScripts)) { // if (!ScriptConfigurationManager.getInstance(declaration.project).updater.ensureConfigurationUpToDate(usedScripts)) {
return TOO_MANY_OCCURRENCES // return TOO_MANY_OCCURRENCES
} // }
} // }
val useScope = psiSearchHelper.getUseScope(declaration) val useScope = psiSearchHelper.getUseScope(declaration)
if (useScope is GlobalSearchScope) { if (useScope is GlobalSearchScope) {