diff --git a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleConfiguratorTest.kt b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleConfiguratorTest.kt index a7262942e65..cd873a54894 100644 --- a/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleConfiguratorTest.kt +++ b/idea/idea-gradle/tests/org/jetbrains/kotlin/idea/codeInsight/gradle/GradleConfiguratorTest.kt @@ -191,10 +191,10 @@ class GradleConfiguratorTest : GradleImportingTestCase() { runReadAction { val configurator = findGradleModuleConfigurator() - val (modules, ableToRunConfigurators) = getConfigurationPossibilities(myProject) + val (modules, ableToRunConfigurators) = getConfigurationPossibilitiesForConfigureNotification(myProject) assertTrue(ableToRunConfigurators.any { it is KotlinGradleModuleConfigurator }) assertTrue(ableToRunConfigurators.any { it is KotlinJsGradleModuleConfigurator }) - val moduleNames = modules.map { it.name } + val moduleNames = modules.map { it.baseModule.name } assertSameElements(moduleNames, "app") val moduleNamesFromConfigurator = getCanBeConfiguredModules(myProject, configurator).map { it.name } @@ -232,7 +232,7 @@ class GradleConfiguratorTest : GradleImportingTestCase() { importProject() runReadAction { - assertEmpty(getConfigurationPossibilities(myProject).first) + assertEmpty(getConfigurationPossibilitiesForConfigureNotification(myProject).first) } } @@ -263,7 +263,7 @@ class GradleConfiguratorTest : GradleImportingTestCase() { importProject() runReadAction { - assertEmpty(getConfigurationPossibilities(myProject).first) + assertEmpty(getConfigurationPossibilitiesForConfigureNotification(myProject).first) } } diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinInProjectUtils.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinInProjectUtils.kt index 72c0ced62bb..5f92c4d273a 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinInProjectUtils.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinInProjectUtils.kt @@ -91,6 +91,10 @@ fun isModuleConfigured(moduleSourceRootGroup: ModuleSourceRootGroup): Boolean { } } +/** + * Returns a list of modules which contain sources in Kotlin. + * Note that this method is expensive and should not be called more often than strictly necessary. + */ fun getModulesWithKotlinFiles(project: Project): Collection { if (!runReadAction { !project.isDisposed && FileTypeIndex.containsFileOfType (KotlinFileType.INSTANCE, GlobalSearchScope.projectScope(project)) @@ -106,6 +110,10 @@ fun getModulesWithKotlinFiles(project: Project): Collection { } } +/** + * Returns a list of modules which contain sources in Kotlin, grouped by base module. + * Note that this method is expensive and should not be called more often than strictly necessary. + */ fun getConfigurableModulesWithKotlinFiles(project: Project): List { val modules = getModulesWithKotlinFiles(project) if (modules.isEmpty()) return emptyList() @@ -121,17 +129,13 @@ fun showConfigureKotlinNotificationIfNeeded(module: Module) { } fun showConfigureKotlinNotificationIfNeeded(project: Project, excludeModules: List = emptyList()) { - val notificationString = DumbService.getInstance(project).runReadActionInSmartMode(Computable { - val modules = getConfigurableModulesWithKotlinFiles(project).exclude(excludeModules) - if (modules.none(::isNotConfiguredNotificationRequired)) - null - else - ConfigureKotlinNotification.getNotificationString(project, excludeModules) + val notificationState = DumbService.getInstance(project).runReadActionInSmartMode(Computable { + ConfigureKotlinNotification.getNotificationState(project, excludeModules) }) - if (notificationString != null) { + if (notificationState != null) { ApplicationManager.getApplication().invokeLater { - ConfigureKotlinNotificationManager.notify(project, ConfigureKotlinNotification(project, excludeModules, notificationString)) + ConfigureKotlinNotificationManager.notify(project, ConfigureKotlinNotification(project, excludeModules, notificationState)) } } } @@ -177,33 +181,43 @@ private fun KotlinProjectConfigurator.canConfigure(moduleSourceRootGroup: Module getStatus(moduleSourceRootGroup) == ConfigureKotlinStatus.CAN_BE_CONFIGURED && (allConfigurators().toList() - this).none { it.getStatus(moduleSourceRootGroup) == ConfigureKotlinStatus.CONFIGURED } +/** + * Returns a list of modules which contain sources in Kotlin and for which it's possible to run the given configurator. + * Note that this method is expensive and should not be called more often than strictly necessary. + */ fun getCanBeConfiguredModulesWithKotlinFiles(project: Project, configurator: KotlinProjectConfigurator): List { val modules = getConfigurableModulesWithKotlinFiles(project) return modules.filter { configurator.getStatus(it) == ConfigureKotlinStatus.CAN_BE_CONFIGURED }.map { it.baseModule } } -fun getConfigurationPossibilities( +fun getConfigurationPossibilitiesForConfigureNotification( project: Project, excludeModules: Collection = emptyList() -): Pair, Collection> { +): Pair, Collection> { val modulesWithKotlinFiles = getConfigurableModulesWithKotlinFiles(project).exclude(excludeModules) val configurators = allConfigurators() val runnableConfigurators = mutableSetOf() - val configurableModules = mutableListOf() + val configurableModules = mutableListOf() // We need to return all modules for which at least one configurator is applicable, as well as all configurators which // are applicable for at least one module. At the same time we want to call getStatus() only once for each module/configurator pair. for (moduleSourceRootGroup in modulesWithKotlinFiles) { var moduleCanBeConfigured = false + var moduleAlreadyConfigured = false for (configurator in configurators) { if (moduleCanBeConfigured && configurator in runnableConfigurators) continue - if (configurator.getStatus(moduleSourceRootGroup) == ConfigureKotlinStatus.CAN_BE_CONFIGURED) { - moduleCanBeConfigured = true - runnableConfigurators.add(configurator) + val status = configurator.getStatus(moduleSourceRootGroup) + when (status) { + ConfigureKotlinStatus.CAN_BE_CONFIGURED -> { + moduleCanBeConfigured = true + runnableConfigurators.add(configurator) + } + ConfigureKotlinStatus.CONFIGURED -> moduleAlreadyConfigured = true } } - if (moduleCanBeConfigured) configurableModules.add(moduleSourceRootGroup.baseModule) + if (moduleCanBeConfigured && !moduleAlreadyConfigured && !SuppressNotificationState.isKotlinNotConfiguredSuppressed(moduleSourceRootGroup)) + configurableModules.add(moduleSourceRootGroup) } return configurableModules to runnableConfigurators diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinNotificationManager.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinNotificationManager.kt index 61ca5e29c0c..13a6b4b1344 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinNotificationManager.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ConfigureKotlinNotificationManager.kt @@ -8,8 +8,11 @@ package org.jetbrains.kotlin.idea.configuration import com.intellij.notification.Notification import com.intellij.notification.NotificationsManager import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.IndexNotReadyException import com.intellij.openapi.project.Project import org.jetbrains.kotlin.idea.configuration.ui.notifications.ConfigureKotlinNotification import java.util.concurrent.atomic.AtomicBoolean @@ -17,9 +20,9 @@ import kotlin.reflect.KClass object ConfigureKotlinNotificationManager: KotlinSingleNotificationManager { fun notify(project: Project, excludeModules: List = emptyList()) { - val notificationString = ConfigureKotlinNotification.getNotificationString(project, excludeModules) - if (notificationString != null) { - notify(project, ConfigureKotlinNotification(project, excludeModules, notificationString)) + val notificationState = ConfigureKotlinNotification.getNotificationState(project, excludeModules) + if (notificationState != null) { + notify(project, ConfigureKotlinNotification(project, excludeModules, notificationState)) } } @@ -47,8 +50,7 @@ interface KotlinSingleNotificationManager { for (oldNotification in notifications) { if (oldNotification == notification) { isNotificationExists = true - } - else { + } else { oldNotification?.expire() } } @@ -59,20 +61,37 @@ interface KotlinSingleNotificationManager { private val checkInProgress = AtomicBoolean(false) fun checkHideNonConfiguredNotifications(project: Project) { - if (checkInProgress.get() || ConfigureKotlinNotificationManager.getVisibleNotifications(project).isEmpty()) return + if (checkInProgress.get()) return + val notification = ConfigureKotlinNotificationManager.getVisibleNotifications(project).firstOrNull() ?: return ApplicationManager.getApplication().executeOnPooledThread { if (!checkInProgress.compareAndSet(false, true)) return@executeOnPooledThread DumbService.getInstance(project).waitForSmartMode() - if (getConfigurableModulesWithKotlinFiles(project).none(::isNotConfiguredNotificationRequired)) { + val moduleSourceRootMap = ModuleSourceRootMap(project) + + val hideNotification = try { + val moduleSourceRootGroups = notification.notificationState.notConfiguredModules + .mapNotNull { ModuleManager.getInstance(project).findModuleByName(it) } + .map { moduleSourceRootMap.getWholeModuleGroup(it) } + moduleSourceRootGroups.none(::isNotConfiguredNotificationRequired) + } catch (e: IndexNotReadyException) { + checkInProgress.set(false) + ApplicationManager.getApplication().invokeLater { + checkHideNonConfiguredNotifications(project) + } + return@executeOnPooledThread + } + + if (hideNotification) { ApplicationManager.getApplication().invokeLater { ConfigureKotlinNotificationManager.expireOldNotifications(project) checkInProgress.set(false) } - } - else { + } else { checkInProgress.set(false) } } } + +private val LOG = Logger.getInstance(ConfigureKotlinNotificationManager::class.java) diff --git a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ui/notifications/ConfigureKotlinNotification.kt b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ui/notifications/ConfigureKotlinNotification.kt index 2646c6e083c..2932de5b291 100644 --- a/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ui/notifications/ConfigureKotlinNotification.kt +++ b/idea/idea-jvm/src/org/jetbrains/kotlin/idea/configuration/ui/notifications/ConfigureKotlinNotification.kt @@ -11,19 +11,25 @@ import com.intellij.notification.NotificationType import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import org.jetbrains.kotlin.idea.configuration.KotlinProjectConfigurator -import org.jetbrains.kotlin.idea.configuration.getConfigurationPossibilities +import org.jetbrains.kotlin.idea.configuration.getConfigurationPossibilitiesForConfigureNotification import org.jetbrains.kotlin.idea.configuration.getConfiguratorByName import org.jetbrains.kotlin.idea.configuration.ui.KotlinConfigurationCheckerComponent import javax.swing.event.HyperlinkEvent +data class ConfigureKotlinNotificationState( + val notificationString: String, + val notConfiguredModules: Collection +) + class ConfigureKotlinNotification( project: Project, excludeModules: List, - notificationString: String + val notificationState: ConfigureKotlinNotificationState ) : Notification( KotlinConfigurationCheckerComponent.CONFIGURE_NOTIFICATION_GROUP_ID, "Configure Kotlin", - notificationString, - NotificationType.WARNING, NotificationListener { notification, event -> + notificationState.notificationString, + NotificationType.WARNING, + NotificationListener { notification, event -> if (event.eventType == HyperlinkEvent.EventType.ACTIVATED) { val configurator = getConfiguratorByName(event.description) ?: throw AssertionError("Missed action: " + event.description) notification.expire() @@ -46,18 +52,21 @@ class ConfigureKotlinNotification( } companion object { - fun getNotificationString(project: Project, excludeModules: Collection): String? { - val (configurableModules, ableToRunConfigurators) = getConfigurationPossibilities(project, excludeModules) - if (ableToRunConfigurators.isEmpty()) return null + fun getNotificationState(project: Project, excludeModules: Collection): ConfigureKotlinNotificationState? { + val (configurableModules, ableToRunConfigurators) = getConfigurationPossibilitiesForConfigureNotification(project, excludeModules) + if (ableToRunConfigurators.isEmpty() || configurableModules.isEmpty()) return null val isOnlyOneModule = configurableModules.size == 1 - val modulesString = if (isOnlyOneModule) "'${configurableModules.first().name}' module" else "modules" + val modulesString = if (isOnlyOneModule) "'${configurableModules.first().baseModule.name}' module" else "modules" val links = ableToRunConfigurators.joinToString(separator = "
") { configurator -> getLink(configurator, isOnlyOneModule) } - return "Configure $modulesString in '${project.name}' project
$links" + return ConfigureKotlinNotificationState( + "Configure $modulesString in '${project.name}' project
$links", + configurableModules.map { it.baseModule.name } + ) } private fun getLink(configurator: KotlinProjectConfigurator, isOnlyOneModule: Boolean): String {