From cde53285c62cc4d09779a248b55e50a0f39e71ca Mon Sep 17 00:00:00 2001 From: Sergey Rostov Date: Wed, 6 May 2020 16:51:57 +0300 Subject: [PATCH] .gradle.kts, listener: support multiple gradle projects linked to one IntelliJ project GradleScriptInputsWatcher moved to GradleScriptListener.kt and GradleBuildRootsManager.kt --- .../listener/ScriptChangeListener.kt | 7 +- .../gradle/AsyncFileChangeListenerHelper.kt | 2 +- .../AsyncFileChangeListenerHelper.kt.191 | 2 +- .../AsyncFileChangeListenerHelper.kt.201 | 2 +- .../GradleKotlinScriptConfigurationInputs.kt | 12 ++- .../gradle/GradleScriptInputsWatcher.kt | 91 ------------------- .../scripting/gradle/GradleScriptListener.kt | 15 ++- .../scripting/gradle/LastModifiedFiles.kt | 51 ++++++++++- .../kotlin/idea/scripting/gradle/README.md | 6 +- .../KotlinDslScrtiptModelProcessor.kt | 10 +- .../scripting/gradle/roots/GradleBuildRoot.kt | 45 ++++++++- .../roots/GradleBuildRootDataSerializer.kt | 10 +- .../gradle/roots/GradleBuildRootIndex.kt | 2 +- .../gradle/roots/GradleBuildRootsManager.kt | 57 +++++++++--- .../kotlin/idea/scripting/gradle/utils.kt | 7 +- .../GradleBuildRootDataSerializerTest.kt | 6 +- 16 files changed, 175 insertions(+), 150 deletions(-) delete mode 100644 idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptInputsWatcher.kt diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt index e8be01fb80d..d97641f6878 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/listener/ScriptChangeListener.kt @@ -16,12 +16,9 @@ import org.jetbrains.kotlin.psi.KtFile /** * [ScriptChangesNotifier] will call first applicable [ScriptChangeListener] when editor is activated or document changed. - * - * Listener may call [ScriptConfigurationUpdater] to invalidate configuration and schedule reloading. - * - * @see DefaultScriptConfigurationManager for more details. + * Listener should do something to invalidate configuration and schedule reloading. */ -abstract class ScriptChangeListener(protected val project: Project) { +abstract class ScriptChangeListener(val project: Project) { val default: DefaultScriptingSupport get() = DefaultScriptingSupport.getInstance(project) diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt index 342b8d72c36..4e50a87b489 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt @@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.newvfs.events.VFileEvent -fun addVfsListener(watcher: GradleScriptInputsWatcher) { +fun addVfsListener(watcher: GradleScriptListener) { VirtualFileManager.getInstance().addAsyncFileListener( object : AsyncFileChangeListenerBase() { override fun isRelevant(path: String): Boolean { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.191 b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.191 index 01160790959..72d756b56c2 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.191 +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.191 @@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.newvfs.BulkFileListener import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.util.concurrency.SequentialTaskExecutor -fun addVfsListener(watcher: GradleScriptInputsWatcher) { +fun addVfsListener(watcher: GradleScriptListener) { watcher.project.messageBus.connect().subscribe( VirtualFileManager.VFS_CHANGES, object : BulkFileListener { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.201 b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.201 index 16509e41bfe..dda0beb20c8 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.201 +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/AsyncFileChangeListenerHelper.kt.201 @@ -10,7 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.newvfs.events.VFileEvent -fun addVfsListener(watcher: GradleScriptInputsWatcher) { +fun addVfsListener(watcher: GradleScriptListener) { VirtualFileManager.getInstance().addAsyncFileListener( object : AsyncFileChangeListenerBase() { override fun isRelevant(path: String): Boolean { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleKotlinScriptConfigurationInputs.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleKotlinScriptConfigurationInputs.kt index 9cfb1f51c23..001172412d9 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleKotlinScriptConfigurationInputs.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleKotlinScriptConfigurationInputs.kt @@ -5,11 +5,11 @@ package org.jetbrains.kotlin.idea.scripting.gradle -import com.intellij.openapi.components.service import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile 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 /** @@ -19,11 +19,12 @@ import org.jetbrains.kotlin.psi.KtFile * 2. When some related file is changed (other gradle script, gradle.properties file) * @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( val sections: String, - val inputsTS: Long + val lastModifiedTs: Long, + val buildRoot: String? = null ) : CachedConfigurationInputs { override fun isUpToDate(project: Project, file: VirtualFile, ktFile: KtFile?): Boolean { try { @@ -31,7 +32,10 @@ data class GradleKotlinScriptConfigurationInputs( if (actualStamp.sections != this.sections) return false - return project.service().areRelatedFilesUpToDate(file, inputsTS) + return buildRoot == null || + GradleBuildRootsManager.getInstance(project) + .getBuildRoot(buildRoot) + ?.areRelatedFilesUpToDate(file, lastModifiedTs) ?: false } catch (cancel: ProcessCanceledException) { return false } diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptInputsWatcher.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptInputsWatcher.kt deleted file mode 100644 index 8215b419414..00000000000 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptInputsWatcher.kt +++ /dev/null @@ -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 { - private var storage = Storage() - - private var cachedGradleProjectsRoots: Set? = null - - fun getGradleProjectsRoots(): Set { - if (cachedGradleProjectsRoots == null) { - cachedGradleProjectsRoots = computeGradleProjectRoots(project) - } - return cachedGradleProjectsRoots ?: emptySet() - } - - fun saveGradleProjectRootsAfterImport(roots: Set) { - val oldRoots = cachedGradleProjectsRoots - if (oldRoots != null && oldRoots.isNotEmpty()) { - cachedGradleProjectsRoots = oldRoots + roots - } else { - cachedGradleProjectsRoots = roots - } - } - - private fun computeGradleProjectRoots(project: Project): Set { - val gradleSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID) - if (gradleSettings.getLinkedProjectsSettings().isEmpty()) return setOf() - - val projectSettings = gradleSettings.getLinkedProjectsSettings().filterIsInstance().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().state) - } -} \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt index a05feef1cd1..fa3c91e449e 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/GradleScriptListener.kt @@ -5,16 +5,25 @@ package org.jetbrains.kotlin.idea.scripting.gradle -import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile 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 class GradleScriptListener(project: Project) : ScriptChangeListener(project) { init { - // start GradleScriptInputsWatcher to track changes in gradle-configuration related files - project.service().startWatching() + // listen changes using VFS events, including gradle-configuration related files + 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) diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/LastModifiedFiles.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/LastModifiedFiles.kt index c4b18e8a559..e0dea307a89 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/LastModifiedFiles.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/LastModifiedFiles.kt @@ -5,6 +5,19 @@ 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 * 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. */ -class LastModifiedFiles { - private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles() +class LastModifiedFiles( + private var last: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles(), private var previous: SimultaneouslyChangedFiles = SimultaneouslyChangedFiles() - +) { class SimultaneouslyChangedFiles( val ts: Long = Long.MIN_VALUE, val fileIds: MutableSet = mutableSetOf() @@ -40,4 +53,36 @@ class LastModifiedFiles { last.fileIds.size == 1 && last.fileIds.contains(fileId) -> previous.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) + } + } } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/README.md b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/README.md index bb1c9ea9df1..400d46e2db1 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/README.md +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/README.md @@ -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. -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 @@ -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. -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.loader` extension point and overriding `getInputsStamp` diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt index e082d9b75a3..81f5368bcdc 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/importing/KotlinDslScrtiptModelProcessor.kt @@ -5,7 +5,6 @@ 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.project.Project 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.jetbrains.kotlin.gradle.BrokenKotlinDslScriptsModel 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.roots.GradleBuildRootsManager import org.jetbrains.kotlin.psi.NotNullableUserDataProperty import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext import java.io.File @@ -115,14 +113,8 @@ fun saveScriptModels(project: Project, build: KotlinDslGradleBuildSync) { errorReporter.reportError(File(model.file), model) } - project.service().saveGradleProjectRootsAfterImport( - build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }.toSet() - ) - // todo: use real info about projects build.projectRoots.addAll(build.models.map { FileUtil.toSystemIndependentName(File(it.file).parent) }) GradleBuildRootsManager.getInstance(project).update(build) - - project.service().clearState() } \ No newline at end of file diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRoot.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRoot.kt index c82df60eb19..c48208e2e84 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRoot.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRoot.kt @@ -10,9 +10,11 @@ import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile 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.LastModifiedFiles import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource +import org.jetbrains.plugins.gradle.settings.GradleProjectSettings 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, * or we cannot known what is it */ + @Suppress("CanSealedSubClassBeObject") class Unlinked : GradleBuildRoot() /** @@ -37,31 +40,65 @@ sealed class GradleBuildRoot { @Volatile var importing = false + abstract val manager: GradleBuildRootsManager + abstract val pathPrefix: String - open val projectRoots: Collection get() = listOf() + abstract val projectRoots: Collection + + 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) */ - class Legacy(override val pathPrefix: String) : Linked() + class Legacy( + override val manager: GradleBuildRootsManager, + settings: GradleProjectSettings + ) : WithoutScriptModels(settings) /** * 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. * Each imported build have info about all of it's Kotlin Build Scripts. */ class Imported( - val project: Project, + override val manager: GradleBuildRootsManager, val dir: VirtualFile, val javaHome: File?, val data: GradleBuildRootData ) : Linked() { + val project: Project + get() = manager.project + override val pathPrefix: String = dir.path override val projectRoots: Collection diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootDataSerializer.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootDataSerializer.kt index f18c0ea60db..1cb9a4782e3 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootDataSerializer.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootDataSerializer.kt @@ -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.writeString 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 java.io.DataInput import java.io.DataInputStream @@ -23,7 +24,7 @@ internal object GradleBuildRootDataSerializer { fun read(buildRoot: VirtualFile): GradleBuildRootData? { return attribute.readAttribute(buildRoot)?.use { it.readNullable { - readKotlinDslScriptModels(it) + readKotlinDslScriptModels(it, buildRoot.path) } } } @@ -38,6 +39,7 @@ internal object GradleBuildRootDataSerializer { fun remove(buildRoot: VirtualFile) { write(buildRoot, null) + LastModifiedFiles.remove(buildRoot) } } @@ -57,14 +59,14 @@ internal fun writeKotlinDslScriptModels(output: DataOutput, data: GradleBuildRoo output.writeList(data.models) { strings.writeStringId(it.file) output.writeString(it.inputs.sections) - output.writeLong(it.inputs.inputsTS) + output.writeLong(it.inputs.lastModifiedTs) strings.writeStringIds(it.classPath) strings.writeStringIds(it.sourcePath) strings.writeStringIds(it.imports) } } -internal fun readKotlinDslScriptModels(input: DataInputStream): GradleBuildRootData { +internal fun readKotlinDslScriptModels(input: DataInputStream, buildRoot: String): GradleBuildRootData { val strings = StringsPool.reader(input) val projectRoots = strings.readStrings() @@ -73,7 +75,7 @@ internal fun readKotlinDslScriptModels(input: DataInputStream): GradleBuildRootD val models = input.readList { KotlinDslScriptModel( strings.readString(), - GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong()), + GradleKotlinScriptConfigurationInputs(input.readString(), input.readLong(), buildRoot), strings.readStrings(), strings.readStrings(), strings.readStrings(), diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootIndex.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootIndex.kt index 7c23614ed04..dbb5fc79b65 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootIndex.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootIndex.kt @@ -13,7 +13,7 @@ class GradleBuildRootIndex { private val byWorkingDir = HashMap() private val byProjectDir = HashMap() - val list: Collection + val list: Collection get() = byWorkingDir.values @Synchronized diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsManager.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsManager.kt index 385fc7f4e79..7fdda436846 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsManager.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsManager.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtil @@ -30,6 +31,7 @@ import org.jetbrains.plugins.gradle.settings.* import org.jetbrains.plugins.gradle.util.GradleConstants import java.io.File 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). @@ -103,40 +105,52 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider( get() = path 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( val root: GradleBuildRoot?, val script: GradleScriptInfo? = null ) - fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? { - if (!isGradleKotlinScript(gradleKtsFile)) return null + fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? = + findScriptBuildRoot(gradleKtsFile.path) - val filePath = gradleKtsFile.path - val scriptInfo = getScriptInfo(gradleKtsFile) + fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true): ScriptUnderRoot? { + if (!filePath.endsWith(".gradle.kts")) return null + + val scriptInfo = getScriptInfo(filePath) val imported = scriptInfo?.buildRoot if (imported != null) return ScriptUnderRoot(imported, scriptInfo) - if (gradleKtsFile.name == "build.gradle.kts" || - gradleKtsFile.name == "settings.gradle.kts" || - gradleKtsFile.name == "init.gradle.kts" + if (filePath.endsWith("/build.gradle.kts") || + filePath.endsWith("/settings.gradle.kts") || + filePath.endsWith("/init.gradle.kts") ) { // 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) } // other scripts: "included", "precompiled" scripts, scripts in unlinked projects, // or just random files with ".gradle.kts" ending - // try find nearest linked legacy gradle project settings first - val found = roots.findNearestRoot(filePath) - if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found) + if (searchNearestLegacy) { + val found = roots.findNearestRoot(filePath) + if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found) + } 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) { actualizeBuildRoot(workingDir)?.importing = inProgress updateNotifications(workingDir) @@ -177,6 +191,19 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider( 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 { getGradleProjectSettings(project).forEach { // 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 { val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version) return when { - supported -> GradleBuildRoot.New(settings.externalProjectPath) - else -> GradleBuildRoot.Legacy(settings.externalProjectPath) + supported -> GradleBuildRoot.New(this, settings) + else -> GradleBuildRoot.Legacy(this, settings) } } @@ -284,7 +311,7 @@ class GradleBuildRootsManager(val project: Project) : ScriptingSupport.Provider( .getExecutionSettings(project, externalProjectPath, GradleConstants.SYSTEM_ID) .javaHome?.let { File(it) } - return GradleBuildRoot.Imported(project, buildRoot, javaHome, data) + return GradleBuildRoot.Imported(this, buildRoot, javaHome, data) } private fun add(newRoot: GradleBuildRoot.Linked) { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/utils.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/utils.kt index dfe78ffc003..5d7304e4d9a 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/utils.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/utils.kt @@ -14,6 +14,8 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.* import com.intellij.psi.impl.source.tree.LeafPsiElement 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.psi.KtCallExpression import org.jetbrains.kotlin.psi.KtFile @@ -36,7 +38,7 @@ fun isInAffectedGradleProjectFiles(project: Project, filePath: String): Boolean return true } - return filePath.substringBeforeLast("/") in project.service().getGradleProjectsRoots() + return GradleBuildRootsManager.getInstance(project).isUnderProjectDir(filePath.substringBeforeLast("/")) } 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 } } diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/GradleBuildRootDataSerializerTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/GradleBuildRootDataSerializerTest.kt index 6a397fd54c7..1e97eed7db5 100644 --- a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/GradleBuildRootDataSerializerTest.kt +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/GradleBuildRootDataSerializerTest.kt @@ -25,7 +25,7 @@ class GradleBuildRootDataSerializerTest { listOf( KotlinDslScriptModel( "a", - GradleKotlinScriptConfigurationInputs("b", 1), + GradleKotlinScriptConfigurationInputs("b", 1, "a"), listOf("c", "a", "b"), listOf("b", "c", "a"), listOf("i", "c", "b"), @@ -33,7 +33,7 @@ class GradleBuildRootDataSerializerTest { ), KotlinDslScriptModel( "a", - GradleKotlinScriptConfigurationInputs("b", 1), + GradleKotlinScriptConfigurationInputs("b", 1, "a"), listOf("c", "a", "b"), listOf("b", "c", "a"), listOf("i", "c", "b"), @@ -45,7 +45,7 @@ class GradleBuildRootDataSerializerTest { val buffer = ByteArrayOutputStream() writeKotlinDslScriptModels(DataOutputStream(buffer), data) - val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray()))) + val restored = readKotlinDslScriptModels(DataInputStream(ByteArrayInputStream(buffer.toByteArray())), "a") assertEquals(data.toString(), restored.toString()) }