Add ability to postpone script configuration loading
Show notification for scripts with out of date configurations ^KT-35573
This commit is contained in:
+111
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-5
@@ -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
|
||||
|
||||
+44
-15
@@ -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)
|
||||
|
||||
+5
@@ -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)
|
||||
}
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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"/>
|
||||
|
||||
Reference in New Issue
Block a user