Add ability to postpone script configuration loading

Show notification for scripts with out of date configurations

^KT-35573
This commit is contained in:
Natalia Selezneva
2020-03-18 10:26:03 +03:00
parent 7f5715f5da
commit 97e5ff858d
7 changed files with 187 additions and 23 deletions
@@ -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<ScriptConfigurationNotificationFactory> =
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<FileEditor, NewLoadConfigurationNotificationPanel>(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<HyperlinkLabel> = Ref.create()
val action = Runnable {
callback(label.get())
}
label.set(createActionLabel(labelText, action))
}
}
}
@@ -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<KtFile>): 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<KtFile>, loadEvenWillNotBeApplied: Boolean): Boolean {
private fun reloadIfOutOfDate(files: List<KtFile>, 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
@@ -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<ScriptConfigurationLoader>,
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)
@@ -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)
}
@@ -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 {
@@ -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)
}
}
@@ -68,6 +68,10 @@
interface="org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.scripting.idea.notificationFactory"
interface="org.jetbrains.kotlin.idea.core.script.ScriptConfigurationNotificationFactory"
area="IDEA_PROJECT"/>
<extensionPoint qualifiedName="org.jetbrains.kotlin.additionalExtractableAnalyser"
interface="org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AdditionalExtractableAnalyser"/>
@@ -101,6 +105,11 @@
<scriptDefinitionsProvider id="MainKtsScriptDefinitionSource"
implementation="org.jetbrains.kotlin.idea.script.MainKtsScriptDefinitionSource"/>
<scripting.idea.notificationFactory
implementation="org.jetbrains.kotlin.idea.core.script.DefaultScriptConfigurationNotificationFactory"
order="last"
/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JvmPlatformKindResolution"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JsPlatformKindResolution"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.CommonPlatformKindResolution"/>