diff --git a/plugins/kotlin/base/scripting.k2/resources/kotlin.base.scripting.k2.xml b/plugins/kotlin/base/scripting.k2/resources/kotlin.base.scripting.k2.xml index 1c418eccde15..ddeffb608c63 100644 --- a/plugins/kotlin/base/scripting.k2/resources/kotlin.base.scripting.k2.xml +++ b/plugins/kotlin/base/scripting.k2/resources/kotlin.base.scripting.k2.xml @@ -18,6 +18,7 @@ + @@ -38,7 +39,7 @@ + description="Enable displaying Kotlin Script definition name in status bar" restartRequired="false"/> - + + + @@ -80,18 +86,23 @@ order="last" implementation="org.jetbrains.kotlin.idea.core.script.k2.definitions.BundledScriptDefinitionSource"/> - + - + + + + \ No newline at end of file diff --git a/plugins/kotlin/base/scripting.k2/src/org/jetbrains/kotlin/idea/core/script/k2/ReloadScriptConfiguration.kt b/plugins/kotlin/base/scripting.k2/src/org/jetbrains/kotlin/idea/core/script/k2/ReloadScriptConfiguration.kt index 0d08985aca14..a4643a4e9d78 100644 --- a/plugins/kotlin/base/scripting.k2/src/org/jetbrains/kotlin/idea/core/script/k2/ReloadScriptConfiguration.kt +++ b/plugins/kotlin/base/scripting.k2/src/org/jetbrains/kotlin/idea/core/script/k2/ReloadScriptConfiguration.kt @@ -1,44 +1,81 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.jetbrains.kotlin.idea.core.script.k2 -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.* +import com.intellij.openapi.actionSystem.CommonDataKeys.PROJECT +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.readAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.toolbar.floating.AbstractFloatingToolbarProvider +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponent +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiManager +import com.intellij.util.application +import com.intellij.util.messages.Topic import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.intellij.lang.annotations.Language +import org.jetbrains.annotations.ApiStatus import org.jetbrains.kotlin.idea.base.scripting.KotlinBaseScriptingBundle import org.jetbrains.kotlin.idea.core.script.alwaysVirtualFile +import org.jetbrains.kotlin.idea.core.script.k2.ReloadScriptConfigurationService.Companion.TOPIC import org.jetbrains.kotlin.idea.core.script.k2.configurations.getConfigurationResolver import org.jetbrains.kotlin.idea.core.script.k2.highlighting.DefaultScriptResolutionStrategy import org.jetbrains.kotlin.idea.core.script.k2.modules.KotlinScriptModuleManager.Companion.removeScriptModules +import org.jetbrains.kotlin.idea.core.script.scriptDiagnostics import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.scripting.definitions.findScriptDefinition +import org.jetbrains.kotlin.scripting.definitions.isNonScript + +@Language("devkit-action-id") +private const val ACTION_GROUP = "KotlinScripts.ReloadConfigurationActionGroup" + +private val SHOW_NOTIFICATION = Key.create("SHOW_NOTIFICATION") + +@ApiStatus.Internal +class ScriptConfigurationFloatingToolbarProvider : AbstractFloatingToolbarProvider(ACTION_GROUP) { + override val autoHideable: Boolean = false + + override fun isApplicable(dataContext: DataContext): Boolean { + val ktFile = getProjectKtFile(dataContext) ?: return false + return ktFile.name.endsWith(".kts") + } + + override fun register(dataContext: DataContext, component: FloatingToolbarComponent, parentDisposable: Disposable) { + val project = dataContext.getData(PROJECT) ?: return + val service = ReloadScriptConfigurationService.getInstance(project) + + application.messageBus.connect(parentDisposable) + .subscribe(TOPIC, object : ReloadScriptConfigurationService.Listener { + override fun onNotificationChanged(virtualFile: VirtualFile) { + service.handleFloatingToolbarComponent(virtualFile, component) + } + }) + } +} internal class ReloadScriptConfiguration : AnAction() { override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return - val ktFile = getProjectKtFile(e) ?: return + val ktFile = getProjectKtFile(e.dataContext) ?: return ReloadScriptConfigurationService.getInstance(project).reloadScriptData(ktFile) } - private fun getProjectKtFile(e: AnActionEvent): KtFile? { - val project = e.project ?: return null - val editor = e.getData(CommonDataKeys.EDITOR) - return if (editor != null) { - PsiDocumentManager.getInstance(project).getPsiFile(editor.document) - } else { - e.getData(CommonDataKeys.PSI_FILE) - } as? KtFile - } - override fun update(e: AnActionEvent) { - val ktFile = getProjectKtFile(e) + val ktFile = getProjectKtFile(e.dataContext) if (ktFile != null && ktFile.name.endsWith(".kts")) { e.presentation.isEnabledAndVisible = true e.presentation.text = KotlinBaseScriptingBundle.message("reload.script.configuration.text", ktFile.name) @@ -51,19 +88,75 @@ internal class ReloadScriptConfiguration : AnAction() { } @Service(Service.Level.PROJECT) -class ReloadScriptConfigurationService(private val project: Project, private val coroutineScope: CoroutineScope) { +class ReloadScriptConfigurationService(private val project: Project, private val scope: CoroutineScope) : Disposable { + init { + EditorFactory.getInstance().eventMulticaster.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + scope.launch { + val virtualFile = FileDocumentManager.getInstance().getFile(event.document)?.takeIf { it.isInLocalFileSystem } + if (virtualFile == null || virtualFile.isNonScript() || !virtualFile.name.endsWith(".main.kts")) return@launch + + val ktFile = readAction { PsiManager.getInstance(project).findFile(virtualFile) } as? KtFile + if (ktFile != null) { + ktFile.putUserData(SHOW_NOTIFICATION, true) + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onNotificationChanged(virtualFile) + } + } + } + }, this) + } + fun reloadScriptData(ktFile: KtFile) { val definition = ktFile.findScriptDefinition() ?: return + val virtualFile = ktFile.alwaysVirtualFile - coroutineScope.launch { - definition.getConfigurationResolver(project).remove(ktFile.alwaysVirtualFile) - project.removeScriptModules(listOf(ktFile.alwaysVirtualFile)) + scope.launch { + definition.getConfigurationResolver(project).remove(virtualFile) + project.removeScriptModules(listOf(virtualFile)) DefaultScriptResolutionStrategy.getInstance(project).execute(ktFile).join() + + ktFile.putUserData(SHOW_NOTIFICATION, false) + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onNotificationChanged(virtualFile) } } + fun handleFloatingToolbarComponent(virtualFile: VirtualFile, component: FloatingToolbarComponent) { + scope.launch { + val ktFile = readAction { PsiManager.getInstance(project).findFile(virtualFile) } as? KtFile + val shouldShow = !virtualFile.scriptDiagnostics.isNullOrEmpty() || ktFile?.getUserData(SHOW_NOTIFICATION) ?: false + + withContext(Dispatchers.EDT) { + if (shouldShow) { + component.scheduleShow() + } else { + component.scheduleHide() + } + } + } + } + + override fun dispose() {} + + interface Listener { + fun onNotificationChanged(virtualFile: VirtualFile) {} + } + companion object { + @JvmField + @Topic.ProjectLevel + val TOPIC: Topic = Topic.create("ReloadScriptConfigurationService", Listener::class.java) + @JvmStatic fun getInstance(project: Project): ReloadScriptConfigurationService = project.service() } } + +private fun getProjectKtFile(context: DataContext): KtFile? { + val project = context.getData(PROJECT) ?: return null + val editor = context.getData(CommonDataKeys.EDITOR) + return if (editor != null) { + PsiDocumentManager.getInstance(project).getPsiFile(editor.document) + } else { + context.getData(CommonDataKeys.PSI_FILE) + } as? KtFile +}