.gradle.kts, listener: support multiple gradle projects linked to one IntelliJ project

GradleScriptInputsWatcher moved to GradleScriptListener.kt and GradleBuildRootsManager.kt
This commit is contained in:
Sergey Rostov
2020-05-06 16:51:57 +03:00
parent 02940f9f17
commit 39c6efd40f
16 changed files with 175 additions and 150 deletions
@@ -16,12 +16,9 @@ import org.jetbrains.kotlin.psi.KtFile
/** /**
* [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed. * [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed.
* * Listener should do something to invalidate configuration and schedule reloading.
* Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading.
*
* @see DefaultScriptConfigurationManager for more details.
*/ */
abstract class ScriptChangeListener(protected val project: Project) { abstract class ScriptChangeListener(val project: Project) {
val default: DefaultScriptingSupport val default: DefaultScriptingSupport
get() = DefaultScriptingSupport.getInstance(project) get() = DefaultScriptingSupport.getInstance(project)
@@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent
fun addVfsListener(watcher: GradleScriptInputsWatcher) { fun addVfsListener(watcher: GradleScriptListener) {
VirtualFileManager.getInstance().addAsyncFileListener( VirtualFileManager.getInstance().addAsyncFileListener(
object : AsyncFileChangeListenerBase() { object : AsyncFileChangeListenerBase() {
override fun isRelevant(path: String): Boolean { override fun isRelevant(path: String): Boolean {
@@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.concurrency.SequentialTaskExecutor import com.intellij.util.concurrency.SequentialTaskExecutor
fun addVfsListener(watcher: GradleScriptInputsWatcher) { fun addVfsListener(watcher: GradleScriptListener) {
watcher.project.messageBus.connect().subscribe( watcher.project.messageBus.connect().subscribe(
VirtualFileManager.VFS_CHANGES, VirtualFileManager.VFS_CHANGES,
object : BulkFileListener { object : BulkFileListener {
@@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent
fun addVfsListener(watcher: GradleScriptInputsWatcher) { fun addVfsListener(watcher: GradleScriptListener) {
VirtualFileManager.getInstance().addAsyncFileListener( VirtualFileManager.getInstance().addAsyncFileListener(
object : AsyncFileChangeListenerBase() { object : AsyncFileChangeListenerBase() {
override fun isRelevant(path: String): Boolean { override fun isRelevant(path: String): Boolean {
@@ -5,11 +5,11 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.components.service
import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs import org.jetbrains.kotlin.idea.core.script.configuration.cache.CachedConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
/** /**
@@ -19,11 +19,12 @@ import org.jetbrains.kotlin.psi.KtFile
* 2. When some related file is changed (other gradle script, gradle.properties file) * 2. When some related file is changed (other gradle script, gradle.properties file)
* @see GradleScriptInputsWatcher.areRelatedFilesUpToDate * @see GradleScriptInputsWatcher.areRelatedFilesUpToDate
* *
* [inputsTS] is needed to check if some related file was changed since last update * [lastModifiedTs] is needed to check if some related file was changed since last update
*/ */
data class GradleKotlinScriptConfigurationInputs( data class GradleKotlinScriptConfigurationInputs(
val sections: String, val sections: String,
val inputsTS: Long val lastModifiedTs: Long,
val buildRoot: String? = null
) : CachedConfigurationInputs { ) : CachedConfigurationInputs {
override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean { override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean {
try { try {
@@ -31,7 +32,10 @@ data class GradleKotlinScriptConfigurationInputs(
if (actualStamp.sections != this.sections) return false if (actualStamp.sections != this.sections) return false
return project.service<GradleScriptInputsWatcher>().areRelatedFilesUpToDate(file, inputsTS) return buildRoot == null ||
GradleBuildRootsManager.getInstance(project)
.getBuildRoot(buildRoot)
?.areRelatedFilesUpToDate(file, lastModifiedTs) ?: false
} catch (cancel: ProcessCanceledException) { } catch (cancel: ProcessCanceledException) {
return false return false
} }
@@ -1,91 +0,0 @@
/*
* 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.scripting.gradle
import com.intellij.openapi.components.*
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.TestOnly
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
@State(
name = "KotlinBuildScriptsModificationInfo",
storages = [Storage(StoragePathMacros.CACHE_FILE)]
)
class GradleScriptInputsWatcher(val project: Project) : PersistentStateComponent<GradleScriptInputsWatcher.Storage> {
private var storage = Storage()
private var cachedGradleProjectsRoots: Set<String>? = null
fun getGradleProjectsRoots(): Set<String> {
if (cachedGradleProjectsRoots == null) {
cachedGradleProjectsRoots = computeGradleProjectRoots(project)
}
return cachedGradleProjectsRoots ?: emptySet()
}
fun saveGradleProjectRootsAfterImport(roots: Set<String>) {
val oldRoots = cachedGradleProjectsRoots
if (oldRoots != null && oldRoots.isNotEmpty()) {
cachedGradleProjectsRoots = oldRoots + roots
} else {
cachedGradleProjectsRoots = roots
}
}
private fun computeGradleProjectRoots(project: Project): Set<String> {
val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
if (gradleSettings.getLinkedProjectsSettings().isEmpty()) return setOf()
val projectSettings = gradleSettings.getLinkedProjectsSettings().filterIsInstance<GradleProjectSettings>().firstOrNull()
?: return setOf()
return projectSettings.modules.takeIf { it.isNotEmpty() } ?: setOf(projectSettings.externalProjectPath)
}
fun startWatching() {
addVfsListener(this)
}
fun areRelatedFilesUpToDate(file: VirtualFile, timeStamp: Long): Boolean {
return storage.lastModifiedTimeStampExcept(file.path) < timeStamp
}
class Storage {
private val lastModifiedFiles = LastModifiedFiles()
fun lastModifiedTimeStampExcept(filePath: String): Long {
return lastModifiedFiles.lastModifiedTimeStampExcept(filePath)
}
fun fileChanged(filePath: String, ts: Long) {
lastModifiedFiles.fileChanged(ts, filePath)
}
}
override fun getState(): Storage {
return storage
}
override fun loadState(state: Storage) {
this.storage = state
}
fun fileChanged(filePath: String, ts: Long) {
storage.fileChanged(filePath, ts)
}
fun clearState() {
storage = Storage()
}
@TestOnly
fun clearAndRefillState() {
loadState(project.service<GradleScriptInputsWatcher>().state)
}
}
@@ -5,16 +5,25 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener import org.jetbrains.kotlin.idea.core.script.configuration.listener.ScriptChangeListener
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
class GradleScriptListener(project: Project) : ScriptChangeListener(project) { class GradleScriptListener(project: Project) : ScriptChangeListener(project) {
init { init {
// start GradleScriptInputsWatcher to track changes in gradle-configuration related files // listen changes using VFS events, including gradle-configuration related files
project.service<GradleScriptInputsWatcher>().startWatching() addVfsListener(this)
}
fun fileChanged(filePath: String, ts: Long) {
// NOTE: this is required for legacy roots too
val root = GradleBuildRootsManager.getInstance(project)
.findScriptBuildRoot(filePath, searchNearestLegacy = false)
?.root as? GradleBuildRoot.Linked
root?.fileChanged(filePath, ts)
} }
override fun isApplicable(vFile: VirtualFile) = GradleBuildRootsManager.getInstance(project).isApplicable(vFile) override fun isApplicable(vFile: VirtualFile) = GradleBuildRootsManager.getInstance(project).isApplicable(vFile)
@@ -5,6 +5,19 @@
package org.jetbrains.kotlin.idea.scripting.gradle package org.jetbrains.kotlin.idea.scripting.gradle
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.FileAttribute
import org.jetbrains.kotlin.idea.core.util.readNullable
import org.jetbrains.kotlin.idea.core.util.readStringList
import org.jetbrains.kotlin.idea.core.util.writeNullable
import org.jetbrains.kotlin.idea.core.util.writeStringList
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootData
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootDataSerializer
import org.jetbrains.kotlin.idea.scripting.gradle.roots.readKotlinDslScriptModels
import org.jetbrains.kotlin.idea.scripting.gradle.roots.writeKotlinDslScriptModels
import java.io.DataInputStream
import java.io.DataOutput
/** /**
* Optimized collection for storing last modified files with ability to * Optimized collection for storing last modified files with ability to
* get time of last modified file expect given one ([lastModifiedTimeStampExcept]). * get time of last modified file expect given one ([lastModifiedTimeStampExcept]).
@@ -14,10 +27,10 @@ package org.jetbrains.kotlin.idea.scripting.gradle
* *
* Actually works by storing two last timestamps with the set of files modified at this times. * Actually works by storing two last timestamps with the set of files modified at this times.
*/ */
class LastModifiedFiles { class LastModifiedFiles(
private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles() private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles(),
private var previous: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles() private var previous: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles()
) {
class SimultaneouslyChangedFiles( class SimultaneouslyChangedFiles(
val ts: Long = Long.MIN_VALUE, val ts: Long = Long.MIN_VALUE,
val fileIds: MutableSet<String> = mutableSetOf() val fileIds: MutableSet<String> = mutableSetOf()
@@ -40,4 +53,36 @@ class LastModifiedFiles {
last.fileIds.size == 1 && last.fileIds.contains(fileId) -> previous.ts last.fileIds.size == 1 && last.fileIds.contains(fileId) -> previous.ts
else -> last.ts else -> last.ts
} }
companion object {
private val fileAttribute = FileAttribute("last-modified-files", 1, false)
fun read(buildRoot: VirtualFile): LastModifiedFiles? {
return fileAttribute.readAttribute(buildRoot)?.use {
it.readNullable {
LastModifiedFiles(readSCF(it), readSCF(it))
}
}
}
fun write(buildRoot: VirtualFile, data: LastModifiedFiles?) {
fileAttribute.writeAttribute(buildRoot).use {
it.writeNullable(data) { data ->
writeSCF(data.last)
writeSCF(data.previous)
}
}
}
fun remove(buildRoot: VirtualFile) {
write(buildRoot, null)
}
private fun readSCF(it: DataInputStream) = SimultaneouslyChangedFiles(it.readLong(), it.readStringList().toMutableSet())
private fun DataOutput.writeSCF(last: SimultaneouslyChangedFiles) {
writeLong(last.ts)
writeStringList(last.fileIds)
}
}
} }
@@ -56,9 +56,9 @@ Note that we can provide custom scripting support only for projects that using G
To have consistent sate of scripts, we should also be aware of external script changes. This is achieved by watching files using the IntelliJ VFS events. [GradleScriptInputsWatcher] is responsible for that. To have consistent sate of scripts, we should also be aware of external script changes. This is achieved by watching files using the IntelliJ VFS events. [GradleScriptInputsWatcher] is responsible for that.
The first tricky part is that scripts are depending on each other: so, when one script is changed, we actually should invalidate all other scripts as we don't know dependencies between them (Gradle will provide this information later, but it is not yet implemented). Actually, we should know the last modified timestamp of all scripts excepting a particular one. This can be achieved by storing timestamps of two last modified files. [LastModifiedFiles] utility is responsible for that. This is not multiple linked Gradle builds aware yet (currently there is only one global state), but it will be fixed shortly. The first tricky part is that scripts are depending on each other: so, when one script is changed, we actually should invalidate all other scripts as we don't know dependencies between them (Gradle will provide this information later, but it is not yet implemented). Actually, we should know the last modified timestamp of all scripts excepting a particular one. This can be achieved by storing timestamps of two last modified files. [LastModifiedFiles] utility is responsible for that.
Another tricky part is that we should track only scripts belong the Gradle project and should ignore all other `*.gradle.kts` files (in `testData` for example). This is achieved by storing Gradle project roots, as scripts can be exactly near Gradle project roots (excepting included and precompiled scripts which are not fully supported yet). This can be gathered from the Gradle project import information or from GradleProjectSettings when the import has not occurred yet. `GradleScriptInputsWatcher.cachedGradleProjectsRoots` is responsible for that. Another tricky part is that we should track only scripts belong the Gradle project and should ignore all other `*.gradle.kts` files (in `testData` for example). This is achieved by storing Gradle project roots, as scripts can be exactly near Gradle project roots (excepting included and precompiled scripts which are not fully supported yet). This can be gathered from the Gradle project import information or from GradleProjectSettings when the import has not occurred yet. `GradleBuildRootsManager` does it.
## Out of project Gradle scripts ## Out of project Gradle scripts
@@ -68,7 +68,7 @@ There are scripts that are not linked to any import Gradle project. In `testData
For Gradle versions before 6.0, we are still falling back to the [DefaultScriptingSupport] with some extensions. For Gradle versions before 6.0, we are still falling back to the [DefaultScriptingSupport] with some extensions.
We are showing notification before loading as we knew what changes will case it. This is done by: We are showing notification before loading as we knew what changes will cause it. This is done by:
- implementing `org.jetbrains.kotlin.scripting.idea.listener` extension point and calling `suggestToUpdateConfigurationIfOutOfDate` instead of `ensureUpToDatedConfigurationSuggested` on document changes. - implementing `org.jetbrains.kotlin.scripting.idea.listener` extension point and calling `suggestToUpdateConfigurationIfOutOfDate` instead of `ensureUpToDatedConfigurationSuggested` on document changes.
- implementing `org.jetbrains.kotlin.scripting.idea.loader` extension point and overriding `getInputsStamp` - implementing `org.jetbrains.kotlin.scripting.idea.loader` extension point and overriding `getInputsStamp`
@@ -5,7 +5,6 @@
package org.jetbrains.kotlin.idea.scripting.gradle.importing package org.jetbrains.kotlin.idea.scripting.gradle.importing
import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
@@ -15,9 +14,8 @@ import org.gradle.tooling.model.kotlin.dsl.EditorReportSeverity
import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel import org.gradle.tooling.model.kotlin.dsl.KotlinDslScriptsModel
import org.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel import org.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel
import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptInputsWatcher
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp import org.jetbrains.kotlin.idea.scripting.gradle.getGradleScriptInputsStamp
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.psi.NotNullableUserDataProperty import org.jetbrains.kotlin.psi.NotNullableUserDataProperty
import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext
import java.io.File import java.io.File
@@ -115,14 +113,8 @@ fun saveScriptModels(project: Project, build: KotlinDslGradleBuildSync) {
errorReporter.reportError(File(model.file), model) errorReporter.reportError(File(model.file), model)
} }
project.service<GradleScriptInputsWatcher>().saveGradleProjectRootsAfterImport(
build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }.toSet()
)
// todo: use real info about projects // todo: use real info about projects
build.projectRoots.addAll(build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }) build.projectRoots.addAll(build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) })
GradleBuildRootsManager.getInstance(project).update(build) GradleBuildRootsManager.getInstance(project).update(build)
project.service<GradleScriptInputsWatcher>().clearState()
} }
@@ -10,9 +10,11 @@ import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache import org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsCache
import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptDefinitionsContributor import org.jetbrains.kotlin.idea.scripting.gradle.GradleScriptDefinitionsContributor
import org.jetbrains.kotlin.idea.scripting.gradle.LastModifiedFiles
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition
import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import java.io.File import java.io.File
/** /**
@@ -28,6 +30,7 @@ sealed class GradleBuildRoot {
* The script not related to any Gradle build that is linked to IntelliJ Project, * The script not related to any Gradle build that is linked to IntelliJ Project,
* or we cannot known what is it * or we cannot known what is it
*/ */
@Suppress("CanSealedSubClassBeObject")
class Unlinked : GradleBuildRoot() class Unlinked : GradleBuildRoot()
/** /**
@@ -37,31 +40,65 @@ sealed class GradleBuildRoot {
@Volatile @Volatile
var importing = false var importing = false
abstract val manager: GradleBuildRootsManager
abstract val pathPrefix: String abstract val pathPrefix: String
open val projectRoots: Collection<String> get() = listOf() abstract val projectRoots: Collection<String>
private val dir: VirtualFile?
get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix)
private val lastModifiedFiles =
dir?.let { LastModifiedFiles.read(it) } ?: LastModifiedFiles()
fun areRelatedFilesUpToDate(file: VirtualFile, lastModified: Long): Boolean =
lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified
fun fileChanged(filePath: String, ts: Long) {
lastModifiedFiles.fileChanged(ts, filePath)
manager.scheduleLastModifiedFilesSave()
}
fun saveLastModifiedFiles() {
LastModifiedFiles.write(dir ?: return, lastModifiedFiles)
}
}
abstract class WithoutScriptModels(settings: GradleProjectSettings) : Linked() {
final override val pathPrefix = settings.externalProjectPath!!
final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix)
} }
/** /**
* Gradle build with old Gradle version (<6.0) * Gradle build with old Gradle version (<6.0)
*/ */
class Legacy(override val pathPrefix: String) : Linked() class Legacy(
override val manager: GradleBuildRootsManager,
settings: GradleProjectSettings
) : WithoutScriptModels(settings)
/** /**
* Linked but not yet imported Gradle build. * Linked but not yet imported Gradle build.
*/ */
class New(override val pathPrefix: String) : Linked() class New(
override val manager: GradleBuildRootsManager,
settings: GradleProjectSettings
) : WithoutScriptModels(settings)
/** /**
* Imported Gradle build. * Imported Gradle build.
* Each imported build have info about all of it's Kotlin Build Scripts. * Each imported build have info about all of it's Kotlin Build Scripts.
*/ */
class Imported( class Imported(
val project: Project, override val manager: GradleBuildRootsManager,
val dir: VirtualFile, val dir: VirtualFile,
val javaHome: File?, val javaHome: File?,
val data: GradleBuildRootData val data: GradleBuildRootData
) : Linked() { ) : Linked() {
val project: Project
get() = manager.project
override val pathPrefix: String = dir.path override val pathPrefix: String = dir.path
override val projectRoots: Collection<String> override val projectRoots: Collection<String>
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.idea.core.util.readString
import org.jetbrains.kotlin.idea.core.util.writeNullable import org.jetbrains.kotlin.idea.core.util.writeNullable
import org.jetbrains.kotlin.idea.core.util.writeString import org.jetbrains.kotlin.idea.core.util.writeString
import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs
import org.jetbrains.kotlin.idea.scripting.gradle.LastModifiedFiles
import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel
import java.io.DataInput import java.io.DataInput
import java.io.DataInputStream import java.io.DataInputStream
@@ -23,7 +24,7 @@ internal object GradleBuildRootDataSerializer {
fun read(buildRoot: VirtualFile): GradleBuildRootData? { fun read(buildRoot: VirtualFile): GradleBuildRootData? {
return attribute.readAttribute(buildRoot)?.use { return attribute.readAttribute(buildRoot)?.use {
it.readNullable { it.readNullable {
readKotlinDslScriptModels(it) readKotlinDslScriptModels(it, buildRoot.path)
} }
} }
} }
@@ -38,6 +39,7 @@ internal object GradleBuildRootDataSerializer {
fun remove(buildRoot: VirtualFile) { fun remove(buildRoot: VirtualFile) {
write(buildRoot, null) write(buildRoot, null)
LastModifiedFiles.remove(buildRoot)
} }
} }
@@ -57,14 +59,14 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleBuildRoo
output.writeList(data.models) { output.writeList(data.models) {
strings.writeStringId(it.file) strings.writeStringId(it.file)
output.writeString(it.inputs.sections) output.writeString(it.inputs.sections)
output.writeLong(it.inputs.inputsTS) output.writeLong(it.inputs.lastModifiedTs)
strings.writeStringIds(it.classPath) strings.writeStringIds(it.classPath)
strings.writeStringIds(it.sourcePath) strings.writeStringIds(it.sourcePath)
strings.writeStringIds(it.imports) strings.writeStringIds(it.imports)
} }
} }
internal fun readKotlinDslScriptModels(input: DataInputStream): GradleBuildRootData { internal fun readKotlinDslScriptModels(input: DataInputStream, buildRoot: String): GradleBuildRootData {
val strings = StringsPool.reader(input) val strings = StringsPool.reader(input)
val projectRoots = strings.readStrings() val projectRoots = strings.readStrings()
@@ -73,7 +75,7 @@ internal fun readKotlinDslScriptModels(input: DataInputStream): GradleBuildRootD
val models = input.readList { val models = input.readList {
KotlinDslScriptModel( KotlinDslScriptModel(
strings.readString(), strings.readString(),
GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong()), GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong(), buildRoot),
strings.readStrings(), strings.readStrings(),
strings.readStrings(), strings.readStrings(),
strings.readStrings(), strings.readStrings(),
@@ -13,7 +13,7 @@ class GradleBuildRootIndex {
private val byWorkingDir = HashMap<String, GradleBuildRoot.Linked>() private val byWorkingDir = HashMap<String, GradleBuildRoot.Linked>()
private val byProjectDir = HashMap<String, GradleBuildRoot.Linked>() private val byProjectDir = HashMap<String, GradleBuildRoot.Linked>()
val list: Collection<GradleBuildRoot> val list: Collection<GradleBuildRoot.Linked>
get() = byWorkingDir.values get() = byWorkingDir.values
@Synchronized @Synchronized
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.progress.util.BackgroundTaskUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtil
@@ -30,6 +31,7 @@ import org.jetbrains.plugins.gradle.settings.*
import org.jetbrains.plugins.gradle.util.GradleConstants import org.jetbrains.plugins.gradle.util.GradleConstants
import java.io.File import java.io.File
import java.nio.file.Paths import java.nio.file.Paths
import java.util.concurrent.atomic.AtomicBoolean
/** /**
* [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build). * [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build).
@@ -103,40 +105,52 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider(
get() = path get() = path
fun getScriptInfo(file: VirtualFile): GradleScriptInfo? = fun getScriptInfo(file: VirtualFile): GradleScriptInfo? =
manager.getLightScriptInfo(file.localPath) as? GradleScriptInfo getScriptInfo(file.localPath)
fun getScriptInfo(localPath: String): GradleScriptInfo? =
manager.getLightScriptInfo(localPath) as? GradleScriptInfo
class ScriptUnderRoot( class ScriptUnderRoot(
val root: GradleBuildRoot?, val root: GradleBuildRoot?,
val script: GradleScriptInfo? = null val script: GradleScriptInfo? = null
) )
fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? { fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? =
if (!isGradleKotlinScript(gradleKtsFile)) return null findScriptBuildRoot(gradleKtsFile.path)
val filePath = gradleKtsFile.path fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true): ScriptUnderRoot? {
val scriptInfo = getScriptInfo(gradleKtsFile) if (!filePath.endsWith(".gradle.kts")) return null
val scriptInfo = getScriptInfo(filePath)
val imported = scriptInfo?.buildRoot val imported = scriptInfo?.buildRoot
if (imported != null) return ScriptUnderRoot(imported, scriptInfo) if (imported != null) return ScriptUnderRoot(imported, scriptInfo)
if (gradleKtsFile.name == "build.gradle.kts" || if (filePath.endsWith("/build.gradle.kts") ||
gradleKtsFile.name == "settings.gradle.kts" || filePath.endsWith("/settings.gradle.kts") ||
gradleKtsFile.name == "init.gradle.kts" filePath.endsWith("/init.gradle.kts")
) { ) {
// build|settings|init.gradle.kts scripts should be located near gradle project root only // build|settings|init.gradle.kts scripts should be located near gradle project root only
val gradleBuild = roots.getBuildByProjectDir(gradleKtsFile.parent.localPath) val gradleBuild = roots.getBuildByProjectDir(filePath.substringBeforeLast("/"))
if (gradleBuild != null) return ScriptUnderRoot(gradleBuild) if (gradleBuild != null) return ScriptUnderRoot(gradleBuild)
} }
// other scripts: "included", "precompiled" scripts, scripts in unlinked projects, // other scripts: "included", "precompiled" scripts, scripts in unlinked projects,
// or just random files with ".gradle.kts" ending // or just random files with ".gradle.kts" ending
// try find nearest linked legacy gradle project settings first if (searchNearestLegacy) {
val found = roots.findNearestRoot(filePath) val found = roots.findNearestRoot(filePath)
if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found) if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found)
}
return ScriptUnderRoot(GradleBuildRoot.Unlinked()) return ScriptUnderRoot(GradleBuildRoot.Unlinked())
} }
fun isUnderProjectDir(localPath: String) =
roots.getBuildByProjectDir(localPath) != null
fun getBuildRoot(gradleWorkingDir: String) =
roots.getBuildRoot(gradleWorkingDir)
fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) { fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) {
actualizeBuildRoot(workingDir)?.importing = inProgress actualizeBuildRoot(workingDir)?.importing = inProgress
updateNotifications(workingDir) updateNotifications(workingDir)
@@ -177,6 +191,19 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider(
return GradleBuildRootData(roots, new.templateClasspath, models.values) return GradleBuildRootData(roots, new.templateClasspath, models.values)
} }
private val lastModifiedFilesSaveScheduled = AtomicBoolean()
fun scheduleLastModifiedFilesSave() {
if (lastModifiedFilesSaveScheduled.compareAndSet(false, true)) {
BackgroundTaskUtil.executeOnPooledThread(project) {
lastModifiedFilesSaveScheduled.set(false)
roots.list.forEach {
it.saveLastModifiedFiles()
}
}
}
}
init { init {
getGradleProjectSettings(project).forEach { getGradleProjectSettings(project).forEach {
// don't call this.add, as we are inside scripting manager initialization // don't call this.add, as we are inside scripting manager initialization
@@ -269,8 +296,8 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider(
private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked { private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked {
val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version) val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version)
return when { return when {
supported -> GradleBuildRoot.New(settings.externalProjectPath) supported -> GradleBuildRoot.New(this, settings)
else -> GradleBuildRoot.Legacy(settings.externalProjectPath) else -> GradleBuildRoot.Legacy(this, settings)
} }
} }
@@ -284,7 +311,7 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider(
.getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID) .getExecutionSettings<GradleExecutionSettings>(project, externalProjectPath, GradleConstants.SYSTEM_ID)
.javaHome?.let { File(it) } .javaHome?.let { File(it) }
return GradleBuildRoot.Imported(project, buildRoot, javaHome, data) return GradleBuildRoot.Imported(this, buildRoot, javaHome, data)
} }
private fun add(newRoot: GradleBuildRoot.Linked) { private fun add(newRoot: GradleBuildRoot.Linked) {
@@ -14,6 +14,8 @@ import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.* import com.intellij.psi.*
import com.intellij.psi.impl.source.tree.LeafPsiElement import com.intellij.psi.impl.source.tree.LeafPsiElement
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRoot
import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager
import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
@@ -36,7 +38,7 @@ fun isInAffectedGradleProjectFiles(project: Project, filePath: String): Boolean
return true return true
} }
return filePath.substringBeforeLast("/") in project.service<GradleScriptInputsWatcher>().getGradleProjectsRoots() return GradleBuildRootsManager.getInstance(project).isUnderProjectDir(filePath.substringBeforeLast("/"))
} }
return false return false
@@ -78,7 +80,8 @@ fun getGradleScriptInputsStamp(
} }
} }
GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp) val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot.Linked
GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp, buildRoot?.pathPrefix)
} else null } else null
} }
} }
@@ -25,7 +25,7 @@ class GradleBuildRootDataSerializerTest {
listOf( listOf(
KotlinDslScriptModel( KotlinDslScriptModel(
"a", "a",
GradleKotlinScriptConfigurationInputs("b", 1), GradleKotlinScriptConfigurationInputs("b", 1, "a"),
listOf("c", "a", "b"), listOf("c", "a", "b"),
listOf("b", "c", "a"), listOf("b", "c", "a"),
listOf("i", "c", "b"), listOf("i", "c", "b"),
@@ -33,7 +33,7 @@ class GradleBuildRootDataSerializerTest {
), ),
KotlinDslScriptModel( KotlinDslScriptModel(
"a", "a",
GradleKotlinScriptConfigurationInputs("b", 1), GradleKotlinScriptConfigurationInputs("b", 1, "a"),
listOf("c", "a", "b"), listOf("c", "a", "b"),
listOf("b", "c", "a"), listOf("b", "c", "a"),
listOf("i", "c", "b"), listOf("i", "c", "b"),
@@ -45,7 +45,7 @@ class GradleBuildRootDataSerializerTest {
val buffer = ByteArrayOutputStream() val buffer = ByteArrayOutputStream()
writeKotlinDslScriptModels(DataOutputStream(buffer), data) writeKotlinDslScriptModels(DataOutputStream(buffer), data)
val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray()))) val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray())), "a")
assertEquals(data.toString(), restored.toString()) assertEquals(data.toString(), restored.toString())
} }