diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt index 48ed12bb6c3..88df523b0d2 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsCache.kt @@ -17,13 +17,20 @@ import com.intellij.psi.search.NonClasspathDirectoriesScope import com.intellij.util.containers.ConcurrentFactoryMap import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager -import org.jetbrains.kotlin.idea.core.script.debug import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper +import java.io.File internal class ScriptClassRootsCache( private val project: Project, private val all: Map ) { + private val scriptsSdksCache: Map = + ConcurrentFactoryMap.createWeakMap { file -> + return@createWeakMap getScriptSdk(all[file]) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) + } + + fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file] + private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? { // workaround for mismatched gradle wrapper and plugin version val javaHome = try { @@ -35,48 +42,21 @@ internal class ScriptClassRootsCache( return getAllProjectSdks().find { it.homeDirectory == javaHome } } - private val scriptsSdksCache: Map = - ConcurrentFactoryMap.createWeakMap { file -> - val compilationConfiguration = all[file] - return@createWeakMap getScriptSdk(compilationConfiguration) - ?: ScriptConfigurationManager.getScriptDefaultSdk(project) - } - - fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file] - val firstScriptSdk: Sdk? by lazy { val firstCachedScript = all.keys.firstOrNull() ?: return@lazy null return@lazy getScriptSdk(firstCachedScript) } - private val allSdks by lazy { - all.mapNotNull { scriptsSdksCache[it.key] } - .distinct() - } - - private val allNonIndexedSdks by lazy { - all.mapNotNull { scriptsSdksCache[it.key] } - .filterNonModuleSdk() - .distinct() - } - - private fun List.filterNonModuleSdk(): List { - val moduleSdks = ModuleManager.getInstance(project).modules.map { ModuleRootManager.getInstance(it).sdk } - return filterNot { moduleSdks.contains(it) } + private fun Sdk.isAlreadyIndexed(): Boolean { + return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this } } val allDependenciesClassFiles by lazy { - val sdkFiles = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.CLASSES).toList() } - val scriptDependenciesClasspath = all.flatMap { it.value.dependenciesClassPath }.distinct() - - sdkFiles + ScriptConfigurationManager.toVfsRoots(scriptDependenciesClasspath) + ScriptClassRootsStorage.getInstance(project).loadClasspathRoots() } val allDependenciesSources by lazy { - val sdkSources = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.SOURCES).toList() } - val scriptDependenciesSources = all.flatMap { it.value.dependenciesSources }.distinct() - - sdkSources + ScriptConfigurationManager.toVfsRoots(scriptDependenciesSources) + ScriptClassRootsStorage.getInstance(project).loadSourcesRoots() } val allDependenciesClassFilesScope by lazy { @@ -116,32 +96,55 @@ internal class ScriptClassRootsCache( } fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean { - val scriptSdk = getScriptSdk(compilationConfiguration) - ?: ScriptConfigurationManager.getScriptDefaultSdk(project) + return !ScriptClassRootsStorage.getInstance(project).containsAll(extractRoots(compilationConfiguration)) + } - val wasSdkChanged = scriptSdk != null && !allSdks.contains(scriptSdk) - if (wasSdkChanged) { - debug { "sdk was changed: $compilationConfiguration" } - return true + private fun extractRoots(configuration: ScriptCompilationConfigurationWrapper): ScriptClassRootsStorage.Companion.ScriptClassRoots { + val scriptSdk = getScriptSdk(configuration) ?: ScriptConfigurationManager.getScriptDefaultSdk(project) + if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) { + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + configuration.dependenciesClassPath, + configuration.dependenciesSources, + listOf(scriptSdk) + ) } - val newClassRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesClassPath) - for (newClassRoot in newClassRoots) { - if (!allDependenciesClassFiles.contains(newClassRoot)) { - debug { "class root was changed: $newClassRoot" } - return true + return ScriptClassRootsStorage.Companion.ScriptClassRoots( + configuration.dependenciesClassPath, + configuration.dependenciesSources, + emptyList() + ) + } + + init { + saveClassRootsToStorage() + } + + private fun saveClassRootsToStorage() { + if (all.isEmpty()) return + + val classpath = mutableSetOf() + val sources = mutableSetOf() + val sdks = mutableSetOf() + + for ((file, configuration) in all) { + val scriptSdk = getScriptSdk(file) + if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) { + sdks.add(scriptSdk) } + + classpath.addAll(configuration.dependenciesClassPath) + sources.addAll(configuration.dependenciesSources) } - val newSourceRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesSources) - for (newSourceRoot in newSourceRoots) { - if (!allDependenciesSources.contains(newSourceRoot)) { - debug { "source root was changed: $newSourceRoot" } - return true - } - } - - return false + val rootsStorage = ScriptClassRootsStorage.getInstance(project) + rootsStorage.save( + ScriptClassRootsStorage.Companion.ScriptClassRoots( + classpath.toList(), + sources.toList(), + sdks.toList() + ) + ) } fun contains(file: VirtualFile) = all.containsKey(file) diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt new file mode 100644 index 00000000000..7a88ffb57b2 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/ScriptClassRootsStorage.kt @@ -0,0 +1,115 @@ +/* + * 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.components.* +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.OrderRootType +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.io.URLUtil +import com.intellij.util.xmlb.XmlSerializerUtil +import org.jetbrains.kotlin.idea.core.script.debug +import java.io.File + +@State( + name = "ScriptClassRootsStorage", + storages = [Storage(StoragePathMacros.CACHE_FILE)] +) +class ScriptClassRootsStorage : PersistentStateComponent { + private var classpath: Set = hashSetOf() + private var sources: Set = hashSetOf() + private var sdks: Set = hashSetOf() + + override fun getState(): ScriptClassRootsStorage? { + return this + } + + override fun loadState(state: ScriptClassRootsStorage) { + XmlSerializerUtil.copyBean(state, this) + } + + private fun toStringNames(sdks: Collection): Set { + return sdks.map { it.name }.toSet() + } + + private fun toStringValues(prop: Collection): Set { + return prop.mapNotNull { + when { + it.isDirectory -> it.absolutePath + it.isFile -> it.absolutePath + URLUtil.JAR_SEPARATOR + else -> null + } + }.toSet() + } + + private fun toVirtualFiles(prop: Set, sources: Boolean): List { + val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES + return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) } + .flatMap { it.rootProvider.getFiles(rootType).toList() } + } + + private fun toVirtualFiles(prop: Collection): List { + return prop.mapNotNull { + StandardFileSystems.local()?.findFileByPath(it)?.let { + return@mapNotNull it + } + + StandardFileSystems.jar()?.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(configuration: ScriptClassRoots): Boolean { + if (!classpath.containsAll(toStringValues(configuration.classpathFiles))) { + debug { "class roots were changed: old = $classpath, new = ${configuration.classpathFiles}" } + return false + } + if (!sources.containsAll(toStringValues(configuration.sourcesFiles))) { + debug { "source roots were changed: old = $sources, new = ${configuration.sourcesFiles}" } + return false + } + if (!sdks.containsAll(toStringNames(configuration.sdks))) { + debug { "sdk classes were changed: old = $sdks, new = ${configuration.sdks.map { it.homePath }}" } + return false + } + return true + } + + fun save(configuration: ScriptClassRoots) { + // TODO: do not drop all storage on save: KT-34444 + classpath = toStringValues(configuration.classpathFiles) + sources = toStringValues(configuration.sourcesFiles) + + sdks = toStringNames(configuration.sdks) + } + + fun loadClasspathRoots(): List { + return toVirtualFiles(sdks, false) + toVirtualFiles(classpath) + } + + fun loadSourcesRoots(): List { + return toVirtualFiles(sdks, true) + toVirtualFiles(sources) + } + + companion object { + fun getInstance(project: Project): ScriptClassRootsStorage = + ServiceManager.getService(project, ScriptClassRootsStorage::class.java) + + data class ScriptClassRoots( + val classpathFiles: List, + val sourcesFiles: List, + val sdks: List + ) + } +} \ No newline at end of file diff --git a/idea/resources/META-INF/scripting-support.xml b/idea/resources/META-INF/scripting-support.xml index 02d0c6371a5..1be40949391 100644 --- a/idea/resources/META-INF/scripting-support.xml +++ b/idea/resources/META-INF/scripting-support.xml @@ -1,5 +1,7 @@ + + \ No newline at end of file