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.*
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager.Companion.toVfsRoots 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.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.ScriptConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationState import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationState
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
@@ -130,13 +131,8 @@ internal abstract class AbstractScriptConfigurationManager(
return reloadIfOutOfDate(files, false) return reloadIfOutOfDate(files, false)
} }
override fun forceConfigurationReload(file: KtFile) { override fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope) {
val virtualFile = file.originalFile.virtualFile ?: return cache.markOutOfDate(scope)
cache.markOutOfDate(virtualFile)
rootsIndexer.transaction {
reloadOutOfDateConfiguration(file, loadEvenWillNotBeApplied = true)
}
} }
} }
@@ -9,7 +9,6 @@ import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ServiceManager import com.intellij.openapi.components.ServiceManager
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.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
@@ -18,12 +17,9 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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.DefaultScriptConfigurationManagerExtensions.LISTENER
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache 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.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot 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.ScriptChangesNotifier
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
@@ -119,14 +115,7 @@ internal class DefaultScriptConfigurationManager(project: Project) :
yield(defaultLoader) yield(defaultLoader)
} }
private val defaultListener = DefaultScriptChangeListener() private val notifier = ScriptChangesNotifier(project, updater)
private val listeners: Sequence<ScriptChangeListener>
get() = sequence {
yieldAll(LISTENER.getPoint(project).extensionList)
yield(defaultListener)
}
private val notifier = ScriptChangesNotifier(project, updater, listeners)
private val saveLock = ReentrantLock() private val saveLock = ReentrantLock()
@@ -322,9 +311,6 @@ internal class DefaultScriptConfigurationManager(project: Project) :
object DefaultScriptConfigurationManagerExtensions { object DefaultScriptConfigurationManagerExtensions {
val LOADER: ExtensionPointName<ScriptConfigurationLoader> = val LOADER: ExtensionPointName<ScriptConfigurationLoader> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader") ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader")
val LISTENER: ExtensionPointName<ScriptChangeListener> =
ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.listener")
} }
val ScriptConfigurationManager.testingBackgroundExecutor val ScriptConfigurationManager.testingBackgroundExecutor
@@ -29,12 +29,18 @@ 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 markOutOfDate(file: VirtualFile) fun markOutOfDate(scope: ScriptConfigurationCacheScope)
fun allApplied(): Collection<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>> fun allApplied(): Collection<Pair<VirtualFile, ScriptCompilationConfigurationWrapper>>
fun clear() fun clear()
} }
sealed class ScriptConfigurationCacheScope {
object All : ScriptConfigurationCacheScope()
class File(val file: KtFile) : ScriptConfigurationCacheScope()
class Except(val file: KtFile) : ScriptConfigurationCacheScope()
}
data class ScriptConfigurationState( data class ScriptConfigurationState(
val applied: ScriptConfigurationSnapshot? = null, val applied: ScriptConfigurationSnapshot? = null,
val loaded: ScriptConfigurationSnapshot? = null val loaded: ScriptConfigurationSnapshot? = null
@@ -37,14 +37,26 @@ open class ScriptConfigurationMemoryCache(
} }
@Synchronized @Synchronized
override fun markOutOfDate(file: VirtualFile) { override fun markOutOfDate(scope: ScriptConfigurationCacheScope) {
val old = memoryCache[file] ?: ScriptConfigurationState() when (scope) {
memoryCache.put( is ScriptConfigurationCacheScope.File -> {
file, old.copy( val file = scope.file.originalFile.virtualFile
applied = old.applied?.copy(inputs = CachedConfigurationInputs.OutOfDate), markFileOutOfDate(file)
loaded = old.loaded?.copy(inputs = CachedConfigurationInputs.OutOfDate) }
) 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 @Synchronized
@@ -59,4 +71,15 @@ open class ScriptConfigurationMemoryCache(
override fun clear() { override fun clear() {
memoryCache.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 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 { open class DefaultScriptChangeListener(project: Project) : ScriptChangeListener(project) {
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean { override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
val file = getAnalyzableKtFileForScript(vFile) ?: return
updater.ensureUpToDatedConfigurationSuggested(file) 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) 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 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.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. * [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. * Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
* *
* @see DefaultScriptConfigurationManager for more details. * @see DefaultScriptConfigurationManager for more details.
*/ */
interface ScriptChangeListener { abstract class ScriptChangeListener(protected val project: Project) {
fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean abstract fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater)
fun documentChanged(file: KtFile, updater: ScriptConfigurationUpdater): Boolean 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.fileEditor.FileEditorManagerListener
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.util.Alarm 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.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( internal class ScriptChangesNotifier(
private val project: Project, private val project: Project,
private val updater: ScriptConfigurationUpdater, private val updater: ScriptConfigurationUpdater
private val listeners: Sequence<ScriptChangeListener>
) { ) {
private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project) private val scriptsQueue = Alarm(Alarm.ThreadToUse.SWING_THREAD, project)
private val scriptChangesListenerDelay = 1400 private val scriptChangesListenerDelay = 1400
@@ -45,9 +41,7 @@ internal class ScriptChangesNotifier(
} }
private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) { private fun runScriptDependenciesUpdateIfNeeded(file: VirtualFile) {
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return getListener(project, file)?.editorActivated(file, updater)
listeners.first { it.editorActivated(ktFile, updater) }
} }
}) })
@@ -55,12 +49,12 @@ internal class ScriptChangesNotifier(
override fun documentChanged(event: DocumentEvent) { override fun documentChanged(event: DocumentEvent) {
val document = event.document val document = event.document
val file = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return val file = FileDocumentManager.getInstance().getFile(document)?.takeIf { it.isInLocalFileSystem } ?: return
val ktFile = getKtFileToStartConfigurationUpdate(file) ?: return
val listener = getListener(project, file) ?: return
scriptsQueue.cancelAllRequests() scriptsQueue.cancelAllRequests()
scriptsQueue.addRequest( scriptsQueue.addRequest(
{ listeners.first { it.documentChanged(ktFile, updater) } }, { listener.documentChanged(file, updater) },
scriptChangesListenerDelay, scriptChangesListenerDelay,
true true
) )
@@ -68,23 +62,20 @@ internal class ScriptChangesNotifier(
}, project.messageBus.connect()) }, project.messageBus.connect())
} }
private fun getKtFileToStartConfigurationUpdate(file: VirtualFile): KtFile? { private val defaultListener = DefaultScriptChangeListener(project)
if (project.isDisposed || !file.isValid || file.isNonScript()) { private val listeners: Sequence<ScriptChangeListener>
return null get() = sequence {
yieldAll(LISTENER.getPoint(project).extensionList)
yield(defaultListener)
} }
if ( private fun getListener(project: Project, file: VirtualFile): ScriptChangeListener? {
ApplicationManager.getApplication().isUnitTestMode && if (project.isDisposed || areListenersDisabled()) return null
ApplicationManager.getApplication().isScriptChangesNotifierDisabled == true
) {
return null
}
val ktFile = PsiManager.getInstance(project).findFile(file) as? KtFile ?: return null return listeners.firstOrNull { it.isApplicable(file) }
if (ProjectRootsUtil.isInProjectSource(ktFile, includeScriptsOutsideSourceRoots = true)) {
return ktFile
}
return null
} }
}
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 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 import org.jetbrains.kotlin.psi.KtFile
interface ScriptConfigurationUpdater { interface ScriptConfigurationUpdater {
@@ -24,7 +25,7 @@ interface ScriptConfigurationUpdater {
fun ensureConfigurationUpToDate(files: List<KtFile>): Boolean 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 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.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() { class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
private val listenGradleRelatedFiles = false // todo
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean { override fun editorActivated(vFile: VirtualFile, updater: ScriptConfigurationUpdater) {
if (!isGradleKotlinScript(file.virtualFile)) return false if (!isGradleKotlinScript(vFile)) return
if (listenGradleRelatedFiles) {
updater.ensureUpToDatedConfigurationSuggested(file) if (useScriptConfigurationFromImportOnly()) {
// todo: force reload if related files changed // do nothing
} else { } else {
// configuration will be reloaded after editor activation, even it is already up-to-date val file = getAnalyzableKtFileForScript(vFile) ?: return
// this is required for Gradle scripts, since it's classpath may depend on other files (`.properties` for example) updater.ensureUpToDatedConfigurationSuggested(file)
updater.forceConfigurationReload(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
} }
} }