Implement Gradle specific behavior for script configuration update logic

We have plans to extract separate implementation for Gradle scripts, but it
would be better to support something at this point.
For now, we have custom listener, loader and up-to-date check for default configuration manager:
- listener will do forced reload on editor activation, even it is already up-to-date
this is required for Gradle scripts, since it's classpath may depend on other files (`.properties` for example)
- loader will force save failures before running loader. also it will skip loading if
`kotlin.gradle.scripts.useIdeaProjectImport` registry key is enabled
- loader will return inputs with overridden up-to-date checks: file will be considered
out-of-date only when `buildscript` or `plugins` blocks are changed
This commit is contained in:
Sergey Rostov
2019-10-31 11:17:14 +03:00
committed by Natalia Selezneva
parent 548e8e27dc
commit 19d35d7f27
3 changed files with 152 additions and 9 deletions
@@ -15,16 +15,15 @@ import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.*
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationSnapshot
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.*
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache
import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache
import org.jetbrains.kotlin.idea.core.script.configuration.listener.DefaultScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.*
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptOutsiderFileConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.utils.BackgroundExecutor
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangesNotifier
import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.psi.KtFile
@@ -34,6 +33,7 @@ import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.script.experimental.api.ScriptDiagnostic
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
/**
* Standard implementation of scripts configuration loading and caching
@@ -100,10 +100,12 @@ internal class DefaultScriptConfigurationManager(project: Project) :
private val loaders: List<ScriptConfigurationLoader> = listOf(
ScriptOutsiderFileConfigurationLoader(project),
ScriptConfigurationFileAttributeCache(project),
GradleScriptConfigurationLoader(project),
DefaultScriptConfigurationLoader(project)
)
private val listeners: List<ScriptChangeListener> = listOf(
GradleScriptListener(),
DefaultScriptChangeListener()
)
@@ -0,0 +1,137 @@
/*
* Copyright 2010-2019 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.configuration
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiRecursiveElementVisitor
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.core.script.configuration.listener.DefaultScriptChangeListener
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptConfigurationUpdater
import org.jetbrains.kotlin.idea.core.script.configuration.loader.DefaultScriptConfigurationLoader
import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext
import org.jetbrains.kotlin.idea.core.util.EDT
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
class GradleScriptListener : DefaultScriptChangeListener() {
private val listenGradleRelatedFiles = false // todo
override fun editorActivated(file: KtFile, updater: ScriptConfigurationUpdater): Boolean {
if (!isGradleKotlinScript(file.virtualFile)) return false
if (listenGradleRelatedFiles) {
updater.ensureUpToDatedConfigurationSuggested(file)
// todo: force reload if related files changed
} else {
// configuration will be reloaded after editor activation, even it is already up-to-date
// this is required for Gradle scripts, since it's classpath may depend on other files (`.properties` for example)
updater.forceConfigurationReload(file)
}
return true
}
}
class GradleScriptConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) {
private val useProjectImport: Boolean
get() = Registry.`is`("kotlin.gradle.scripts.useIdeaProjectImport", false)
override fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean {
return if (useProjectImport) false else super.shouldRunInBackground(scriptDefinition)
}
override fun loadDependencies(
isFirstLoad: Boolean,
virtualFile: VirtualFile,
scriptDefinition: ScriptDefinition,
context: ScriptConfigurationLoadingContext
): Boolean {
if (!isGradleKotlinScript(virtualFile)) return false
if (useProjectImport) {
// do nothing, project import notification will be already showed
// and configuration for gradle build scripts will be saved at the end of import
// todo: use default configuration loader for out-of-project scripts?
return true
} else {
// Gradle read files from FS
GlobalScope.launch(EDT(project)) {
runWriteAction {
FileDocumentManager.getInstance().saveAllDocuments()
}
}
return super.loadDependencies(isFirstLoad, virtualFile, scriptDefinition, context)
}
}
override fun getInputsStamp(file: KtFile): CachedConfigurationInputs {
return getGradleScriptInputsStamp(project, file.virtualFile, file) ?: super.getInputsStamp(file)
}
}
data class GradleKotlinScriptConfigurationInputs(
val buildScriptAndPluginsSections: String
) : CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean {
val actualStamp = getGradleScriptInputsStamp(project, file, ktFile)
return actualStamp == this
}
}
fun isGradleKotlinScript(virtualFile: VirtualFile) = virtualFile.name.endsWith(".gradle.kts")
fun getGradleScriptInputsStamp(
project: Project,
file: VirtualFile,
givenKtFile: KtFile? = null
): GradleKotlinScriptConfigurationInputs? {
if (!isGradleKotlinScript(file)) return null
return runReadAction {
val ktFile = givenKtFile ?: PsiManager.getInstance(project).findFile(file) as? KtFile
if (ktFile != null) {
val result = StringBuilder()
ktFile.script?.blockExpression
?.getChildrenOfType<KtScriptInitializer>()
?.forEach {
val call = it.children.singleOrNull() as? KtCallExpression
val callRef = call?.firstChild?.text
if (callRef == "buildscript" || callRef == "plugins") {
result.append(callRef)
val lambda = call.lambdaArguments.singleOrNull()
lambda?.accept(object : PsiRecursiveElementVisitor(false) {
override fun visitElement(element: PsiElement) {
super.visitElement(element)
when (element) {
is PsiWhiteSpace -> if (element.text.contains("\n")) result.append("\n")
is LeafPsiElement -> result.append(element.text)
}
}
})
result.append("\n")
}
}
GradleKotlinScriptConfigurationInputs(result.toString())
} else null
}
}
+8 -4
View File
@@ -78,11 +78,11 @@
implementation="org.jetbrains.kotlin.idea.script.ScriptTemplatesFromDependenciesProvider"/>
<scriptDefinitionContributor id="StandardScriptDefinitionContributor"
order="last"
implementation="org.jetbrains.kotlin.idea.core.script.StandardScriptDefinitionContributor"/>
order="last"
implementation="org.jetbrains.kotlin.idea.core.script.StandardScriptDefinitionContributor"/>
<scriptDefinitionContributor id="ConsoleScriptDefinitionContributor"
implementation="org.jetbrains.kotlin.console.ConsoleScriptDefinitionContributor"/>
implementation="org.jetbrains.kotlin.console.ConsoleScriptDefinitionContributor"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JvmPlatformKindResolution"/>
<idePlatformKindResolution implementation="org.jetbrains.kotlin.caches.resolve.JsPlatformKindResolution"/>
@@ -93,7 +93,7 @@
<j2kConverterExtension implementation="org.jetbrains.kotlin.idea.j2k.OldJ2kConverterExtension"/>
<j2kConverterExtension implementation="org.jetbrains.kotlin.nj2k.NewJ2kConverterExtension"/>
</extensions>
</extensions>
<extensions defaultExtensionNs="com.intellij">
<registryKey key="kotlin.mpp.editTargetPlatformEnabled"
@@ -104,6 +104,10 @@
description="Add all transitive dependencies on MPP modules when Android project is imported"
defaultValue="true"
restartRequired="false"/>
<registryKey key="kotlin.gradle.scripts.useIdeaProjectImport"
description="Use IDEA project import mechanics to load Kotlin Gradle scripts configuration"
defaultValue="false"
restartRequired="false"/>
<fileEditorProvider implementation="org.jetbrains.kotlin.idea.scratch.ui.KtScratchFileEditorProvider"/>
</extensions>