From 97e5ff858d900a676ff3f417365e74e0996d516e Mon Sep 17 00:00:00 2001 From: Natalia Selezneva Date: Wed, 18 Mar 2020 10:26:03 +0300 Subject: [PATCH] Add ability to postpone script configuration loading Show notification for scripts with out of date configurations ^KT-35573 --- .../ScriptConfigurationNotificationFactory.kt | 111 ++++++++++++++++++ .../AbstractScriptConfigurationManager.kt | 18 ++- .../DefaultScriptConfigurationManager.kt | 59 +++++++--- .../listener/ScriptConfigurationUpdater.kt | 5 + .../gradle/GradleScriptConfigurationLoader.kt | 4 +- .../scripting/gradle/GradleScriptListener.kt | 4 +- idea/resources/META-INF/extensions/ide.xml | 9 ++ 7 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt new file mode 100644 index 00000000000..8936e40e577 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptConfigurationNotificationFactory.kt @@ -0,0 +1,111 @@ +/* + * 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 + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.fileEditor.FileEditor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.Ref +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.HyperlinkLabel +import org.jetbrains.annotations.TestOnly +import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings +import org.jetbrains.kotlin.psi.UserDataProperty + +interface ScriptConfigurationNotificationFactory { + fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean + fun hideNotification(file: VirtualFile, project: Project): Boolean + + companion object { + val NOTIFICATION_FACTORY: ExtensionPointName = + ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.notificationFactory") + } +} + +class DefaultScriptConfigurationNotificationFactory : ScriptConfigurationNotificationFactory { + override fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean { + file.addLoadConfigurationNotificationPanel(project, onClick) + return true + } + + override fun hideNotification(file: VirtualFile, project: Project): Boolean { + file.removeLoadConfigurationNotificationPanel(project) + return true + } + + + private fun VirtualFile.removeLoadConfigurationNotificationPanel(project: Project) { + withSelectedEditor(project) { manager -> + notificationPanel?.let { + manager.removeTopComponent(this, it) + } + notificationPanel = null + } + } + + private fun VirtualFile.addLoadConfigurationNotificationPanel( + project: Project, + onClick: () -> Unit + ) { + withSelectedEditor(project) { manager -> + val existingPanel = notificationPanel + if (existingPanel != null) { + notificationPanel?.let { + manager.removeTopComponent(this, it) + } + } + + val panel = NewLoadConfigurationNotificationPanel(onClick, project) + notificationPanel = panel + manager.addTopComponent(this, panel) + } + } + + private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(FileEditorManager) -> Unit) { + ApplicationManager.getApplication().invokeLater { + if (project.isDisposed) return@invokeLater + + val fileEditorManager = FileEditorManager.getInstance(project) + (fileEditorManager.getSelectedEditor(this))?.let { + f(it, fileEditorManager) + } + } + } + + private var FileEditor.notificationPanel: NewLoadConfigurationNotificationPanel? + by UserDataProperty(Key.create("load.script.configuration.panel")) + + private class NewLoadConfigurationNotificationPanel( + val onClick: () -> Unit, + project: Project + ) : EditorNotificationPanel() { + + init { + setText("Script Configuration might be changed") + createComponentActionLabel("Load configuration") { + onClick() + + } + + createComponentActionLabel("Enable auto-reload") { + onClick() + KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled = true + } + } + + private fun EditorNotificationPanel.createComponentActionLabel(labelText: String, callback: (HyperlinkLabel) -> Unit) { + val label: Ref = Ref.create() + val action = Runnable { + callback(label.get()) + } + label.set(createActionLabel(labelText, action)) + } + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt index f7181d06ac3..07c110a4230 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/AbstractScriptConfigurationManager.kt @@ -75,12 +75,14 @@ internal abstract class AbstractScriptConfigurationManager( * user can see potential notification and accept new configuration. In other cases this should be `false` since * loaded configuration will be just leaved in hidden user notification and cannot be used in any way. * @param forceSync should be used in tests only + * @param isPostponedLoad is used to postspone loading: show a notification for out of date script and start loading when user request */ protected abstract fun reloadOutOfDateConfiguration( file: KtFile, isFirstLoad: Boolean = getAppliedConfiguration(file.originalFile.virtualFile) == null, loadEvenWillNotBeApplied: Boolean = false, - forceSync: Boolean = false + forceSync: Boolean = false, + isPostponedLoad: Boolean = false ) /** @@ -149,19 +151,23 @@ internal abstract class AbstractScriptConfigurationManager( override val updater: ScriptConfigurationUpdater = object : ScriptConfigurationUpdater { override fun ensureUpToDatedConfigurationSuggested(file: KtFile) { - reloadIfOutOfDate(listOf(file), true) + reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = false) } override fun ensureConfigurationUpToDate(files: List): Boolean { - return reloadIfOutOfDate(files, false) + return reloadIfOutOfDate(files, loadEvenWillNotBeApplied = false, isPostponedLoad = false) } override fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope) { cache.markOutOfDate(scope) } + + override fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) { + reloadIfOutOfDate(listOf(file), loadEvenWillNotBeApplied = true, isPostponedLoad = true) + } } - private fun reloadIfOutOfDate(files: List, loadEvenWillNotBeApplied: Boolean): Boolean { + private fun reloadIfOutOfDate(files: List, loadEvenWillNotBeApplied: Boolean, isPostponedLoad: Boolean): Boolean { if (!ScriptDefinitionsManager.getInstance(project).isReady()) return false var upToDate = true @@ -175,7 +181,8 @@ internal abstract class AbstractScriptConfigurationManager( reloadOutOfDateConfiguration( file, isFirstLoad = state == null, - loadEvenWillNotBeApplied = loadEvenWillNotBeApplied + loadEvenWillNotBeApplied = loadEvenWillNotBeApplied, + isPostponedLoad = isPostponedLoad ) } } @@ -282,6 +289,7 @@ internal abstract class AbstractScriptConfigurationManager( // ScriptRootsCache private val classpathRootsLock = ReentrantLock() + @Volatile private var _classpathRoots: ScriptClassRootsCache? = null private val classpathRoots: ScriptClassRootsCache diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt index b9fb22daaad..b68efe77c50 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt @@ -31,6 +31,7 @@ import org.jetbrains.kotlin.idea.core.script.configuration.utils.DefaultBackgrou import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition import org.jetbrains.kotlin.scripting.resolve.ScriptReportSink import java.util.concurrent.locks.ReentrantLock @@ -138,6 +139,7 @@ internal class DefaultScriptConfigurationManager(project: Project) : * - invalid, in queue. `cache[file]?.upToDate == false && file in backgroundExecutor`. * - invalid, loading. `cache[file]?.upToDate == false && file !in backgroundExecutor`. * - invalid, waiting for apply. `cache[file]?.upToDate == false && file !in backgroundExecutor` and has notification panel? + * - invalid, waiting for update. `cache[file]?.upToDate == false` and has notification panel * * Async: * - up-to-date: [reloadOutOfDateConfiguration] will not be called. @@ -151,6 +153,8 @@ internal class DefaultScriptConfigurationManager(project: Project) : * - `invalid, waiting for apply`: * Loading will not be queued, since we are marking file as up-to-date with * not yet applied configuration. + * - `invalid, waiting for update`: + * Loading wasn't started, only notification is shown * * Sync: * - up-to-date: @@ -163,7 +167,8 @@ internal class DefaultScriptConfigurationManager(project: Project) : file: KtFile, isFirstLoad: Boolean, loadEvenWillNotBeApplied: Boolean, - forceSync: Boolean + forceSync: Boolean, + isPostponedLoad: Boolean ) { val virtualFile = file.originalFile.virtualFile ?: return @@ -171,6 +176,8 @@ internal class DefaultScriptConfigurationManager(project: Project) : val shouldLoad = isFirstLoad || loadEvenWillNotBeApplied || autoReloadEnabled if (!shouldLoad) return + val postponeLoading = isPostponedLoad && !autoReloadEnabled + if (!ScriptDefinitionsManager.getInstance(project).isReady()) return val scriptDefinition = file.findScriptDefinition() ?: return @@ -178,28 +185,46 @@ internal class DefaultScriptConfigurationManager(project: Project) : val syncLoader = sync.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) } if (syncLoader == null) { - // run async loader if (forceSync) { - async.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) } + loaders.firstOrNull { it.loadDependencies(isFirstLoad, file, scriptDefinition, loadingContext) } } else { - backgroundExecutor.ensureScheduled(virtualFile) { - val cached = getCachedConfigurationState(virtualFile) - - val applied = cached?.applied - if (applied != null && applied.inputs.isUpToDate(project, virtualFile)) { - // in case user reverted to applied configuration - suggestOrSaveConfiguration(virtualFile, applied, false) - } else if (cached == null || !cached.isUpToDate(project, virtualFile)) { - // don't start loading if nothing was changed - // (in case we checking for up-to-date and loading concurrently) - val actualIsFirstLoad = cached == null - async.firstOrNull { it.loadDependencies(actualIsFirstLoad, file, scriptDefinition, loadingContext) } + if (postponeLoading) { + ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { + it.showNotification(virtualFile, project) { + runAsyncLoaders(file, virtualFile, scriptDefinition, async, postponeLoading) + } } + } else { + runAsyncLoaders(file, virtualFile, scriptDefinition, async, postponeLoading) } } } } + // в нотификации остается старый колбек - нужно перезаписывать колбек + private fun runAsyncLoaders( + file: KtFile, + virtualFile: VirtualFile, + scriptDefinition: ScriptDefinition, + loaders: List, + isLoadingPostponed: Boolean + ) { + backgroundExecutor.ensureScheduled(virtualFile) { + val cached = getCachedConfigurationState(virtualFile) + + val applied = cached?.applied + if (applied != null && applied.inputs.isUpToDate(project, virtualFile)) { + // in case user reverted to applied configuration + suggestOrSaveConfiguration(virtualFile, applied, isLoadingPostponed) + } else if (cached == null || !cached.isUpToDate(project, virtualFile)) { + // don't start loading if nothing was changed + // (in case we checking for up-to-date and loading concurrently) + val actualIsFirstLoad = cached == null + loaders.firstOrNull { it.loadDependencies(actualIsFirstLoad, file, scriptDefinition, loadingContext) } + } + } + } + override fun forceReloadConfiguration(file: KtFile, loader: ScriptConfigurationLoader) { val virtualFile = file.originalFile.virtualFile ?: return @@ -251,6 +276,10 @@ internal class DefaultScriptConfigurationManager(project: Project) : setLoadedConfiguration(file, newResult) + ScriptConfigurationNotificationFactory.NOTIFICATION_FACTORY.getPoint(project).extensionList.firstOrNull { + it.hideNotification(file, project) + } + val newConfiguration = newResult.configuration if (newConfiguration == null) { saveReports(file, newResult.reports) diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt index 1a7eb818ab6..0df967cb3dc 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptConfigurationUpdater.kt @@ -28,4 +28,9 @@ interface ScriptConfigurationUpdater { * Invalidate current configuration */ fun postponeConfigurationReload(scope: ScriptConfigurationCacheScope) + + /** + * Suggest to update script configuration if configuration for [file] is out of date + */ + fun suggestToUpdateConfigurationIfOutOfDate(file: KtFile) } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt index 7dad45a8f01..811e414f71a 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptConfigurationLoader.kt @@ -78,7 +78,9 @@ class GradleScriptConfigurationLoader(project: Project) : DefaultScriptConfigura } } - return super.loadDependencies(isFirstLoad, ktFile, scriptDefinition, context) + val result = getConfigurationThroughScriptingApi(ktFile, vFile, scriptDefinition) + context.saveNewConfiguration(vFile, result) + return true } override fun getInputsStamp(virtualFile: VirtualFile, file: KtFile): CachedConfigurationInputs { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt index 9238fb13e8c..930abec4b2e 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt @@ -22,7 +22,7 @@ open class GradleScriptListener(project: Project) : ScriptChangeListener(project // do nothing } else { val file = getAnalyzableKtFileForScript(vFile) ?: return - updater.ensureUpToDatedConfigurationSuggested(file) + updater.suggestToUpdateConfigurationIfOutOfDate(file) } } @@ -30,7 +30,7 @@ open class GradleScriptListener(project: Project) : ScriptChangeListener(project val file = getAnalyzableKtFileForScript(vFile) if (file != null) { // *.gradle.kts file was changed - updater.ensureUpToDatedConfigurationSuggested(file) + updater.suggestToUpdateConfigurationIfOutOfDate(file) } } diff --git a/idea/resources/META-INF/extensions/ide.xml b/idea/resources/META-INF/extensions/ide.xml index 0a023d83e2e..a1998d0a649 100644 --- a/idea/resources/META-INF/extensions/ide.xml +++ b/idea/resources/META-INF/extensions/ide.xml @@ -68,6 +68,10 @@ interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener" area="IDEA_PROJECT"/> + + @@ -101,6 +105,11 @@ + +