diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt index 5ebf418af08..68a54217a12 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptNewDependenciesNotification.kt @@ -14,13 +14,10 @@ import com.intellij.openapi.util.Ref import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.EditorNotificationPanel import com.intellij.ui.HyperlinkLabel +import org.jetbrains.annotations.TestOnly import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.psi.UserDataProperty import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper -import java.awt.event.ComponentAdapter -import java.awt.event.ComponentEvent -import java.awt.event.ComponentListener -import javax.swing.JPanel fun VirtualFile.removeScriptDependenciesNotificationPanel(project: Project) { withSelectedEditor(project) { manager -> @@ -53,6 +50,20 @@ fun VirtualFile.addScriptDependenciesNotificationPanel( } } +@TestOnly +fun VirtualFile.hasSuggestedScriptConfiguration(project: Project): Boolean { + return FileEditorManager.getInstance(project).getSelectedEditor(this)?.notificationPanel != null +} + +@TestOnly +fun VirtualFile.applySuggestedScriptConfiguration(project: Project): Boolean { + val notificationPanel = FileEditorManager.getInstance(project).getSelectedEditor(this)?.notificationPanel + ?: return false + notificationPanel.onClick.invoke() + + return true +} + private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(FileEditorManager) -> Unit) { ApplicationManager.getApplication().invokeLater { if (project.isDisposed) return@invokeLater @@ -67,7 +78,7 @@ private fun VirtualFile.withSelectedEditor(project: Project, f: FileEditor.(File private var FileEditor.notificationPanel: NewScriptDependenciesNotificationPanel? by UserDataProperty(Key.create("script.dependencies.panel")) private class NewScriptDependenciesNotificationPanel( - onClick: () -> Unit, + val onClick: () -> Unit, val compilationConfigurationResult: ScriptCompilationConfigurationWrapper, project: Project ) : EditorNotificationPanel() { diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt index b7850db1ef9..96db03e831a 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/DefaultScriptConfigurationManager.kt @@ -8,6 +8,8 @@ package org.jetbrains.kotlin.idea.core.script.configuration import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.Extensions import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiManager @@ -15,6 +17,7 @@ import com.intellij.ui.EditorNotifications import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.kotlin.idea.core.script.* +import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions.LOADER import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationFileAttributeCache import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationMemoryCache import org.jetbrains.kotlin.idea.core.script.configuration.cache.ScriptConfigurationSnapshot @@ -27,6 +30,9 @@ import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigur import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptConfigurationLoadingContext import org.jetbrains.kotlin.idea.core.script.configuration.loader.ScriptOutsiderFileConfigurationLoader import org.jetbrains.kotlin.idea.core.script.configuration.utils.BackgroundExecutor +import org.jetbrains.kotlin.idea.core.script.configuration.utils.DefaultBackgroundExecutor +import org.jetbrains.kotlin.idea.core.script.configuration.utils.TestingBackgroundExecutor +import org.jetbrains.kotlin.idea.core.script.configuration.utils.isUnitTestModeWithoutScriptLoadingNotification import org.jetbrains.kotlin.idea.core.script.settings.KotlinScriptingSettings import org.jetbrains.kotlin.idea.core.util.EDT import org.jetbrains.kotlin.psi.KtFile @@ -96,21 +102,23 @@ import kotlin.script.experimental.api.ScriptDiagnostic */ internal class DefaultScriptConfigurationManager(project: Project) : AbstractScriptConfigurationManager(project) { - private val backgroundExecutor = BackgroundExecutor(project, rootsIndexer) + + internal val backgroundExecutor: BackgroundExecutor = + if (ApplicationManager.getApplication().isUnitTestMode) TestingBackgroundExecutor(rootsIndexer) + else DefaultBackgroundExecutor(project, rootsIndexer) private val fileAttributeCache = ScriptConfigurationFileAttributeCache(project) - private val loaders: List = listOf( - ScriptOutsiderFileConfigurationLoader(project), - fileAttributeCache, - GradleScriptConfigurationLoader(project), - DefaultScriptConfigurationLoader(project) - ) + private val loaders: List + get() = listOf( + ScriptOutsiderFileConfigurationLoader(project), + ScriptConfigurationFileAttributeCache(project) + ) + project[LOADER] + listOf( + DefaultScriptConfigurationLoader(project) + ) - private val listeners: List = listOf( - GradleScriptListener(), - DefaultScriptChangeListener() - ) + private val listeners: List + get() = project[DefaultScriptConfigurationManagerExtensions.LISTENER] + listOf(DefaultScriptChangeListener()) private val notifier = ScriptChangesNotifier(project, updater, listeners) @@ -123,6 +131,9 @@ internal class DefaultScriptConfigurationManager(project: Project) : } } + private operator fun Project.get(epName: ExtensionPointName): List = + Extensions.getArea(this).getExtensionPoint(epName).extensionList + /** * Will be called on [cache] miss to initiate loading of [file]'s script configuration. * @@ -248,7 +259,7 @@ internal class DefaultScriptConfigurationManager(project: Project) : val autoReload = skipNotification || oldConfiguration == null || KotlinScriptingSettings.getInstance(project).isAutoReloadEnabled - || ApplicationManager.getApplication().isUnitTestMode + || ApplicationManager.getApplication().isUnitTestModeWithoutScriptLoadingNotification if (autoReload) { if (oldConfiguration != null) { @@ -303,4 +314,15 @@ internal class DefaultScriptConfigurationManager(project: Project) : } } } -} \ No newline at end of file +} + +object DefaultScriptConfigurationManagerExtensions { + val LOADER: ExtensionPointName = + ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.loader") + + val LISTENER: ExtensionPointName = + ExtensionPointName.create("org.jetbrains.kotlin.scripting.idea.listener") +} + +val ScriptConfigurationManager.testingBackgroundExecutor + get() = (this as DefaultScriptConfigurationManager).backgroundExecutor as TestingBackgroundExecutor diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/FileContentsDependentConfigurationLoader.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/FileContentsDependentConfigurationLoader.kt new file mode 100644 index 00000000000..c8e8fa2e390 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/FileContentsDependentConfigurationLoader.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.core.script.configuration.loader + +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.psi.KtFile + +open class FileContentsDependentConfigurationLoader(project: Project) : DefaultScriptConfigurationLoader(project) { + override fun getInputsStamp(virtualFile: VirtualFile, file: KtFile): CachedConfigurationInputs { + return CachedConfigurationInputs.SourceContentsStamp.get(project, virtualFile, file) + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt index 8632b4808a3..7fb919b2d35 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/loader/ScriptConfigurationLoader.kt @@ -13,7 +13,7 @@ import org.jetbrains.kotlin.scripting.definitions.ScriptDefinition * * @see [org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManager] for more details. */ -internal interface ScriptConfigurationLoader { +interface ScriptConfigurationLoader { fun shouldRunInBackground(scriptDefinition: ScriptDefinition): Boolean = false /** diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt index ea1043949aa..e408a93a155 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/BackgroundExecutor.kt @@ -5,236 +5,8 @@ package org.jetbrains.kotlin.idea.core.script.configuration.utils -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task -import com.intellij.openapi.progress.util.BackgroundTaskUtil -import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.Alarm -import com.intellij.util.containers.HashSetQueue -import org.jetbrains.kotlin.idea.core.script.debug -import java.util.* -/** - * Sequentially loads script configuration in background. - * Loading tasks scheduled by calling [ensureScheduled]. - * - * Progress indicator will be shown after [PROGRESS_INDICATOR_DELAY] ms or if - * more then [PROGRESS_INDICATOR_MIN_QUEUE] tasks scheduled. - * - * States: - * silentWorker underProgressWorker - * - sleep - * - silent x - * - silent and under progress x x - * - under progress x - */ -internal class BackgroundExecutor( - val project: Project, - val rootsManager: ScriptClassRootsIndexer -) { - companion object { - const val PROGRESS_INDICATOR_DELAY = 1000 - const val PROGRESS_INDICATOR_MIN_QUEUE = 3 - } - - private val work = Any() - private val queue: Queue = HashSetQueue() - - /** - * Let's fix queue size when progress bar displayed. - * Progress for rest items will be counted from zero - */ - private var currentProgressSize: Int = 0 - private var currentProgressDone: Int = 0 - - private var silentWorker: SilentWorker? = null - private var underProgressWorker: UnderProgressWorker? = null - private val longRunningAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project) - private var longRunningAlaramRequested = false - - private var inTransaction: Boolean = false - private var currentFile: VirtualFile? = null - - class LoadTask(val key: VirtualFile, val actions: () -> Unit) { - override fun equals(other: Any?) = - this === other || (other is LoadTask && key == other.key) - - override fun hashCode() = key.hashCode() - } - - @Synchronized - fun ensureScheduled(key: VirtualFile, actions: () -> Unit) { - val task = LoadTask(key, actions) - - if (queue.add(task)) { - debug(task.key) { "added to update queue" } - - // If the queue is longer than PROGRESS_INDICATOR_MIN_QUEUE, show progress and cancel button - if (queue.size > PROGRESS_INDICATOR_MIN_QUEUE) { - requireUnderProgressWorker() - } else { - requireSilentWorker() - - if (!longRunningAlaramRequested) { - longRunningAlaramRequested = true - longRunningAlarm.addRequest( - { - longRunningAlaramRequested = false - requireUnderProgressWorker() - }, - PROGRESS_INDICATOR_DELAY - ) - } - } - } - } - - @Synchronized - private fun requireSilentWorker() { - if (silentWorker == null && underProgressWorker == null) { - silentWorker = SilentWorker().also { it.start() } - } - } - - @Synchronized - private fun requireUnderProgressWorker() { - if (queue.isEmpty() && silentWorker == null) return - - silentWorker?.stopGracefully() - if (underProgressWorker == null) { - underProgressWorker = UnderProgressWorker().also { it.start() } - restartProgressBar() - updateProgress() - } - } - - @Synchronized - private fun restartProgressBar() { - currentProgressSize = queue.size - currentProgressDone = 0 - } - - @Synchronized - fun updateProgress() { - underProgressWorker?.progressIndicator?.let { - it.text2 = currentFile?.path ?: "" - if (queue.size == 0) { - // last file - it.isIndeterminate = true - } else { - it.isIndeterminate = false - if (currentProgressDone > currentProgressSize) { - restartProgressBar() - } - it.fraction = currentProgressDone.toDouble() / currentProgressSize.toDouble() - } - } - } - - @Synchronized - private fun ensureInTransaction() { - if (inTransaction) return - inTransaction = true - rootsManager.startTransaction() - } - - @Synchronized - private fun endBatch() { - check(inTransaction) - rootsManager.commit() - inTransaction = false - } - - private abstract inner class Worker { - private var shouldStop = false - - open fun start() { - ensureInTransaction() - } - - fun stopGracefully() { - shouldStop = true - } - - protected open fun checkCancelled() = false - protected abstract fun close() - - protected fun run() { - try { - while (true) { - // prevent parallel work in both silent and under progress - synchronized(work) { - val next = synchronized(this@BackgroundExecutor) { - if (shouldStop) return - - if (checkCancelled()) { - queue.clear() - endBatch() - return - } else if (queue.isEmpty()) { - endBatch() - return - } - - queue.poll()?.also { - currentFile = it.key - currentProgressDone++ - updateProgress() - } - } - - next?.actions?.invoke() - - synchronized(work) { - currentFile = null - } - } - } - } finally { - close() - } - } - } - - private inner class UnderProgressWorker : Worker() { - var progressIndicator: ProgressIndicator? = null - - override fun start() { - super.start() - - object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) { - override fun run(indicator: ProgressIndicator) { - progressIndicator = indicator - updateProgress() - run() - } - }.queue() - } - - override fun checkCancelled(): Boolean = progressIndicator?.isCanceled == true - - override fun close() { - synchronized(this@BackgroundExecutor) { - underProgressWorker = null - progressIndicator = null - } - } - } - - private inner class SilentWorker : Worker() { - override fun start() { - super.start() - - BackgroundTaskUtil.executeOnPooledThread(project, Runnable { - run() - }) - } - - override fun close() { - synchronized(this@BackgroundExecutor) { - silentWorker = null - } - } - } +interface BackgroundExecutor { + fun ensureScheduled(key: VirtualFile, actions: () -> Unit) } \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultBackgroundExecutor.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultBackgroundExecutor.kt new file mode 100644 index 00000000000..926648f51bf --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/DefaultBackgroundExecutor.kt @@ -0,0 +1,241 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.core.script.configuration.utils + +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.progress.util.BackgroundTaskUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Alarm +import com.intellij.util.containers.HashSetQueue +import org.jetbrains.kotlin.idea.core.script.debug +import java.util.* + +/** + * Sequentially loads script configuration in background. + * Loading tasks scheduled by calling [ensureScheduled]. + * + * Progress indicator will be shown after [PROGRESS_INDICATOR_DELAY] ms or if + * more then [PROGRESS_INDICATOR_MIN_QUEUE] tasks scheduled. + * + * States: + * silentWorker underProgressWorker + * - sleep + * - silent x + * - silent and under progress x x + * - under progress x + */ +internal class DefaultBackgroundExecutor( + val project: Project, + val rootsManager: ScriptClassRootsIndexer +) : BackgroundExecutor { + companion object { + const val PROGRESS_INDICATOR_DELAY = 1000 + const val PROGRESS_INDICATOR_MIN_QUEUE = 3 + } + + private val work = Any() + private val queue: Queue = HashSetQueue() + + /** + * Let's fix queue size when progress bar displayed. + * Progress for rest items will be counted from zero + */ + private var currentProgressSize: Int = 0 + private var currentProgressDone: Int = 0 + + private var silentWorker: SilentWorker? = null + private var underProgressWorker: UnderProgressWorker? = null + private val longRunningAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, project) + private var longRunningAlaramRequested = false + + private var inTransaction: Boolean = false + private var currentFile: VirtualFile? = null + + class LoadTask(val key: VirtualFile, val actions: () -> Unit) { + override fun equals(other: Any?) = + this === other || (other is LoadTask && key == other.key) + + override fun hashCode() = key.hashCode() + } + + @Synchronized + override fun ensureScheduled(key: VirtualFile, actions: () -> Unit) { + val task = LoadTask(key, actions) + + if (queue.add(task)) { + debug(task.key) { "added to update queue" } + + // If the queue is longer than PROGRESS_INDICATOR_MIN_QUEUE, show progress and cancel button + if (queue.size > PROGRESS_INDICATOR_MIN_QUEUE) { + requireUnderProgressWorker() + } else { + requireSilentWorker() + + if (!longRunningAlaramRequested) { + longRunningAlaramRequested = true + longRunningAlarm.addRequest( + { + longRunningAlaramRequested = false + requireUnderProgressWorker() + }, + PROGRESS_INDICATOR_DELAY + ) + } + } + } + } + + @Synchronized + private fun requireSilentWorker() { + if (silentWorker == null && underProgressWorker == null) { + silentWorker = SilentWorker().also { it.start() } + } + } + + @Synchronized + private fun requireUnderProgressWorker() { + if (queue.isEmpty() && silentWorker == null) return + + silentWorker?.stopGracefully() + if (underProgressWorker == null) { + underProgressWorker = UnderProgressWorker().also { it.start() } + restartProgressBar() + updateProgress() + } + } + + @Synchronized + private fun restartProgressBar() { + currentProgressSize = queue.size + currentProgressDone = 0 + } + + @Synchronized + fun updateProgress() { + underProgressWorker?.progressIndicator?.let { + it.text2 = currentFile?.path ?: "" + if (queue.size == 0) { + // last file + it.isIndeterminate = true + } else { + it.isIndeterminate = false + if (currentProgressDone > currentProgressSize) { + restartProgressBar() + } + it.fraction = currentProgressDone.toDouble() / currentProgressSize.toDouble() + } + } + } + + @Synchronized + private fun ensureInTransaction() { + if (inTransaction) return + inTransaction = true + rootsManager.startTransaction() + } + + @Synchronized + private fun endBatch() { + check(inTransaction) + rootsManager.commit() + inTransaction = false + } + + private abstract inner class Worker { + private var shouldStop = false + + open fun start() { + ensureInTransaction() + } + + fun stopGracefully() { + shouldStop = true + } + + protected open fun checkCancelled() = false + protected abstract fun close() + + protected fun run() { + try { + while (true) { + // prevent parallel work in both silent and under progress + synchronized(work) { + val next = synchronized(this@DefaultBackgroundExecutor) { + if (shouldStop) return + + if (checkCancelled()) { + queue.clear() + endBatch() + return + } else if (queue.isEmpty()) { + endBatch() + return + } + + queue.poll()?.also { + currentFile = it.key + currentProgressDone++ + updateProgress() + } + } + + next?.actions?.invoke() + + synchronized(work) { + currentFile = null + } + } + } + } finally { + close() + } + } + } + + private inner class UnderProgressWorker : Worker() { + var progressIndicator: ProgressIndicator? = null + + override fun start() { + super.start() + + object : Task.Backgroundable(project, "Kotlin: Loading script dependencies...", true) { + override fun run(indicator: ProgressIndicator) { + progressIndicator = indicator + updateProgress() + run() + } + }.queue() + } + + override fun checkCancelled(): Boolean = progressIndicator?.isCanceled == true + + override fun close() { + synchronized(this@DefaultBackgroundExecutor) { + underProgressWorker = null + progressIndicator = null + } + } + } + + private inner class SilentWorker : Worker() { + override fun start() { + super.start() + + BackgroundTaskUtil.executeOnPooledThread(project, Runnable { + run() + }) + } + + override fun close() { + synchronized(this@DefaultBackgroundExecutor) { + silentWorker = null + } + } + } +} + diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/TestingBackgroundExecutor.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/TestingBackgroundExecutor.kt new file mode 100644 index 00000000000..8cb752a1e63 --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/TestingBackgroundExecutor.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.core.script.configuration.utils + +import com.intellij.openapi.application.impl.LaterInvocator +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.containers.HashSetQueue + +class TestingBackgroundExecutor internal constructor( + private val rootsManager: ScriptClassRootsIndexer +) : BackgroundExecutor { + val backgroundQueue = HashSetQueue() + + class BackgroundTask(val file: VirtualFile, val actions: () -> Unit) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BackgroundTask + + if (file != other.file) return false + + return true + } + + override fun hashCode(): Int { + return file.hashCode() + } + } + + @Synchronized + override fun ensureScheduled(key: VirtualFile, actions: () -> Unit) { + backgroundQueue.add(BackgroundTask(key, actions)) + } + + fun doAllBackgroundTaskWith(actions: () -> Unit): Boolean { + val copy = backgroundQueue.toList() + backgroundQueue.clear() + + actions() + + rootsManager.transaction { + copy.forEach { + it.actions() + } + } + + LaterInvocator.ensureFlushRequested() + LaterInvocator.dispatchPendingFlushes() + + return copy.isNotEmpty() + } +} \ No newline at end of file diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/testing.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/testing.kt new file mode 100644 index 00000000000..fbe5149960b --- /dev/null +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/configuration/utils/testing.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.core.script.configuration.utils + +import com.intellij.openapi.application.Application + +var testScriptConfigurationNotification: Boolean = false + +val Application.isUnitTestModeWithoutScriptLoadingNotification: Boolean + get() = isUnitTestMode && !testScriptConfigurationNotification \ No newline at end of file diff --git a/idea/resources/META-INF/extensions/ide.xml b/idea/resources/META-INF/extensions/ide.xml index f0ed001b820..b37d72b5132 100644 --- a/idea/resources/META-INF/extensions/ide.xml +++ b/idea/resources/META-INF/extensions/ide.xml @@ -59,6 +59,14 @@ + + + + diff --git a/idea/resources/META-INF/extensions/ide.xml.183 b/idea/resources/META-INF/extensions/ide.xml.183 index f1f34485a46..4da9395cd4d 100644 --- a/idea/resources/META-INF/extensions/ide.xml.183 +++ b/idea/resources/META-INF/extensions/ide.xml.183 @@ -67,6 +67,14 @@ + + + + diff --git a/idea/resources/META-INF/extensions/ide.xml.as34 b/idea/resources/META-INF/extensions/ide.xml.as34 index 80a80c5b7f4..1ce198d0a96 100644 --- a/idea/resources/META-INF/extensions/ide.xml.as34 +++ b/idea/resources/META-INF/extensions/ide.xml.as34 @@ -59,6 +59,14 @@ + + + + diff --git a/idea/testData/script/definition/loading/async/script.kts b/idea/testData/script/definition/loading/async/script.kts new file mode 100644 index 00000000000..c72f08c3900 --- /dev/null +++ b/idea/testData/script/definition/loading/async/script.kts @@ -0,0 +1 @@ +initial \ No newline at end of file diff --git a/idea/testData/script/definition/loading/async/template/template.kt b/idea/testData/script/definition/loading/async/template/template.kt new file mode 100644 index 00000000000..245d2c4a65f --- /dev/null +++ b/idea/testData/script/definition/loading/async/template/template.kt @@ -0,0 +1,42 @@ +package custom.scriptDefinition + +import kotlin.script.dependencies.* +import kotlin.script.experimental.dependencies.* +import kotlin.script.templates.* +import java.io.File +import kotlin.script.experimental.location.* + +class TestDependenciesResolver : AsyncDependenciesResolver { + suspend override fun resolveAsync(scriptContents: ScriptContents, environment: Environment): DependenciesResolver.ResolveResult { + val text = scriptContents.text as String + + val result = when { + text.startsWith("#BAD:") -> DependenciesResolver.ResolveResult.Failure( + reports = listOf(ScriptReport(text)) + ) + else -> DependenciesResolver.ResolveResult.Success( + dependencies = ScriptDependencies( + classpath = listOf(environment["template-classes"] as File), + imports = listOf("x_" + text.replace(Regex("#IGNORE_IN_CONFIGURATION"), "")) + ), + reports = listOf(ScriptReport(text)) + ) + } + + javaClass.classLoader + .loadClass("org.jetbrains.kotlin.idea.script.ScriptConfigurationLoadingTest") + .methods.single { it.name == "loadingScriptConfigurationCallback" } + .invoke(null) + + return result + } +} + +@ScriptExpectedLocations([ScriptExpectedLocation.Everywhere]) +@ScriptTemplateDefinition(TestDependenciesResolver::class, scriptFilePattern = "script.kts") +class Template : Base() + +open class Base { + val i = 3 + val str = "" +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationLoadingTest.kt b/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationLoadingTest.kt new file mode 100644 index 00000000000..df2cca1d6ca --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationLoadingTest.kt @@ -0,0 +1,145 @@ +/* + * 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.script + +import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.kotlin.idea.core.script.IdeScriptReportSink +import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager +import org.jetbrains.kotlin.idea.core.script.applySuggestedScriptConfiguration +import org.jetbrains.kotlin.idea.core.script.configuration.DefaultScriptConfigurationManagerExtensions +import org.jetbrains.kotlin.idea.core.script.configuration.loader.FileContentsDependentConfigurationLoader +import org.jetbrains.kotlin.idea.core.script.configuration.testingBackgroundExecutor +import org.jetbrains.kotlin.idea.core.script.configuration.utils.testScriptConfigurationNotification +import org.jetbrains.kotlin.idea.core.script.hasSuggestedScriptConfiguration +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.psi.KtFile + +abstract class AbstractScriptConfigurationLoadingTest : AbstractScriptConfigurationTest() { + val ktFile: KtFile get() = myFile as KtFile + val virtualFile get() = myFile.virtualFile + + lateinit var scriptConfigurationManager: ScriptConfigurationManager + + companion object { + private var occurredLoadings = 0 + private var currentLoadingScriptConfigurationCallback: (() -> Unit)? = null + @JvmStatic + @Suppress("unused") + fun loadingScriptConfigurationCallback() { + // this method is called from testData/script/definition/loading/async/template/template.kt + currentLoadingScriptConfigurationCallback?.invoke() + occurredLoadings++ + } + } + + override fun setUp() { + super.setUp() + testScriptConfigurationNotification = true + + addExtensionPointInTest( + DefaultScriptConfigurationManagerExtensions.LOADER, + project, + FileContentsDependentConfigurationLoader(project), + testRootDisposable + ) + + configureScriptFile("idea/testData/script/definition/loading/async/") + scriptConfigurationManager = ServiceManager.getService(project, ScriptConfigurationManager::class.java) + } + + override fun tearDown() { + super.tearDown() + testScriptConfigurationNotification = false + occurredLoadings = 0 + currentLoadingScriptConfigurationCallback = null + } + + override fun loadScriptConfigurationSynchronously(script: VirtualFile) { + // do nothings + } + + protected fun assertAndDoAllBackgroundTasks() { + assertDoAllBackgroundTaskAndDoWhileLoading { } + } + + protected fun assertDoAllBackgroundTaskAndDoWhileLoading(actions: () -> Unit) { + assertTrue(doAllBackgroundTasksWith(actions)) + } + + protected fun doAllBackgroundTasksWith(actions: () -> Unit): Boolean { + return scriptConfigurationManager.testingBackgroundExecutor.doAllBackgroundTaskWith { + currentLoadingScriptConfigurationCallback = { + actions() + currentLoadingScriptConfigurationCallback = null + } + } + } + + protected fun assertAppliedConfiguration(contents: String) { + val secondConfiguration = scriptConfigurationManager.getConfiguration(ktFile)!! + assertEquals( + contents, + secondConfiguration.defaultImports.single().let { + check(it.startsWith("x_")) + it.removePrefix("x_") + } + ) + } + + protected fun makeChanges(contents: String) { + changeContents(contents) + + scriptConfigurationManager.updater.ensureUpToDatedConfigurationSuggested(ktFile) + } + + protected fun changeContents(contents: String) { + runWriteAction { + val fileDocumentManager = FileDocumentManager.getInstance() + fileDocumentManager.reloadFiles(virtualFile) + val document = fileDocumentManager.getDocument(virtualFile)!! + document.setText(contents) + fileDocumentManager.saveDocument(document) + psiManager.reloadFromDisk(myFile) + myFile = psiManager.findFile(virtualFile) + } + } + + protected fun assertReports(expected: String) { + val actual = IdeScriptReportSink.getReports(virtualFile).single().message + assertEquals(expected, actual) + } + + protected fun assertSuggestedConfiguration() { + assertTrue(virtualFile.hasSuggestedScriptConfiguration(project)) + } + + protected fun assertAndApplySuggestedConfiguration() { + assertTrue(virtualFile.applySuggestedScriptConfiguration(project)) + } + + protected fun assertNoSuggestedConfiguration() { + assertFalse(virtualFile.applySuggestedScriptConfiguration(project)) + } + + protected fun assertNoLoading() { + assertEquals(0, occurredLoadings) + occurredLoadings = 0 + } + + protected fun assertSingleLoading() { + assertEquals(1, occurredLoadings) + occurredLoadings = 0 + } + + protected fun assertAndLoadInitialConfiguration() { + assertNull(scriptConfigurationManager.getConfiguration(ktFile)) + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAppliedConfiguration("initial") + } +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationTest.kt b/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationTest.kt index d5da21bb8cb..4b0075ebe2e 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/script/AbstractScriptConfigurationTest.kt @@ -265,6 +265,10 @@ abstract class AbstractScriptConfigurationTest : KotlinCompletionTestCase() { if (script == null) error("Test file with script couldn't be found in test project") configureByExistingFile(script) + loadScriptConfigurationSynchronously(script) + } + + protected open fun loadScriptConfigurationSynchronously(script: VirtualFile) { updateScriptDependenciesSynchronously(myFile, project) VfsUtil.markDirtyAndRefresh(false, true, true, project.baseDir) diff --git a/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationLoadingTest.kt b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationLoadingTest.kt new file mode 100644 index 00000000000..065af568b53 --- /dev/null +++ b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationLoadingTest.kt @@ -0,0 +1,205 @@ +/* + * 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.script + +class ScriptConfigurationLoadingTest : AbstractScriptConfigurationLoadingTest() { + fun testSimple() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + } + + fun testConcurrentLoadingWhileInQueue() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + makeChanges("B") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("B") + } + + fun testConcurrentLoadingWhileInQueueABA() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + makeChanges("B") + makeChanges("A") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + } + + fun testConcurrentLoadingWhileInQueueABA2() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + makeChanges("initial") + assertAndDoAllBackgroundTasks() + assertNoLoading() + assertNoSuggestedConfiguration() + } + + fun testConcurrentLoadingWhileAnotherLoadInProgress() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertDoAllBackgroundTaskAndDoWhileLoading { + makeChanges("B") + } + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("B") + } + + fun testConcurrentLoadingWhileAnotherLoadInProgressABA() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertDoAllBackgroundTaskAndDoWhileLoading { + makeChanges("B") + makeChanges("A") + } + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + + assertAndDoAllBackgroundTasks() + assertNoLoading() + assertNoSuggestedConfiguration() + assertAppliedConfiguration("A") + } + + fun mutedTestConcurrentLoadingWhileAnotherLoadInProgressABA2() { // todo: fix and unmute + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertDoAllBackgroundTaskAndDoWhileLoading { + makeChanges("initial") + } + assertSingleLoading() + assertSuggestedConfiguration() + + assertAndDoAllBackgroundTasks() + assertNoLoading() + assertNoSuggestedConfiguration() + } + + fun testConcurrentLoadingWhileNotApplied() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAppliedConfiguration("initial") + + // we have loaded and not applied configuration for A + // let's invalidate file again and check that loading will occur + + makeChanges("B") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("B") + } + + fun testConcurrentLoadingWhileNotAppliedABA() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAppliedConfiguration("initial") + + // we have loaded and not applied configuration for A + // let's invalidate file and change it back + // and check that loading will NOT occur + + makeChanges("B") + makeChanges("A") + + assertAndDoAllBackgroundTasks() + assertNoLoading() + assertAppliedConfiguration("initial") + + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + } + + fun testConcurrentLoadingWhileNotAppliedABA2() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertSuggestedConfiguration() + + makeChanges("initial") + assertAndDoAllBackgroundTasks() + assertNoLoading() + assertNoSuggestedConfiguration() + } + + fun testLoadingForUsagesSearch() { + assertAndLoadInitialConfiguration() + + changeContents("A") + assertNoLoading() + assertNoSuggestedConfiguration() + assertFalse(doAllBackgroundTasksWith {}) + scriptConfigurationManager.updater.ensureConfigurationUpToDate(listOf(ktFile)) + } + + fun testReportsOnAutoApply() { + assertAndLoadInitialConfiguration() + assertReports("initial") + } + + fun testReportsOnFailedConfiguration() { + assertAndLoadInitialConfiguration() + + makeChanges("#BAD:A") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertNoSuggestedConfiguration() + assertReports("#BAD:A") + } + + fun testReportsOnSameConfiguration() { + assertAndLoadInitialConfiguration() + + makeChanges("initial#IGNORE_IN_CONFIGURATION") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertNoSuggestedConfiguration() + assertAppliedConfiguration("initial") + assertReports("initial#IGNORE_IN_CONFIGURATION") + } + + fun testReportsAfterApply() { + assertAndLoadInitialConfiguration() + + makeChanges("A") + assertAndDoAllBackgroundTasks() + assertSingleLoading() + assertAndApplySuggestedConfiguration() + assertAppliedConfiguration("A") + assertReports("A") + } +} \ No newline at end of file