Scripting: avoid just in case configuration requests to Gradle (KT-34442)

Listen to changes in files that may affect gradle configuration
Remove configuration request if none of such file was changed since last request
^KT-34442 Fixed
This commit is contained in:
Natalia Selezneva
2019-11-26 16:07:04 +03:00
parent f27e13035a
commit 72e99b5863
9 changed files with 138 additions and 85 deletions
@@ -24,6 +24,7 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.toVfsRoots
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCacheScope
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationState
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
@@ -130,13 +131,8 @@ internal abstract class AbstractScriptConfigurationManager(
return reloadIfOutOfDate(files, false)
}
override fun forceConfigurationReload(file: KtFile) {
val virtualFile = file.originalFile.virtualFile ?: return
cache.markOutOfDate(virtualFile)
rootsIndexer.transaction {
reloadOutOfDateConfiguration(file, loadEvenWillNotBeApplied = true)
}
override fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope) {
cache.markOutOfDate(scope)
}
}
@@ -9,7 +9,6 @@ import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
@@ -18,12 +17,9 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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.LISTENER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.listener.DefaultScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
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.DefaultScriptConfigurationLoader
@@ -119,14 +115,7 @@ internal class DefaultScriptConfigurationManager(project: Project) :
yield(defaultLoader)
}
private val defaultListener = DefaultScriptChangeListener()
private val listeners: Sequence<ScriptChangeListener>
get() = sequence {
yieldAll(LISTENER.getPoint(project).extensionList)
yield(defaultListener)
}
private val notifier = ScriptChangesNotifier(project, updater, listeners)
private val notifier = ScriptChangesNotifier(project, updater)
private val saveLock = ReentrantLock()
@@ -322,9 +311,6 @@ internal class DefaultScriptConfigurationManager(project: Project) :
object DefaultScriptConfigurationManagerExtensions {
val LOADER: ExtensionPointName<ScriptConfigurationLoader> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader")
val LISTENER: ExtensionPointName<ScriptChangeListener> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.listener")
}
val ScriptConfigurationManager.testingBackgroundExecutor
@@ -29,12 +29,18 @@ interface ScriptConfigurationCache {
fun setApplied(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun setLoaded(file: VirtualFile, configurationSnapshot: ScriptConfigurationSnapshot)
fun markOutOfDate(file: VirtualFile)
fun markOutOfDate(scope: ScriptConfigurationCacheScope)
fun allApplied(): Collection<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>
fun clear()
}
sealed class ScriptConfigurationCacheScope {
object All : ScriptConfigurationCacheScope()
class File(val file: KtFile) : ScriptConfigurationCacheScope()
class Except(val file: KtFile) : ScriptConfigurationCacheScope()
}
data class ScriptConfigurationState(
val applied: ScriptConfigurationSnapshot? = null,
val loaded: ScriptConfigurationSnapshot? = null
@@ -37,14 +37,26 @@ open class ScriptConfigurationMemoryCache(
}
@Synchronized
override fun markOutOfDate(file: VirtualFile) {
val old = memoryCache[file] ?: ScriptConfigurationState()
memoryCache.put(
file, old.copy(
applied = old.applied?.copy(inputs = CachedConfigurationInputs.OutOfDate),
loaded = old.loaded?.copy(inputs = CachedConfigurationInputs.OutOfDate)
)
)
override fun markOutOfDate(scope: ScriptConfigurationCacheScope) {
when (scope) {
is ScriptConfigurationCacheScope.File -> {
val file = scope.file.originalFile.virtualFile
markFileOutOfDate(file)
}
is ScriptConfigurationCacheScope.Except -> {
val file = scope.file.originalFile.virtualFile
memoryCache.entrySet().forEach {
if (it.key != file) {
markFileOutOfDate(it.key)
}
}
}
is ScriptConfigurationCacheScope.All ->{
memoryCache.entrySet().forEach {
markFileOutOfDate(it.key)
}
}
}
}
@Synchronized
@@ -59,4 +71,15 @@ open class ScriptConfigurationMemoryCache(
override fun clear() {
memoryCache.clear()
}
@Synchronized
private fun markFileOutOfDate(file: VirtualFile) {
val old = memoryCache[file] ?: return
memoryCache.put(
file, old.copy(
applied = old.applied?.copy(inputs = CachedConfigurationInputs.OutOfDate),
loaded = old.loaded?.copy(inputs = CachedConfigurationInputs.OutOfDate)
)
)
}
}
@@ -4,16 +4,22 @@
*/
package org.jetbrains.kotlin.idea.core.script.configuration.listener
import org.jetbrains.kotlin.psi.KtFile
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
open class DefaultScriptChangeListener : ScriptChangeListener {
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
open class DefaultScriptChangeListener(project: Project) : ScriptChangeListener(project) {
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file)
return true
}
override fun documentChanged(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file)
return true
}
override fun isApplicable(vFile: VirtualFile): Boolean {
return vFile.isValid && !vFile.isNonScript()
}
}
@@ -5,18 +5,37 @@
package org.jetbrains.kotlin.idea.core.script.configuration.listener
import org.jetbrains.kotlin.psi.KtFile
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile
/**
* [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed.
* (it treated as applicable if [editorActivated] or [documentChanged] will return true).
*
* Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
*
* @see DefaultScriptConfigurationManager for more details.
*/
interface ScriptChangeListener {
fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean
fun documentChanged(file: KtFile, updater: ScriptConfigurationUpdater): Boolean
abstract class ScriptChangeListener(protected val project: Project) {
abstract fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater)
abstract fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater)
abstract fun isApplicable(vFile: VirtualFile): Boolean
protected fun getAnalyzableKtFileForScript(vFile: VirtualFile): KtFile? {
if (project.isDisposed) return null
return (PsiManager.getInstance(project).findFile(vFile) as? KtFile)?.takeIf {
ProjectRootsUtil.isInProjectSource(it, includeScriptsOutsideSourceRoots = true)
}
}
companion object {
val LISTENER: ExtensionPointName<ScriptChangeListener> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.listener")
}
}
@@ -15,17 +15,13 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager
import com.intellij.util.Alarm
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener.Companion.LISTENER
import org.jetbrains.kotlin.idea.core.script.isScriptChangesNotifierDisabled
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.scripting.definitions.isNonScript
internal class ScriptChangesNotifier(
private val project: Project,
private val updater: ScriptConfigurationUpdater,
private val listeners: Sequence<ScriptChangeListener>
private val updater: ScriptConfigurationUpdater
) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400
@@ -45,9 +41,7 @@ internal class ScriptChangesNotifier(
}
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
listeners.first { it.editorActivated(ktFile, updater) }
getListener(project, file)?.editorActivated(file, updater)
}
})
@@ -55,12 +49,12 @@ internal class ScriptChangesNotifier(
override fun documentChanged(event: DocumentEvent) {
val document = event.document
val file = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
val listener = getListener(project, file) ?: return
scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest(
{ listeners.first { it.documentChanged(ktFile, updater) } },
{ listener.documentChanged(file, updater) },
scriptChangesListenerDelay,
true
)
@@ -68,23 +62,20 @@ internal class ScriptChangesNotifier(
}, project.messageBus.connect())
}
private fun getKtFileToStartConfigurationUpdate(file: VirtualFile): KtFile? {
if (project.isDisposed || !file.isValid || file.isNonScript()) {
return null
private val defaultListener = DefaultScriptChangeListener(project)
private val listeners: Sequence<ScriptChangeListener>
get() = sequence {
yieldAll(LISTENER.getPoint(project).extensionList)
yield(defaultListener)
}
if (
ApplicationManager.getApplication().isUnitTestMode &&
ApplicationManager.getApplication().isScriptChangesNotifierDisabled == true
) {
return null
}
private fun getListener(project: Project, file: VirtualFile): ScriptChangeListener? {
if (project.isDisposed || areListenersDisabled()) return null
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null
if (ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) {
return ktFile
}
return null
return listeners.firstOrNull { it.isApplicable(file) }
}
}
private fun areListenersDisabled(): Boolean {
return ApplicationManager.getApplication().isUnitTestMode && ApplicationManager.getApplication().isScriptChangesNotifierDisabled == true
}
}
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.idea.core.script.configuration.listener
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCacheScope
import org.jetbrains.kotlin.psi.KtFile
interface ScriptConfigurationUpdater {
@@ -24,7 +25,7 @@ interface ScriptConfigurationUpdater {
fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean
/**
* Invalidate current configuration and start loading new
* Invalidate current configuration
*/
fun forceConfigurationReload(file: KtFile)
fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope)
}
@@ -5,24 +5,49 @@
package org.jetbrains.kotlin.idea.scripting.gradle
import org.jetbrains.kotlin.idea.core.script.configuration.listener.DefaultScriptChangeListener
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCacheScope
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.plugins.gradle.GradleManager
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
class GradleScriptListener : DefaultScriptChangeListener() {
private val listenGradleRelatedFiles = false // todo
class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
if (!isGradleKotlinScript(file.virtualFile)) return false
if (listenGradleRelatedFiles) {
updater.ensureUpToDatedConfigurationSuggested(file)
// todo: force reload if related files changed
override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isGradleKotlinScript(vFile)) return
if (useScriptConfigurationFromImportOnly()) {
// do nothing
} else {
// configuration will be reloaded after editor activation, even it is already up-to-date
// this is required for Gradle scripts, since it's classpath may depend on other files (`.properties` for example)
updater.forceConfigurationReload(file)
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file)
}
}
return true
override fun documentChanged(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile)
if (file != null) {
// *.gradle.kts file was changed
updater.ensureUpToDatedConfigurationSuggested(file)
updater.postponeConfigurationReload(ScriptConfigurationCacheScope.Except(file))
} else {
updater.postponeConfigurationReload(ScriptConfigurationCacheScope.All)
}
}
override fun isApplicable(vFile: VirtualFile): Boolean {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
if (gradleSettings.getLinkedProjectsSettings().isEmpty()) return false
val projectSettings = gradleSettings.getLinkedProjectsSettings().filterIsInstance<GradleProjectSettings>().firstOrNull()
?: return false
val affectedFiles = ExternalSystemApiUtil.getAllManagers().filterIsInstance<GradleManager>().firstOrNull()
?.getAffectedExternalProjectFiles(projectSettings.externalProjectPath, project)
return affectedFiles?.any { it.path == vFile.path } == true
}
}