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 c04f3a302b1..d5b50da34ca 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 @@ -10,14 +10,14 @@ 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.idea.scripting.gradle.roots.GradleBuildRoot +import org.jetbrains.kotlin.idea.scripting.gradle.roots.* import org.jetbrains.kotlin.psi.KtFile /** * Up to date of gradle script depends on following factors: * 1. It is out of date when essential [sections] are changed. See [getGradleScriptInputsStamp]. * 2. When some related file is changed (other gradle script, gradle.properties file) - * See [GradleBuildRoot.Linked.areRelatedFilesChangedBefore]. + * See [GradleBuildRoot.areRelatedFilesChangedBefore]. * * [lastModifiedTs] is needed to check if some related file was changed since last update */ diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/MissingGradleScriptConfigurationNotificationProvider.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/MissingGradleScriptConfigurationNotificationProvider.kt index 30d693c4669..aad9dafd340 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/MissingGradleScriptConfigurationNotificationProvider.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/MissingGradleScriptConfigurationNotificationProvider.kt @@ -14,6 +14,7 @@ import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.EditorNotifications import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinIdeaGradleBundle +import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.* import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsManager class MissingGradleScriptConfigurationNotificationProvider(private val project: Project) : @@ -24,32 +25,63 @@ class MissingGradleScriptConfigurationNotificationProvider(private val project: if (!isGradleKotlinScript(file)) return null if (file.fileType != KotlinFileType.INSTANCE) return null - val scriptUnderRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file) ?: return null - return when { - scriptUnderRoot.isUnrelatedScript -> EditorNotificationPanel().apply { - text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported")) - - createActionLabel(KotlinIdeaGradleBundle.message("action.text.standalone")) { - GradleBuildRootsManager.getInstance(project).addStandaloneScript(file) + val rootsManager = GradleBuildRootsManager.getInstance(project) + val scriptUnderRoot = rootsManager.findScriptBuildRoot(file) ?: return null + return when (scriptUnderRoot.notificationKind) { + dontCare -> null + outsideAnyting -> EditorNotificationPanel().apply { + text("Code insight unavailable (related Gradle project not linked)") + // todo: Code insight unavailable (cannot find related Gradle project) + createActionLabel("Link related Gradle project") { + runPartialGradleImport(project) } - - val helpIcon = createActionLabel("") {} - helpIcon.setIcon(AllIcons.General.ContextHelp) - helpIcon.setUseIconAsLink(true) - helpIcon.toolTipText = KotlinIdeaGradleBundle.message( - "tool.tip.text.the.external.gradle.project.needs.to.be.imported.to.get.this.script.analyzed" - ) } - scriptUnderRoot.importRequired -> EditorNotificationPanel().apply { - text(getMissingConfigurationNotificationText()) + wasNotImportedAfterCreation -> EditorNotificationPanel().apply { + text("Code insight unavailable (Gradle project Sync required)") createActionLabel(getMissingConfigurationActionText()) { runPartialGradleImport(project) } } - else -> null + notEvaluatedInLastImport -> EditorNotificationPanel().apply { + text(KotlinIdeaGradleBundle.message("text.the.associated.gradle.project.isn.t.imported")) + + createActionLabel(KotlinIdeaGradleBundle.message("action.text.standalone")) { + rootsManager.updateStandaloneScripts { + removeStandaloneScript(file.path) + } + } + + contextHelp( + KotlinIdeaGradleBundle.message("tool.tip.text.the.external.gradle.project.needs.to.be.imported.to.get.this.script.analyzed") + ) + } + standalone -> EditorNotificationPanel().apply { + text("Standalone script") + createActionLabel("Disable script") { + rootsManager.updateStandaloneScripts { + addStandaloneScript(file.path) + } + } + contextHelp( + """ +
\ +

Configuration for this script will be loaded separately from Gradle project Sync. \ +
+

This can be expensive for large Gradle projects.

\ +
+ """ + ) + } } } + private fun EditorNotificationPanel.contextHelp(text: String) { + val helpIcon = createActionLabel("") {} + helpIcon.setIcon(AllIcons.General.ContextHelp) + helpIcon.setUseIconAsLink(true) + helpIcon.toolTipText = text + } + companion object { private val KEY = Key.create("GradleScriptOutOfSourceNotification") } 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 c55fb20790d..039077e5ae6 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 @@ -5,7 +5,6 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots -import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.annotations.TestOnly @@ -18,6 +17,10 @@ import org.jetbrains.kotlin.scripting.resolve.VirtualFileScriptSource import org.jetbrains.plugins.gradle.settings.GradleProjectSettings import java.io.File +@set:TestOnly +@get:TestOnly +internal var skipLastModifiedFilesLoading = false + /** * [GradleBuildRoot] is a linked gradle build (don't confuse with gradle project and included build). * Each [GradleBuildRoot] may have it's own Gradle version, Java home and other settings. @@ -27,132 +30,114 @@ import java.io.File * See [GradleBuildRootsManager] for more details. */ 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() + @Volatile + var importing = false - /** - * Linked project, that may be itself: [Legacy], [New] or [Imported]. - */ - abstract class Linked : GradleBuildRoot() { - @Volatile - var importing = false + abstract val pathPrefix: String - abstract val pathPrefix: String + abstract val projectRoots: Collection - abstract val projectRoots: Collection + val dir: VirtualFile? + get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix) - val dir: VirtualFile? - get() = LocalFileSystem.getInstance().findFileByPath(pathPrefix) + private lateinit var lastModifiedFiles: LastModifiedFiles - private lateinit var lastModifiedFiles: LastModifiedFiles + fun loadLastModifiedFiles() { + val loaded = if (!skipLastModifiedFilesLoading) { + val dir = dir + if (dir != null) LastModifiedFiles.read(dir) + else null + } else null - fun loadLastModifiedFiles() { - val loaded = if (!skipLastModifiedFilesLoading) { - val dir = dir - if (dir != null) LastModifiedFiles.read(dir) - else null - } else null + lastModifiedFiles = loaded ?: LastModifiedFiles() + } - lastModifiedFiles = loaded ?: LastModifiedFiles() + fun saveLastModifiedFiles() { + LastModifiedFiles.write(dir ?: return, lastModifiedFiles) + } + + fun areRelatedFilesChangedBefore(file: VirtualFile, lastModified: Long): Boolean = + lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified + + fun fileChanged(filePath: String, ts: Long) { + lastModifiedFiles.fileChanged(ts, filePath) + } +} + +sealed class WithoutScriptModels(settings: GradleProjectSettings) : GradleBuildRoot() { + final override val pathPrefix = settings.externalProjectPath!! + final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix) + + init { + loadLastModifiedFiles() + } +} + +/** + * Gradle build with old Gradle version (<6.0) + */ +class Legacy( + settings: GradleProjectSettings +) : WithoutScriptModels(settings) { + init { + loadLastModifiedFiles() + } +} + +/** + * Linked but not yet imported Gradle build. + */ +class New( + settings: GradleProjectSettings +) : WithoutScriptModels(settings) { + init { + loadLastModifiedFiles() + } +} + +/** + * Imported Gradle build. + * Each imported build have info about all of it's Kotlin Build Scripts. + */ +class Imported( + override val pathPrefix: String, + val javaHome: File?, + val data: GradleBuildRootData +) : GradleBuildRoot() { + override val projectRoots: Collection + get() = data.projectRoots + + init { + loadLastModifiedFiles() + } + + fun collectConfigurations(builder: ScriptClassRootsBuilder) { + if (javaHome != null) { + builder.sdks.addSdk(javaHome) } - fun saveLastModifiedFiles() { - LastModifiedFiles.write(dir ?: return, lastModifiedFiles) - } + val definitions = GradleScriptDefinitionsContributor.getDefinitions(builder.project) - fun areRelatedFilesChangedBefore(file: VirtualFile, lastModified: Long): Boolean = - lastModifiedFiles.lastModifiedTimeStampExcept(file.path) < lastModified + builder.addTemplateClassesRoots(data.templateClasspath) - fun fileChanged(filePath: String, ts: Long) { - lastModifiedFiles.fileChanged(ts, filePath) + data.models.forEach { script -> + val definition = selectScriptDefinition(script, definitions) + + builder.addCustom( + script.file, + script.classPath, + script.sourcePath, + GradleScriptInfo(this, definition, script) + ) } } - abstract class WithoutScriptModels(settings: GradleProjectSettings) : Linked() { - final override val pathPrefix = settings.externalProjectPath!! - final override val projectRoots = settings.modules.takeIf { it.isNotEmpty() } ?: listOf(pathPrefix) - - init { - loadLastModifiedFiles() - } - } - - /** - * Gradle build with old Gradle version (<6.0) - */ - class Legacy( - settings: GradleProjectSettings - ) : WithoutScriptModels(settings) { - init { - loadLastModifiedFiles() - } - } - - /** - * Linked but not yet imported Gradle build. - */ - class New( - settings: GradleProjectSettings - ) : WithoutScriptModels(settings) { - init { - loadLastModifiedFiles() - } - } - - /** - * Imported Gradle build. - * Each imported build have info about all of it's Kotlin Build Scripts. - */ - class Imported( - override val pathPrefix: String, - val javaHome: File?, - val data: GradleBuildRootData - ) : Linked() { - override val projectRoots: Collection - get() = data.projectRoots - - init { - loadLastModifiedFiles() - } - - fun collectConfigurations(builder: ScriptClassRootsBuilder) { - if (javaHome != null) { - builder.sdks.addSdk(javaHome) - } - - val definitions = GradleScriptDefinitionsContributor.getDefinitions(builder.project) - - builder.addTemplateClassesRoots(data.templateClasspath) - - data.models.forEach { script -> - val definition = selectScriptDefinition(script, definitions) - - builder.addCustom( - script.file, - script.classPath, - script.sourcePath, - GradleScriptInfo(this, definition, script) - ) - } - } - - private fun selectScriptDefinition( - script: KotlinDslScriptModel, - definitions: List - ): ScriptDefinition? { - val file = LocalFileSystem.getInstance().findFileByPath(script.file) ?: return null - val scriptSource = VirtualFileScriptSource(file) - return definitions.firstOrNull { it.isScript(scriptSource) } - } - } - - companion object { - @set:TestOnly - @get:TestOnly - internal var skipLastModifiedFilesLoading = false + private fun selectScriptDefinition( + script: KotlinDslScriptModel, + definitions: List + ): ScriptDefinition? { + val file = LocalFileSystem.getInstance().findFileByPath(script.file) ?: return null + val scriptSource = VirtualFileScriptSource(file) + return definitions.firstOrNull { it.isScript(scriptSource) } } } \ No newline at end of file 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 68e104f5c94..126124cc128 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 @@ -7,15 +7,15 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots import com.intellij.openapi.diagnostic.logger -class GradleBuildRootIndex { +class GradleBuildRootIndex : StandaloneScriptsUpdater { private val log = logger() - private val standaloneScriptRoots = mutableMapOf() + private val standaloneScriptRoots = mutableMapOf() - private val byWorkingDir = HashMap() - private val byProjectDir = HashMap() + private val byWorkingDir = HashMap() + private val byProjectDir = HashMap() - val list: Collection + val list: Collection get() = byWorkingDir.values @Synchronized @@ -34,8 +34,8 @@ class GradleBuildRootIndex { fun getBuildByRootDir(dir: String) = byWorkingDir[dir] @Synchronized - fun findNearestRoot(path: String): GradleBuildRoot.Linked? { - var max: Pair? = null + fun findNearestRoot(path: String): GradleBuildRoot? { + var max: Pair? = null byWorkingDir.entries.forEach { if (path.startsWith(it.key) && (max == null || it.key.length > max!!.first.length)) { max = it.key to it.value @@ -54,7 +54,7 @@ class GradleBuildRootIndex { fun getStandaloneScriptRoot(path: String) = standaloneScriptRoots[path] @Synchronized - fun add(value: GradleBuildRoot.Linked): GradleBuildRoot.Linked? { + fun add(value: GradleBuildRoot): GradleBuildRoot? { val prefix = value.pathPrefix val old = byWorkingDir.put(prefix, value) rebuildProjectRoots() @@ -69,11 +69,15 @@ class GradleBuildRootIndex { } @Synchronized - fun addStandaloneScript(path: String) { + override fun addStandaloneScript(path: String) { computeStandaloneScriptRoot(path) } - var standaloneScripts: Collection + @Synchronized + override fun removeStandaloneScript(path: String) = + standaloneScriptRoots.remove(path) + + override var standaloneScripts: Collection @Synchronized get() = standaloneScriptRoots.keys @Synchronized set(value) { standaloneScriptRoots.clear() diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocator.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocator.kt index 3545e1affbb..22c2eebfb99 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocator.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocator.kt @@ -38,7 +38,7 @@ abstract class GradleBuildRootsLocator { findAffectedFileRoot(filePath) != null || roots.isStandaloneScript(filePath) - fun findAffectedFileRoot(filePath: String): GradleBuildRoot.Linked? { + fun findAffectedFileRoot(filePath: String): GradleBuildRoot? { if (filePath.endsWith("/gradle.properties") || filePath.endsWith("/gradle.local") ) { @@ -50,25 +50,42 @@ abstract class GradleBuildRootsLocator { return roots.getBuildByRootDir(filePath.substring(0, buildDirIndex)) } - return findScriptBuildRoot(filePath, searchNearestLegacy = false)?.root as? GradleBuildRoot.Linked + return findScriptBuildRoot(filePath, searchNearestLegacy = false)?.root as? GradleBuildRoot + } + + enum class NotificationKind { + dontCare, // one of: imported, inside linked legacy gradle build + outsideAnyting, // suggest link related gradle build or just say that there is no one + wasNotImportedAfterCreation, // project not yet imported after this file was created + notEvaluatedInLastImport, // all other scripts, suggest to sync or mark as standalone + standalone } class ScriptUnderRoot( val root: GradleBuildRoot?, val script: GradleScriptInfo? = null, - val standalone: Boolean = false + val standalone: Boolean = false, + val nearest: GradleBuildRoot? = null ) { - val isUnrelatedScript: Boolean - get() = root is GradleBuildRoot.Unlinked - - val importRequired: Boolean - get() = root is GradleBuildRoot.Linked && - !isImported && - !root.importing && - !standalone + val notificationKind: NotificationKind + get() = when { + isImported -> NotificationKind.dontCare + standalone -> NotificationKind.standalone + nearest == null -> NotificationKind.outsideAnyting + nearest.importing -> NotificationKind.dontCare + else -> when (nearest) { + is Legacy -> NotificationKind.dontCare + is New -> NotificationKind.wasNotImportedAfterCreation + is Imported -> NotificationKind.notEvaluatedInLastImport // todo: wasNotImportedAfterCreation + } + } private val isImported: Boolean get() = script != null + + override fun toString(): String { + return "ScriptUnderRoot(root=$root, script=$script, standalone=$standalone, nearest=$nearest)" + } } fun findScriptBuildRoot(gradleKtsFile: VirtualFile): ScriptUnderRoot? = @@ -98,12 +115,14 @@ abstract class GradleBuildRootsLocator { return ScriptUnderRoot(standaloneScriptRoot, standalone = true) } - // todo(gradle6): remove, it is required only for projects with old gradle if (searchNearestLegacy) { - val found = roots.findNearestRoot(filePath) - if (found is GradleBuildRoot.Legacy) return ScriptUnderRoot(found) + val nearest = roots.findNearestRoot(filePath) + return when (nearest) { + is Legacy -> ScriptUnderRoot(nearest) + else -> ScriptUnderRoot(null, nearest = nearest) + } + } else { + return ScriptUnderRoot(null) } - - return ScriptUnderRoot(GradleBuildRoot.Unlinked()) } } \ No newline at end of file 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 0351aa7410d..0a08b31d0a6 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 @@ -21,11 +21,11 @@ import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.configuration.CompositeScriptConfigurationManager import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport import org.jetbrains.kotlin.idea.core.script.configuration.ScriptingSupport.Companion.EPN -import org.jetbrains.kotlin.idea.core.script.configuration.utils.getKtFile import org.jetbrains.kotlin.idea.core.script.ucache.ScriptClassRootsBuilder import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.idea.scripting.gradle.* import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslGradleBuildSync +import org.jetbrains.kotlin.idea.util.application.runReadAction import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstance import org.jetbrains.plugins.gradle.config.GradleSettingsListenerAdapter @@ -50,12 +50,10 @@ import java.util.concurrent.atomic.AtomicBoolean * It also used to show related notification and floating actions depending on root kind, state and script state itself. * * Roots may be: - * - [GradleBuildRoot.Unlinked] - The script not related to any Gradle build that is linked to IntelliJ Project, - * or we cannot known what is it - * - [GradleBuildRoot.Linked] - Linked project, that may be itself: - * - [GradleBuildRoot.Legacy] - Gradle build with old Gradle version (<6.0) - * - [GradleBuildRoot.New] - not yet imported - * - [GradleBuildRoot.Imported] - imported + * - [GradleBuildRoot] - Linked project, that may be itself: + * - [Legacy] - Gradle build with old Gradle version (<6.0) + * - [New] - not yet imported + * - [Imported] - imported */ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), ScriptingSupport { private val manager: CompositeScriptConfigurationManager @@ -69,14 +67,14 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), override fun isApplicable(file: VirtualFile): Boolean { val scriptUnderRoot = findScriptBuildRoot(file) ?: return false - if (scriptUnderRoot.root is GradleBuildRoot.Legacy) return false + if (scriptUnderRoot.root is Legacy) return false if (roots.isStandaloneScript(file.path)) return false return true } override fun isConfigurationLoadingInProgress(file: KtFile): Boolean { return when (val root = findScriptBuildRoot(file.originalFile.virtualFile)?.root) { - is GradleBuildRoot.Linked -> root.importing + is GradleBuildRoot -> root.importing else -> false } } @@ -98,7 +96,7 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), override fun collectConfigurations(builder: ScriptClassRootsBuilder) { roots.list.forEach { root -> - if (root is GradleBuildRoot.Imported) { + if (root is Imported) { root.collectConfigurations(builder) } } @@ -116,32 +114,32 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), fun markImportingInProgress(workingDir: String, inProgress: Boolean = true) { actualizeBuildRoot(workingDir)?.importing = inProgress - updateNotifications(workingDir) - scriptConfigurationsAreUpToDate(project) + updateNotifications { it.startsWith(workingDir) } + hideNotificationForProjectImport(project) } fun update(build: KotlinDslGradleBuildSync) { // fast path for linked gradle builds without .gradle.kts support if (build.models.isEmpty()) { val root = getBuildRootByWorkingDir(build.workingDir) ?: return - if (root is GradleBuildRoot.Imported && root.data.models.isEmpty()) return + if (root is Imported && root.data.models.isEmpty()) return } val root = actualizeBuildRoot(build.workingDir) ?: return root.importing = false - if (root is GradleBuildRoot.Legacy) return + if (root is Legacy) return val templateClasspath = GradleScriptDefinitionsContributor.getDefinitionsTemplateClasspath(project) val newData = GradleBuildRootData(build.projectRoots, templateClasspath, build.models) - val mergedData = if (build.failed && root is GradleBuildRoot.Imported) merge(root.data, newData) else newData + val mergedData = if (build.failed && root is Imported) merge(root.data, newData) else newData val newSupport = tryCreateImportedRoot(build.workingDir) { mergedData } ?: return GradleBuildRootDataSerializer.write(newSupport.dir ?: return, mergedData) add(newSupport) - scriptConfigurationsAreUpToDate(project) + hideNotificationForProjectImport(project) } private fun merge(old: GradleBuildRootData, new: GradleBuildRootData): GradleBuildRootData { @@ -168,12 +166,22 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), } } - fun addStandaloneScript(file: VirtualFile) { - roots.addStandaloneScript(file.path) - roots.rebuildProjectRoots() - updateNotifications(file.path) - val ktFile = project.getKtFile(file) ?: return - ScriptConfigurationManager.getInstance(project).getConfiguration(ktFile) + fun updateStandaloneScripts(update: StandaloneScriptsUpdater.() -> Unit) { + val changes = StandaloneScriptsUpdater.collectChanges(delegate = roots, update) + + updateNotifications { it in changes.new || it in changes.removed } + + runReadAction { + changes.new.forEach { + val virtualFile = LocalFileSystem.getInstance().findFileByPath(it) + if (virtualFile != null) { + val ktFile = PsiManager.getInstance(project).findFile(virtualFile) as? KtFile + if (ktFile != null) { + ScriptConfigurationManager.getInstance(project).getConfiguration(ktFile) + } + } + } + } } init { @@ -218,7 +226,7 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), * Actually this should be true, but we may miss some change events. * For that cases we are rechecking this on each Gradle Project sync (importing/reimporting) */ - private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot.Linked? { + private fun actualizeBuildRoot(workingDir: String): GradleBuildRoot? { val actualSettings = getGradleProjectSettings(workingDir) val buildRoot = getBuildRootByWorkingDir(workingDir) @@ -232,15 +240,15 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), } } - private fun GradleBuildRoot.Linked.checkActual(actualSettings: GradleProjectSettings?): Boolean { + private fun GradleBuildRoot.checkActual(actualSettings: GradleProjectSettings?): Boolean { if (actualSettings == null) return false - val knownAsSupported = this !is GradleBuildRoot.Legacy + val knownAsSupported = this !is Legacy val shouldBeSupported = kotlinDslScriptsModelImportSupported(actualSettings.resolveGradleVersion().version) return knownAsSupported == shouldBeSupported } - private fun reloadBuildRoot(rootPath: String): GradleBuildRoot.Linked? { + private fun reloadBuildRoot(rootPath: String): GradleBuildRoot? { val settings = getGradleProjectSettings(rootPath) if (settings == null) { remove(rootPath) @@ -267,45 +275,45 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), settings: GradleProjectSettings ) = data.copy(projectRoots = data.projectRoots.toSet() + settings.modules) - private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot.Linked { + private fun createOtherLinkedRoot(settings: GradleProjectSettings): GradleBuildRoot { val supported = kotlinDslScriptsModelImportSupported(settings.resolveGradleVersion().version) return when { - supported -> GradleBuildRoot.New(settings) - else -> GradleBuildRoot.Legacy(settings) + supported -> New(settings) + else -> Legacy(settings) } } private fun tryCreateImportedRoot( externalProjectPath: String, dataProvider: (buildRoot: VirtualFile) -> GradleBuildRootData? - ): GradleBuildRoot.Imported? { + ): Imported? { val buildRoot = VfsUtil.findFile(Paths.get(externalProjectPath), true) ?: return null val data = dataProvider(buildRoot) ?: return null val javaHome = ExternalSystemApiUtil .getExecutionSettings(project, externalProjectPath, GradleConstants.SYSTEM_ID) .javaHome?.let { File(it) } - return GradleBuildRoot.Imported(externalProjectPath, javaHome, data) + return Imported(externalProjectPath, javaHome, data) } - private fun add(newRoot: GradleBuildRoot.Linked) { + private fun add(newRoot: GradleBuildRoot) { val old = roots.add(newRoot) - if (old is GradleBuildRoot.Imported && newRoot !is GradleBuildRoot.Imported) removeData(old.pathPrefix) - if (old is GradleBuildRoot.Imported || newRoot is GradleBuildRoot.Imported) { + if (old is Imported && newRoot !is Imported) removeData(old.pathPrefix) + if (old is Imported || newRoot is Imported) { updater.invalidateAndCommit() } - updateNotifications(newRoot.pathPrefix) + updateNotifications { it.startsWith(newRoot.pathPrefix) } } private fun remove(rootPath: String) { val removed = roots.remove(rootPath) - if (removed is GradleBuildRoot.Imported) { + if (removed is Imported) { removeData(rootPath) updater.invalidateAndCommit() } - updateNotifications(rootPath) + updateNotifications { it.startsWith(rootPath) } } private fun removeData(rootPath: String) { @@ -317,11 +325,11 @@ class GradleBuildRootsManager(val project: Project) : GradleBuildRootsLocator(), } @Suppress("MemberVisibilityCanBePrivate") - private fun updateNotifications(dir: String) { + private fun updateNotifications(shouldUpdatePath: (String) -> Boolean) { if (!project.isOpen) return val openedScripts = FileEditorManager.getInstance(project).openFiles.filter { - it.path.startsWith(dir) && maybeAffectedGradleProjectFile(it.path) + shouldUpdatePath(it.path) && maybeAffectedGradleProjectFile(it.path) } if (openedScripts.isEmpty()) return diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleScriptInfo.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleScriptInfo.kt index 5e830fd424b..6704cd6ff9e 100644 --- a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleScriptInfo.kt +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleScriptInfo.kt @@ -19,7 +19,7 @@ import kotlin.script.experimental.jvm.jdkHome import kotlin.script.experimental.jvm.jvm class GradleScriptInfo( - val buildRoot: GradleBuildRoot.Imported, + val buildRoot: Imported, scriptDefinition: ScriptDefinition?, val model: KotlinDslScriptModel ) : ScriptClassRootsCache.LightScriptInfo(scriptDefinition) { diff --git a/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/StandaloneScriptsUpdater.kt b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/StandaloneScriptsUpdater.kt new file mode 100644 index 00000000000..7f926a32111 --- /dev/null +++ b/idea/idea-gradle/src/org/jetbrains/kotlin/idea/scripting/gradle/roots/StandaloneScriptsUpdater.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2020 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.roots + +interface StandaloneScriptsUpdater { + var standaloneScripts: Collection + + fun addStandaloneScript(path: String) + + fun removeStandaloneScript(path: String): GradleBuildRoot? + + class Changes { + val new = mutableSetOf() + val removed = mutableSetOf() + } + + companion object { + fun collectChanges( + delegate: StandaloneScriptsUpdater, + update: StandaloneScriptsUpdater.() -> Unit + ): Changes { + synchronized(delegate) { + val old = delegate.standaloneScripts.toSet() + delegate.update() + val changes = Changes() + val new = delegate.standaloneScripts + + new.forEach { + if (it !in old) changes.new.add(it) + } + + old.forEach { + if (it !in new) changes.removed.add(it) + } + + return changes + } + } + } +} \ No newline at end of file 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 50475b053c5..c3e5ed3317a 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 @@ -8,7 +8,6 @@ package org.jetbrains.kotlin.idea.scripting.gradle import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil import com.intellij.openapi.project.Project -import com.intellij.openapi.util.registry.Registry import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.* import com.intellij.psi.impl.source.tree.LeafPsiElement @@ -64,7 +63,7 @@ fun getGradleScriptInputsStamp( } } - val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot.Linked + val buildRoot = GradleBuildRootsManager.getInstance(project).findScriptBuildRoot(file)?.root as? GradleBuildRoot GradleKotlinScriptConfigurationInputs(result.toString(), givenTimeStamp, buildRoot?.pathPrefix) } else null } diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/AbstractGradleBuildRootsLocatorTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/AbstractGradleBuildRootsLocatorTest.kt index 03948ace16d..3967ef868f9 100644 --- a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/AbstractGradleBuildRootsLocatorTest.kt +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/AbstractGradleBuildRootsLocatorTest.kt @@ -7,10 +7,11 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots import org.jetbrains.kotlin.idea.scripting.gradle.GradleKotlinScriptConfigurationInputs import org.jetbrains.kotlin.idea.scripting.gradle.importing.KotlinDslScriptModel +import kotlin.test.assertEquals open class AbstractGradleBuildRootsLocatorTest { init { - GradleBuildRoot.skipLastModifiedFilesLoading = true + skipLastModifiedFilesLoading = true } private val scripts = mutableMapOf() @@ -21,7 +22,7 @@ open class AbstractGradleBuildRootsLocatorTest { fun accessRoots() = roots } - private fun add(root: GradleBuildRoot.Linked) { + private fun add(root: GradleBuildRoot) { locator.accessRoots().add(root) } @@ -31,7 +32,7 @@ open class AbstractGradleBuildRootsLocatorTest { relativeScripts: List = listOf("build.gradle.kts") ) { val pathPrefix = "$dir/" - val root = GradleBuildRoot.Imported( + val root = Imported( dir, null, GradleBuildRootData( @@ -62,4 +63,8 @@ open class AbstractGradleBuildRootsLocatorTest { fun findScriptBuildRoot(filePath: String, searchNearestLegacy: Boolean = true) = locator.findScriptBuildRoot(filePath, searchNearestLegacy) + + fun assertNotificationKind(filePath: String, notificationKind: GradleBuildRootsLocator.NotificationKind) { + assertEquals(notificationKind, findScriptBuildRoot(filePath)?.notificationKind) + } } \ No newline at end of file diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocatorTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocatorTest.kt index a4765fd0f66..d31b799377e 100644 --- a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocatorTest.kt +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/scripting/gradle/roots/GradleBuildRootsLocatorTest.kt @@ -5,7 +5,10 @@ package org.jetbrains.kotlin.idea.scripting.gradle.roots +import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.dontCare +import org.jetbrains.kotlin.idea.scripting.gradle.roots.GradleBuildRootsLocator.NotificationKind.wasNotImportedAfterCreation import org.junit.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -16,18 +19,14 @@ class GradleBuildRootsLocatorTest : AbstractGradleBuildRootsLocatorTest() { // the build.gradle.kts under the project root will be definitive import at next import // so, we should not treat it as unlinked newImportedGradleProject("imported", relativeScripts = listOf()) - val scriptUnderRoot = findScriptBuildRoot("imported/build.gradle.kts") - assertNotNull(scriptUnderRoot) - assertTrue(scriptUnderRoot.importRequired) - assertFalse(scriptUnderRoot.isUnrelatedScript) + + assertNotificationKind("imported/build.gradle.kts", wasNotImportedAfterCreation) } @Test fun testBuildGradleKtsNearProjectRoot() { newImportedGradleProject("imported", relativeScripts = listOf("build.gradle.kts")) - val scriptUnderRoot = findScriptBuildRoot("imported/build.gradle.kts") - assertNotNull(scriptUnderRoot) - assertFalse(scriptUnderRoot.importRequired) - assertFalse(scriptUnderRoot.isUnrelatedScript) + + assertNotificationKind("imported/build.gradle.kts", dontCare) } } \ No newline at end of file