[kotlin] K2: improve floating toolbar for main.kts script

#KTIJ-33496

GitOrigin-RevId: fad635fd00120351fe0ed55b4edf5bef076c829d
This commit is contained in:
Vlad Koshkin
2025-06-27 04:05:21 +00:00
committed by intellij-monorepo-bot
parent 9f0baea357
commit ae85f0e33f
2 changed files with 128 additions and 24 deletions
@@ -18,6 +18,7 @@
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<psi.implicitReferenceProvider implementation="org.jetbrains.kotlin.idea.core.script.k2.codeInsight.MainKtsReferenceProvider"/>
<resolveScopeProvider implementation="org.jetbrains.kotlin.idea.core.script.k2.dependencies.KotlinScriptResolveScopeProvider"/>
@@ -38,7 +39,7 @@
<editorNotificationProvider implementation="org.jetbrains.kotlin.idea.core.script.k2.notifications.ScriptingSupportChecker"/>
<registryKey defaultValue="false" key="kotlin.scripting.show.widget"
description="Enable displaying Kotlin Script definition name in status bar" restartRequired="false" />
description="Enable displaying Kotlin Script definition name in status bar" restartRequired="false"/>
<registryKey defaultValue="false"
key="kotlin.k2.scripting.show.modules"
@@ -68,7 +69,12 @@
<configuration.moduleStructureFilterExtension
implementation="org.jetbrains.kotlin.idea.core.script.k2.modules.KotlinScriptsProjectStructureFilter"/>
<trafficLightRendererContributor implementation="org.jetbrains.kotlin.idea.core.script.k2.highlighting.ScriptTrafficLightRendererContributor"/>
<trafficLightRendererContributor
implementation="org.jetbrains.kotlin.idea.core.script.k2.highlighting.ScriptTrafficLightRendererContributor"/>
<editorFloatingToolbarProvider
implementation="org.jetbrains.kotlin.idea.core.script.k2.ScriptConfigurationFloatingToolbarProvider"
id="ScriptConfigurationFloatingToolbarProvider"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
@@ -80,18 +86,23 @@
order="last"
implementation="org.jetbrains.kotlin.idea.core.script.k2.definitions.BundledScriptDefinitionSource"/>
<k2IdeScriptAdditionalIdeaDependenciesProvider implementation="org.jetbrains.kotlin.idea.core.script.k2.modules.MainKtsScriptDependenciesProvider"/>
<k2IdeScriptAdditionalIdeaDependenciesProvider
implementation="org.jetbrains.kotlin.idea.core.script.k2.modules.MainKtsScriptDependenciesProvider"/>
<scriptDefinitionsSource implementation="org.jetbrains.kotlin.idea.core.script.k2.definitions.MainKtsScriptDefinitionSource"/>
<scriptDefinitionsSource implementation="org.jetbrains.kotlin.idea.core.script.k2.definitions.ScriptTemplatesFromDependenciesDefinitionSource"/>
<scriptDefinitionsSource
implementation="org.jetbrains.kotlin.idea.core.script.k2.definitions.ScriptTemplatesFromDependenciesDefinitionSource"/>
</extensions>
<actions resource-bundle="messages.KotlinBaseScriptingBundle">
<group id="KotlinScripts.ReloadConfigurationActionGroup"/>
<action id="ReloadScriptConfiguration"
class="org.jetbrains.kotlin.idea.core.script.k2.ReloadScriptConfiguration"
icon="org.jetbrains.kotlin.idea.KotlinIcons.LOAD_SCRIPT_CONFIGURATION">
<keyboard-shortcut keymap="$default" first-keystroke="meta shift C"/>
<add-to-group group-id="RunContextGroup"/>
<add-to-group group-id="KotlinScripts.ReloadConfigurationActionGroup"/>
</action>
</actions>
</idea-plugin>
@@ -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<Boolean>("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<Listener> = 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
}