gradle scripts: custom notification wording gradle with default scripting support (gradle older then 6.0)

This commit is contained in:
Sergey Rostov
2020-06-18 20:46:49 +03:00
parent 37fbc75008
commit 9f4569e5a1
13 changed files with 178 additions and 208 deletions
@@ -1,118 +0,0 @@
/*
* 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.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.idea.core.util.KotlinIdeaCoreBundle
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
object LoadScriptConfigurationNotificationFactory {
fun showNotification(file: VirtualFile, project: Project, onClick: () -> Unit): Boolean {
file.addLoadConfigurationNotificationPanel(project, onClick)
return true
}
fun hideNotification(file: VirtualFile, project: Project): Boolean {
file.removeLoadConfigurationNotificationPanel(project)
return true
}
@TestOnly
fun hasNotification(file: VirtualFile, project: Project): Boolean {
return FileEditorManager.getInstance(project).getSelectedEditor(file)?.notificationPanel != null
}
@TestOnly
fun performSuggestedLoading(file: VirtualFile, project: Project): Boolean {
val notificationPanel = FileEditorManager.getInstance(project).getSelectedEditor(file)?.notificationPanel
?: return false
notificationPanel.onClick.invoke()
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) {
existingPanel.onClick = onClick
return@withSelectedEditor
}
val panel = NewLoadConfigurationNotificationPanel(onClick, project, this@addLoadConfigurationNotificationPanel)
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.getAllEditors(this).forEach {
f(it, fileEditorManager)
}
}
}
private var FileEditor.notificationPanel: NewLoadConfigurationNotificationPanel?
by UserDataProperty<FileEditor, NewLoadConfigurationNotificationPanel>(Key.create("load.script.configuration.panel"))
private class NewLoadConfigurationNotificationPanel(
var onClick: () -> Unit,
project: Project,
file: VirtualFile
) : EditorNotificationPanel() {
init {
setText(KotlinIdeaCoreBundle.message("notification.text.script.configuration.has.been.changed"))
createComponentActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.load.script.configuration")) {
onClick()
file.removeLoadConfigurationNotificationPanel(project)
}
createComponentActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.enable.auto.reload")) {
onClick()
file.removeLoadConfigurationNotificationPanel(project)
val scriptDefinition = file.findScriptDefinition(project) ?: return@createComponentActionLabel
KotlinScriptingSettings.getInstance(project).setAutoReloadConfigurations(scriptDefinition, 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))
}
}
}
@@ -177,8 +177,11 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
val autoReloadEnabled = KotlinScriptingSettings.getInstance(project).autoReloadConfigurations(scriptDefinition)
val forceSkipNotification = skipNotification || autoReloadEnabled
// sync loaders can do something, let's recheck
val isFirstLoadActual = getCachedConfigurationState(virtualFile)?.applied != null
val intercepted = !forceSkipNotification && async.any {
it.interceptBackgroundLoading(virtualFile) {
it.interceptBackgroundLoading(virtualFile, isFirstLoadActual) {
runAsyncLoaders(file, virtualFile, scriptDefinition, listOf(it), true)
}
}
@@ -263,7 +266,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
setLoadedConfiguration(file, newResult)
LoadScriptConfigurationNotificationFactory.hideNotification(file, project)
hideInterceptedNotification(file)
val newConfiguration = newResult.configuration
if (newConfiguration == null) {
@@ -340,7 +343,7 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
}
// this notification can be shown before something will be in cache
LoadScriptConfigurationNotificationFactory.hideNotification(file, project)
hideInterceptedNotification(file)
}
fun updateScriptDefinitionsReferences() {
@@ -348,12 +351,18 @@ class DefaultScriptingSupport(manager: CompositeScriptConfigurationManager) : De
cache.allApplied().forEach { (file, _) ->
saveReports(file, listOf())
file.removeScriptDependenciesNotificationPanel(project)
LoadScriptConfigurationNotificationFactory.hideNotification(file, project)
hideInterceptedNotification(file)
}
cache.clear()
}
fun hideInterceptedNotification(file: VirtualFile) {
loaders.forEach {
it.hideInterceptedNotification(file)
}
}
companion object {
fun getInstance(project: Project) =
(ScriptConfigurationManager.getInstance(project) as CompositeScriptConfigurationManager).default
@@ -17,7 +17,9 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
interface ScriptConfigurationLoader {
fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean = false
fun interceptBackgroundLoading(file: VirtualFile, doLoad: () -> Unit): Boolean = false
fun interceptBackgroundLoading(file: VirtualFile, isFirstLoad: Boolean, doLoad: () -> Unit): Boolean = false
fun hideInterceptedNotification(file: VirtualFile) = Unit
/**
* Implementation should load configuration and call `context.suggestNewConfiguration` or `saveNewConfiguration`.
@@ -1,3 +1,14 @@
notification.oldGradle.firstLoad=Code insight is disabled to avoid Gradle build configuration
notification.oldGradle.firstLoad.info=<div width=400>\
<p>The Gradle configuration phase required to be run to get Script Configuration. \
It is disabled by default since it can be expensive for large Gradle projects.</p>\
<br/>\
<p>Click "Load Configuration" to load evaluate Gradle build scripts.</p>\
<br/>\
<p>Alternatively, you can "Enable auto-reload" for all files to automatically load \
configuration on the first opening and reload it on each change of build script configuration blocks.\
This option is not recommended for a big Gradle project with a long configuration time.</p>\
</div>
gradle.script.configurations.importing.feature=Load gradle kotlin scripts configuration from project importing
notification.outsideAnything.text=Code insight unavailable (related Gradle project not linked)
notification.outsideAnything.linkAction=Link Gradle project
@@ -21,10 +21,11 @@ import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.core.util.KotlinIdeaCoreBundle
import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActions
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActionsManager
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.*
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.roots.Imported
import java.io.File
class GradleScriptNotificationProvider(private val project: Project) :
@@ -35,10 +36,61 @@ class GradleScriptNotificationProvider(private val project: Project) :
if (!isGradleKotlinScript(file)) return null
if (file.fileType != KotlinFileType.INSTANCE) return null
val standaloneScriptActions = GradleStandaloneScriptActionsManager.getInstance(project)
val rootsManager = GradleBuildRootsManager.getInstance(project)
val scriptUnderRoot = rootsManager.findScriptBuildRoot(file) ?: return null
// todo: this actions will be usefull only when gradle fix https://github.com/gradle/gradle/issues/12640
fun EditorNotificationPanel.showActionsToFixNotEvaluated() {
// suggest to reimport project if something changed after import
val build: Imported = scriptUnderRoot.nearest as? Imported ?: return
val importTs = build.data.importTs
if (!build.areRelatedFilesChangedBefore(file, importTs)) {
createActionLabel(getMissingConfigurationActionText()) {
rootsManager.updateStandaloneScripts {
runPartialGradleImport(project, build)
}
}
}
// suggest to choose new gradle project
createActionLabel(KotlinIdeaGradleBundle.message("notification.outsideAnything.linkAction")) {
linkProject(project, scriptUnderRoot)
}
}
return when (scriptUnderRoot.notificationKind) {
dontCare -> null
legacy -> {
val actions = standaloneScriptActions[file]
if (actions == null) null
else {
object : EditorNotificationPanel() {
val contextHelp = KotlinIdeaGradleBundle.message("notification.oldGradle.firstLoad.info")
init {
if (actions.isFirstLoad) {
text(KotlinIdeaGradleBundle.message("notification.oldGradle.firstLoad"))
toolTipText = contextHelp
} else {
text(KotlinIdeaCoreBundle.message("notification.text.script.configuration.has.been.changed"))
}
createActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.load.script.configuration")) {
actions.reload()
}
createActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.enable.auto.reload")) {
actions.enableAutoReload()
}
if (actions.isFirstLoad) {
contextHelp(contextHelp)
}
}
}
}
}
legacyOutside -> EditorNotificationPanel().apply {
text("Out of project script")
createActionLabel(KotlinIdeaGradleBundle.message("notification.notEvaluatedInLastImport.addAsStandaloneAction")) {
@@ -66,22 +118,7 @@ class GradleScriptNotificationProvider(private val project: Project) :
text(KotlinIdeaGradleBundle.message("notification.notEvaluatedInLastImport.text"))
// todo: this actions will be usefull only when gradle fix https://github.com/gradle/gradle/issues/12640
// suggest to reimport project if something changed after import
// val root = scriptUnderRoot.nearest as? Imported
val importTs = root?.data?.importTs
// if (root != null && importTs != null && !root.areRelatedFilesChangedBefore(file, importTs)) {
// createActionLabel(getMissingConfigurationActionText()) {
// rootsManager.updateStandaloneScripts {
// runPartialGradleImport(project, root)
// }
// }
// }
// todo: this actions will be usefull only when gradle fix https://github.com/gradle/gradle/issues/12640
// suggest to choose new gradle project
// createActionLabel(KotlinIdeaGradleBundle.message("notification.outsideAnything.linkAction")) {
// linkProject(project, scriptUnderRoot)
// }
// showActionsToFixNotEvaluated()
createActionLabel(KotlinIdeaGradleBundle.message("notification.notEvaluatedInLastImport.addAsStandaloneAction")) {
rootsManager.updateStandaloneScripts {
@@ -92,7 +129,7 @@ class GradleScriptNotificationProvider(private val project: Project) :
contextHelp(KotlinIdeaGradleBundle.message("notification.notEvaluatedInLastImport.info"))
}
standalone -> EditorNotificationPanel().apply {
val actions = GradleStandaloneScriptActions.getInstance(project).byFile[file]
val actions = standaloneScriptActions[file]
if (actions != null) {
text(
KotlinIdeaGradleBundle.message("notification.standalone.text") +
@@ -100,13 +137,12 @@ class GradleScriptNotificationProvider(private val project: Project) :
KotlinIdeaCoreBundle.message("notification.text.script.configuration.has.been.changed")
)
createActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.load.script.configuration")) {
actions.reload()
}
createActionLabel(KotlinIdeaCoreBundle.message("notification.action.text.enable.auto.reload")) {
actions.toggleAutoReload()
actions.enableAutoReload()
}
} else {
text(KotlinIdeaGradleBundle.message("notification.standalone.text"))
@@ -10,7 +10,6 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.LoadScriptConfigurationNotificationFactory
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
@@ -30,24 +29,22 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
class GradleLegacyScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
private val buildRootsManager = GradleBuildRootsManager.getInstance(project)
override fun interceptBackgroundLoading(file: VirtualFile, doLoad: () -> Unit): Boolean {
override fun interceptBackgroundLoading(file: VirtualFile, isFirstLoad: Boolean, doLoad: () -> Unit): Boolean {
if (!isGradleKotlinScript(file)) return false
val info = buildRootsManager.findScriptBuildRoot(file) ?: return false
if (info.standalone) {
val actionsManager = GradleStandaloneScriptActions.getInstance(project)
val fileActions = actionsManager.ForFile(file, doLoad)
actionsManager.byFile[file] = fileActions
fileActions.updateNotification()
} else {
LoadScriptConfigurationNotificationFactory.showNotification(file, project) {
doLoad()
}
GradleStandaloneScriptActionsManager.getInstance(project).add {
GradleStandaloneScriptActions(it, file, isFirstLoad, doLoad)
}
return true
}
override fun hideInterceptedNotification(file: VirtualFile) {
if (!isGradleKotlinScript(file)) return
GradleStandaloneScriptActionsManager.getInstance(project).remove(file)
}
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition) = true
override fun loadDependencies(
@@ -60,8 +57,7 @@ class GradleLegacyScriptConfigurationLoader(project: Project) : DefaultScriptCon
if (!isGradleKotlinScript(vFile)) return false
GradleStandaloneScriptActions.getInstance(project)
.byFile.remove(vFile)?.updateNotification()
hideInterceptedNotification(vFile)
if (!buildRootsManager.isAffectedGradleProjectFile(vFile.path)) {
// not known gradle file and not configured as standalone script
@@ -5,58 +5,51 @@
package org.jetbrains.kotlin.idea.scripting.gradle.legacy
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition
class GradleStandaloneScriptActions(val project: Project) {
val byFile = mutableMapOf<VirtualFile, ForFile>()
class GradleStandaloneScriptActions(
val manager: GradleStandaloneScriptActionsManager,
val file: VirtualFile,
val isFirstLoad: Boolean,
private val doLoad: () -> Unit
) {
val project get() = manager.project
inner class ForFile(
val file: VirtualFile,
private val doLoad: () -> Unit
) {
private val scriptDefinition
get() = file.findScriptDefinition(project)
private val scriptDefinition
get() = file.findScriptDefinition(project)
val isAutoReloadAvailable: Boolean
val isAutoReloadEnabled: Boolean
val isAutoReloadAvailable: Boolean
val isAutoReloadEnabled: Boolean
init {
val scriptDefinition = scriptDefinition
if (scriptDefinition != null) {
isAutoReloadAvailable = true
isAutoReloadEnabled = KotlinScriptingSettings.getInstance(project)
.autoReloadConfigurations(scriptDefinition)
} else {
isAutoReloadAvailable = false
isAutoReloadEnabled = false
}
}
fun toggleAutoReload() {
KotlinScriptingSettings.getInstance(project).setAutoReloadConfigurations(scriptDefinition!!, !isAutoReloadEnabled)
updateNotification()
}
fun reload() {
byFile.remove(file)
doLoad()
updateNotification()
}
fun updateNotification() {
GradleBuildRootsManager.getInstance(project).updateNotifications(false) {
it == file.path
}
init {
val scriptDefinition = scriptDefinition
if (scriptDefinition != null) {
isAutoReloadAvailable = true
isAutoReloadEnabled = KotlinScriptingSettings.getInstance(project)
.autoReloadConfigurations(scriptDefinition)
} else {
isAutoReloadAvailable = false
isAutoReloadEnabled = false
}
}
companion object {
fun getInstance(project: Project) =
ServiceManager.getService(project, GradleStandaloneScriptActions::class.java)
fun enableAutoReload() {
KotlinScriptingSettings.getInstance(project).setAutoReloadConfigurations(scriptDefinition!!, true)
doLoad()
updateNotification()
}
fun reload() {
doLoad()
manager.remove(file)
}
fun updateNotification() {
GradleBuildRootsManager.getInstance(project).updateNotifications(false) {
it == file.path
}
}
}
@@ -0,0 +1,39 @@
/*
* 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.scripting.gradle.legacy
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.TestOnly
class GradleStandaloneScriptActionsManager(val project: Project) {
private val byFile = mutableMapOf<VirtualFile, GradleStandaloneScriptActions>()
operator fun get(file: VirtualFile): GradleStandaloneScriptActions? = byFile[file]
fun add(actionsProvider: (GradleStandaloneScriptActionsManager) -> GradleStandaloneScriptActions) {
val actions = actionsProvider(this)
byFile[actions.file] = actions
actions.updateNotification()
}
fun remove(file: VirtualFile) {
byFile.remove(file)?.updateNotification()
}
@TestOnly
fun performSuggestedLoading(file: VirtualFile): Boolean {
val actions = byFile[file] ?: return false
actions.reload()
return true
}
companion object {
fun getInstance(project: Project) =
ServiceManager.getService(project, GradleStandaloneScriptActionsManager::class.java)
}
}
@@ -63,7 +63,8 @@ abstract class GradleBuildRootsLocator {
@Suppress("EnumEntryName")
enum class NotificationKind {
dontCare, // one of: imported, inside linked legacy gradle build
dontCare, // one of: imported,
legacy, // inside linked legacy gradle build
legacyOutside, // gradle 6-: suggest to mark as standalone
outsideAnything, // suggest link related gradle build or just say that there is no one
wasNotImportedAfterCreation, // project not yet imported after this file was created
@@ -93,7 +94,7 @@ abstract class GradleBuildRootsLocator {
else -> when (nearest) {
is Legacy -> when {
root == null -> NotificationKind.legacyOutside
else -> NotificationKind.dontCare
else -> NotificationKind.legacy
}
is New -> NotificationKind.wasNotImportedAfterCreation
is Imported -> when {
@@ -146,7 +146,10 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(),
if (oldRoot is Imported && oldRoot.data.models.isEmpty()) return
}
if (oldRoot is Legacy) return
if (oldRoot is Legacy) {
oldRoot.importing.set(updated)
return
}
oldRoot.importing.set(updatingCaches)
@@ -11,9 +11,9 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.idea.core.script.LoadScriptConfigurationNotificationFactory
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.script.AbstractScriptConfigurationLoadingTest
import org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActionsManager
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners
@@ -283,7 +283,7 @@ open class GradleScriptListenerTest : AbstractScriptConfigurationLoadingTest() {
assertTrue(
"reloading configuration should be suggested",
LoadScriptConfigurationNotificationFactory.performSuggestedLoading(file.virtualFile, project)
GradleStandaloneScriptActionsManager.getInstance(project).performSuggestedLoading(file.virtualFile)
)
}
+1 -2
View File
@@ -43,8 +43,7 @@
defaultValue="false"
restartRequired="false"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActions"
serviceImplementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActions"/>
<projectService serviceImplementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActionsManager"/>
</extensions>
<actions>
+1 -2
View File
@@ -45,8 +45,7 @@
defaultValue="true"
restartRequired="false"/>
<projectService serviceInterface="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActions"
serviceImplementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActions"/>
<projectService serviceImplementation="org.jetbrains.kotlin.idea.scripting.gradle.legacy.GradleStandaloneScriptActionsManager"/>
</extensions>
<actions>