From 2604c5a552355558385dc137889ad85a385104e7 Mon Sep 17 00:00:00 2001 From: Sergei Vorobyov Date: Fri, 1 May 2020 15:38:53 +0300 Subject: [PATCH] IDEA-238567 added 3 state auto-reload option IDEA-238228 added storing of auto-reload option in workspace.xml GitOrigin-RevId: 0a4ce55c040f553fc65ef1e957ed281e2f4a46c6 --- .../messages/ExternalSystemBundle.properties | 10 +- .../ExternalSystemProjectTracker.kt | 1 + .../ExternalSystemProjectTrackerSettings.kt | 34 ++++ .../autoimport/AutoImportProjectTracker.kt | 49 ++++-- .../autoimport/AutoReloadTypeAction.kt | 42 +++++ .../autoimport/ProjectSettingsTracker.kt | 9 +- .../autoimport/ProjectStatus.kt | 23 ++- .../autoimport/ProjectTrackerSettings.kt | 35 ++++ .../autoimport/ToggleAutoReloadAction.kt | 36 ----- .../autoimport/ToggleAutoReloadActionGroup.kt | 31 ++++ .../autoimport/AutoImportTest.kt | 150 +++++++++++++++--- .../autoimport/AutoImportTestCase.kt | 130 ++++++++++----- .../autoimport/MockProjectAware.kt | 91 ++++++++--- .../autoimport/ProjectStatusTest.kt | 28 ++++ .../actionSystem/impl/ActionButton.java | 5 +- .../src/META-INF/ExternalSystemExtensions.xml | 2 + .../src/idea/ExternalSystemActions.xml | 10 +- .../src/main/resources/META-INF/plugin.xml | 1 + 18 files changed, 538 insertions(+), 149 deletions(-) create mode 100644 platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTrackerSettings.kt create mode 100644 platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoReloadTypeAction.kt create mode 100644 platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectTrackerSettings.kt delete mode 100644 platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadAction.kt create mode 100644 platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadActionGroup.kt diff --git a/platform/external-system-api/resources/messages/ExternalSystemBundle.properties b/platform/external-system-api/resources/messages/ExternalSystemBundle.properties index bbf92c495dfd..f4d779f58cc8 100644 --- a/platform/external-system-api/resources/messages/ExternalSystemBundle.properties +++ b/platform/external-system-api/resources/messages/ExternalSystemBundle.properties @@ -57,9 +57,13 @@ action.unignore.external.projects.description=Unignore selected {0} {1,choice,1# action.open.config.text=Open {0} Config action.open.config.description=Opens the {0} project file in the editor -action.refresh.project.auto.text=Auto-reload external changes -action.refresh.project.auto.description.enable=Enable to automatically reload the project after VCS updates and changes to build files made outside IDE -action.refresh.project.auto.description.disable=Disable to automatically reload the project after VCS updates and changes to build files made outside IDE +action.refresh.project.auto.text=Toggle Auto-reload Mode +action.refresh.project.auto.text.all=Enable auto-reload after any changes +action.refresh.project.auto.text.selective=Enable auto-reload only after external changes +action.refresh.project.auto.text.none=Disable auto-reload after all changes +action.refresh.project.auto.description.all=Auto-reload the project after any changes to build files +action.refresh.project.auto.description.selective=Auto-reload the project after VCS updates and changes to build files made outside IDE +action.refresh.project.auto.description.none=Don't auto-reload project after any changes to build files action.open.settings.text={0} Settings action.open.settings.description=Edit {0} settings for the current project diff --git a/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTracker.kt b/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTracker.kt index 1efd43c440e2..4f52171577d0 100644 --- a/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTracker.kt +++ b/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTracker.kt @@ -11,6 +11,7 @@ interface ExternalSystemProjectTracker { /** * Enables/disables auto reload external changes for all projects */ + @Deprecated("Use ExternalSystemProjectTrackerSettings.autoReloadType instead") var isAutoReloadExternalChanges: Boolean /** diff --git a/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTrackerSettings.kt b/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTrackerSettings.kt new file mode 100644 index 000000000000..4ac3f20260de --- /dev/null +++ b/platform/external-system-api/src/com/intellij/openapi/externalSystem/autoimport/ExternalSystemProjectTrackerSettings.kt @@ -0,0 +1,34 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.externalSystem.autoimport + +import com.intellij.openapi.components.ServiceManager +import com.intellij.openapi.project.Project + +interface ExternalSystemProjectTrackerSettings { + + var autoReloadType: AutoReloadType + + enum class AutoReloadType { + /** + * Auto-reload project after any changes to build files + */ + ALL, + + /** + * Auto-reload project after VCS updates and changes to build files made outside IDE + */ + SELECTIVE, + + /** + * Don't auto-reload project + */ + NONE + } + + companion object { + @JvmStatic + fun getInstance(project: Project): ExternalSystemProjectTrackerSettings { + return ServiceManager.getService(project, ExternalSystemProjectTrackerSettings::class.java) + } + } +} \ No newline at end of file diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoImportProjectTracker.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoImportProjectTracker.kt index ae951d748dbd..4e950983cebb 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoImportProjectTracker.kt +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoImportProjectTracker.kt @@ -10,11 +10,13 @@ import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.components.StoragePathMacros.CACHE_FILE import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType import com.intellij.openapi.extensions.ExtensionPointUtil import com.intellij.openapi.externalSystem.ExternalSystemManager import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus.SUCCESS import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.EXTERNAL +import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.INTERNAL import com.intellij.openapi.externalSystem.autoimport.update.PriorityEatUpdate import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil @@ -22,12 +24,14 @@ import com.intellij.openapi.observable.operations.AnonymousParallelOperationTrac import com.intellij.openapi.observable.operations.CompoundParallelOperationTrace import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.observable.properties.BooleanProperty +import com.intellij.openapi.observable.properties.PropertyView import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.registry.Registry import com.intellij.util.LocalTimeCounter.currentTime import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.ui.update.MergingUpdateQueue +import com.intellij.util.ui.update.Update import org.jetbrains.annotations.TestOnly import java.util.concurrent.ConcurrentHashMap import kotlin.streams.asStream @@ -41,17 +45,21 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro private val LOG = Logger.getInstance("#com.intellij.openapi.externalSystem.autoimport") private val AUTO_REPARSE_DELAY get() = DaemonCodeAnalyzerSettings.getInstance().autoReparseDelay + private val settings get() = ProjectTrackerSettings.getInstance(project) private val projectStates = ConcurrentHashMap() private val projectDataMap = ConcurrentHashMap() private val isDisabled = AtomicBooleanProperty(ApplicationManager.getApplication().isUnitTestMode) private val asyncChangesProcessingProperty = AtomicBooleanProperty(!ApplicationManager.getApplication().isHeadlessEnvironment) - private val autoReloadExternalChangesProperty = AtomicBooleanProperty(true) private val projectChangeOperation = AnonymousParallelOperationTrace(debugName = "Project change operation") private val projectRefreshOperation = CompoundParallelOperationTrace(debugName = "Project refresh operation") private val dispatcher = MergingUpdateQueue("AutoImportProjectTracker.dispatcher", AUTO_REPARSE_DELAY, false, null, project) private val backgroundExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("AutoImportProjectTracker.backgroundExecutor", 1) - override var isAutoReloadExternalChanges by autoReloadExternalChangesProperty + override var isAutoReloadExternalChanges by PropertyView( + settings.autoReloadTypeProperty, + { it == AutoReloadType.SELECTIVE }, + { if (it) AutoReloadType.SELECTIVE else AutoReloadType.NONE } + ) var isAsyncChangesProcessing by asyncChangesProcessingProperty @@ -75,7 +83,7 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro } override fun afterProjectRefresh(status: ExternalSystemRefreshStatus) { - if (status != SUCCESS) projectData.status.markDirty(currentTime()) + if (status != SUCCESS) projectData.status.markBroken(currentTime()) projectRefreshOperation.finishTask(id) } } @@ -97,15 +105,26 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro fun scheduleChangeProcessing() { LOG.debug("Schedule change processing") dispatcher.queue(PriorityEatUpdate(1) { - if (getModificationType() == EXTERNAL && isAutoReloadExternalChanges) { - refreshProject(doImportDeactivatedProjects = false) - } - else { - updateProjectNotification() - } + processChanges() }) } + private fun processChanges() { + when (settings.autoReloadType) { + AutoReloadType.ALL -> when (getModificationType()) { + INTERNAL -> refreshProject(doImportDeactivatedProjects = false) + EXTERNAL -> refreshProject(doImportDeactivatedProjects = false) + null -> updateProjectNotification() + } + AutoReloadType.SELECTIVE -> when (getModificationType()) { + INTERNAL -> updateProjectNotification() + EXTERNAL -> refreshProject(doImportDeactivatedProjects = false) + null -> updateProjectNotification() + } + AutoReloadType.NONE -> updateProjectNotification() + } + } + private fun refreshProject(doImportDeactivatedProjects: Boolean) { LOG.debug("Incremental project refresh") if (isDisabled.get() || Registry.`is`("external.system.auto.import.disabled")) return @@ -221,11 +240,10 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro val projectSettingsTrackerStates = projectDataMap.asSequence() .map { (id, data) -> id.getState() to data.getState() } .toMap() - return State(isAutoReloadExternalChanges, projectSettingsTrackerStates) + return State(projectSettingsTrackerStates) } override fun loadState(state: State) { - isAutoReloadExternalChanges = state.isAutoReloadExternalChanges projectStates.putAll(state.projectSettingsTrackerStates) projectDataMap.forEach { (id, data) -> loadState(id, data) } } @@ -270,13 +288,13 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro val notificationAware = ProjectNotificationAware.getInstance(project) projectRefreshOperation.beforeOperation { LOG.debug("Project refresh started") } projectRefreshOperation.beforeOperation { notificationAware.notificationExpire() } - projectRefreshOperation.afterOperation { scheduleProjectNotificationUpdate() } + projectRefreshOperation.afterOperation { scheduleChangeProcessing() } projectRefreshOperation.afterOperation { LOG.debug("Project refresh finished") } projectChangeOperation.beforeOperation { LOG.debug("Project change started") } projectChangeOperation.beforeOperation { notificationAware.notificationExpire() } projectChangeOperation.afterOperation { scheduleChangeProcessing() } projectChangeOperation.afterOperation { LOG.debug("Project change finished") } - autoReloadExternalChangesProperty.afterSet { scheduleProjectRefresh() } + settings.autoReloadTypeProperty.afterChange { scheduleChangeProcessing() } asyncChangesProcessingProperty.afterChange { dispatcher.isPassThrough = !it } } @@ -308,10 +326,7 @@ class AutoImportProjectTracker(private val project: Project) : ExternalSystemPro } } - data class State( - var isAutoReloadExternalChanges: Boolean = true, - var projectSettingsTrackerStates: Map = emptyMap() - ) { + data class State(var projectSettingsTrackerStates: Map = emptyMap()) { data class Id(var systemId: String? = null, var externalProjectPath: String? = null) data class Project( var isDirty: Boolean = false, diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoReloadTypeAction.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoReloadTypeAction.kt new file mode 100644 index 000000000000..daee5820fcfd --- /dev/null +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/AutoReloadTypeAction.kt @@ -0,0 +1,42 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.externalSystem.autoimport + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Toggleable +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType +import com.intellij.openapi.externalSystem.autoimport.ToggleAutoReloadActionGroup.Companion.projectTrackerSettings +import com.intellij.openapi.externalSystem.util.ExternalSystemBundle +import com.intellij.openapi.project.DumbAwareAction + +class UseAllAutoReloadTypeAction : AutoReloadTypeAction(AutoReloadType.ALL) + +class UseSelectiveAutoReloadTypeAction : AutoReloadTypeAction(AutoReloadType.SELECTIVE) + +class UseNoneAutoReloadTypeAction : AutoReloadTypeAction(AutoReloadType.NONE) + +abstract class AutoReloadTypeAction(val type: AutoReloadType) : DumbAwareAction(), Toggleable { + override fun update(e: AnActionEvent) { + val settings = e.projectTrackerSettings + e.presentation.isEnabledAndVisible = settings != null + val isSelected = settings?.autoReloadType == type + Toggleable.setSelected(e.presentation, isSelected) + } + + override fun actionPerformed(e: AnActionEvent) { + val settings = e.projectTrackerSettings ?: return + settings.autoReloadType = type + } + + init { + templatePresentation.text = when (type) { + AutoReloadType.ALL -> ExternalSystemBundle.message("action.refresh.project.auto.text.all") + AutoReloadType.SELECTIVE -> ExternalSystemBundle.message("action.refresh.project.auto.text.selective") + AutoReloadType.NONE -> ExternalSystemBundle.message("action.refresh.project.auto.text.none") + } + templatePresentation.description = when (type) { + AutoReloadType.ALL -> ExternalSystemBundle.message("action.refresh.project.auto.description.all") + AutoReloadType.SELECTIVE -> ExternalSystemBundle.message("action.refresh.project.auto.description.selective") + AutoReloadType.NONE -> ExternalSystemBundle.message("action.refresh.project.auto.description.none") + } + } +} diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectSettingsTracker.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectSettingsTracker.kt index 0007edbd64fa..3e8c59664293 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectSettingsTracker.kt +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectSettingsTracker.kt @@ -201,7 +201,7 @@ class ProjectSettingsTracker( status.markModified(currentTime(), modificationType) } else { - status.markDirty(currentTime()) + status.markDirty(currentTime(), modificationType) } } @@ -215,7 +215,12 @@ class ProjectSettingsTracker( if (!hasChanges(newSettingsFilesCRC)) { status.markReverted(currentTime()) } - projectTracker.scheduleChangeProcessing() + if (applyChangesOperation.isOperationCompleted()) { + projectTracker.scheduleChangeProcessing() + } + else { + projectTracker.scheduleProjectNotificationUpdate() + } } } } diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectStatus.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectStatus.kt index b18cfb6fe44d..3b6adb16f33e 100644 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectStatus.kt +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectStatus.kt @@ -17,14 +17,18 @@ class ProjectStatus(private val debugName: String? = null) { fun isDirty() = state.get() is Dirty fun isUpToDate() = when (state.get()) { - is Modified, is Dirty -> false + is Modified, is Dirty, is Broken -> false is Synchronized, is Reverted -> true } fun getModificationType() = when (val state = state.get()) { is Dirty -> state.type is Modified -> state.type - is Synchronized, is Reverted -> null + else -> null + } + + fun markBroken(stamp: Long): ProjectState { + return update(Break(stamp)) } fun markDirty(stamp: Long, type: ModificationType = INTERNAL): ProjectState { @@ -58,24 +62,35 @@ class ProjectStatus(private val debugName: String? = null) { is Invalidate -> event.ifFuture(currentState) { Dirty(it, event.type) } is Modify -> event.ifFuture(currentState) { Modified(it, event.type) } is Revert -> event.ifFuture(currentState, ::Reverted) + is Break -> event.ifFuture(currentState, ::Broken) } is Dirty -> when (event) { is Synchronize -> event.ifFuture(currentState, ::Synchronized) is Invalidate -> event.withFuture(currentState) { Dirty(it, currentState.type.merge(event.type)) } is Modify -> event.withFuture(currentState) { Dirty(it, currentState.type.merge(event.type)) } is Revert -> event.withFuture(currentState) { Dirty(it, currentState.type) } + is Break -> event.withFuture(currentState) { Dirty(it, currentState.type) } } is Modified -> when (event) { is Synchronize -> event.ifFuture(currentState, ::Synchronized) is Invalidate -> event.withFuture(currentState) { Dirty(it, currentState.type.merge(event.type)) } is Modify -> event.withFuture(currentState) { Modified(it, currentState.type.merge(event.type)) } is Revert -> event.ifFuture(currentState, ::Reverted) + is Break -> event.withFuture(currentState) { Dirty(it, currentState.type) } } is Reverted -> when (event) { is Synchronize -> event.ifFuture(currentState, ::Synchronized) is Invalidate -> event.withFuture(currentState) { Dirty(it, event.type) } is Modify -> event.ifFuture(currentState) { Modified(it, event.type) } is Revert -> event.withFuture(currentState, ::Reverted) + is Break -> event.ifFuture(currentState, ::Broken) + } + is Broken -> when (event) { + is Synchronize -> event.ifFuture(currentState, ::Synchronized) + is Invalidate -> event.withFuture(currentState) { Dirty(it, event.type) } + is Modify -> event.withFuture(currentState) { Dirty(it, event.type) } + is Revert -> event.withFuture(currentState, ::Broken) + is Break -> event.withFuture(currentState, ::Broken) } } } @@ -116,6 +131,8 @@ class ProjectStatus(private val debugName: String? = null) { class Modify(stamp: Long, val type: ModificationType) : ProjectEvent(stamp) class Revert(stamp: Long) : ProjectEvent(stamp) + + class Break(stamp: Long) : ProjectEvent(stamp) } sealed class ProjectState(val stamp: Long) { @@ -126,5 +143,7 @@ class ProjectStatus(private val debugName: String? = null) { class Modified(stamp: Long, val type: ModificationType) : ProjectState(stamp) class Reverted(stamp: Long) : ProjectState(stamp) + + class Broken(stamp: Long) : ProjectState(stamp) } } diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectTrackerSettings.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectTrackerSettings.kt new file mode 100644 index 000000000000..2ea5ba20bd40 --- /dev/null +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ProjectTrackerSettings.kt @@ -0,0 +1,35 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.externalSystem.autoimport + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.StoragePathMacros.WORKSPACE_FILE +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType +import com.intellij.openapi.observable.properties.AtomicLazyProperty +import com.intellij.openapi.project.Project +import org.jetbrains.annotations.ApiStatus + +@State(name = "AutoImportSettings", storages = [Storage(WORKSPACE_FILE)]) +class ProjectTrackerSettings : ExternalSystemProjectTrackerSettings, PersistentStateComponent { + + internal val autoReloadTypeProperty = AtomicLazyProperty { AutoReloadType.SELECTIVE } + + override var autoReloadType by autoReloadTypeProperty + + override fun getState() = State(autoReloadType) + + override fun loadState(state: State) { + autoReloadType = state.autoReloadType + } + + data class State(var autoReloadType: AutoReloadType = AutoReloadType.SELECTIVE) + + companion object { + @JvmStatic + @ApiStatus.Internal + fun getInstance(project: Project): ProjectTrackerSettings { + return ExternalSystemProjectTrackerSettings.getInstance(project) as ProjectTrackerSettings + } + } +} \ No newline at end of file diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadAction.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadAction.kt deleted file mode 100644 index b85da7e5a7de..000000000000 --- a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadAction.kt +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -package com.intellij.openapi.externalSystem.autoimport - -import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.ToggleAction -import com.intellij.openapi.externalSystem.util.ExternalSystemBundle -import com.intellij.openapi.project.DumbAware - -class ToggleAutoReloadAction : ToggleAction(), DumbAware { - override fun update(e: AnActionEvent) { - super.update(e) - e.presentation.description = when (isSelected(e)) { - true -> ExternalSystemBundle.message("action.refresh.project.auto.description.disable") - else -> ExternalSystemBundle.message("action.refresh.project.auto.description.enable") - } - } - - override fun isSelected(e: AnActionEvent): Boolean { - val project = e.project ?: return false - val projectTracker = ExternalSystemProjectTracker.getInstance(project) - return projectTracker.isAutoReloadExternalChanges - } - - override fun setSelected(e: AnActionEvent, state: Boolean) { - val project = e.project ?: return - val projectTracker = ExternalSystemProjectTracker.getInstance(project) - projectTracker.isAutoReloadExternalChanges = state - } - - init { - templatePresentation.icon = AllIcons.Actions.BuildAutoReloadChanges - templatePresentation.text = ExternalSystemBundle.message("action.refresh.project.auto.text") - templatePresentation.description = ExternalSystemBundle.message("action.refresh.project.auto.description.disable") - } -} diff --git a/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadActionGroup.kt b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadActionGroup.kt new file mode 100644 index 000000000000..0a5c529c0c00 --- /dev/null +++ b/platform/external-system-impl/src/com/intellij/openapi/externalSystem/autoimport/ToggleAutoReloadActionGroup.kt @@ -0,0 +1,31 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.externalSystem.autoimport + +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CheckedActionGroup +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.Toggleable +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType +import com.intellij.openapi.externalSystem.util.ExternalSystemBundle +import com.intellij.openapi.project.DumbAware + +class ToggleAutoReloadActionGroup : DefaultActionGroup(), Toggleable, CheckedActionGroup, DumbAware { + override fun update(e: AnActionEvent) { + val settings = e.projectTrackerSettings + e.presentation.isEnabledAndVisible = settings != null + val isDisabledAutoReload = settings?.autoReloadType == AutoReloadType.NONE + Toggleable.setSelected(e.presentation, !isDisabledAutoReload) + } + + init { + isPopup = true + templatePresentation.icon = AllIcons.Actions.BuildAutoReloadChanges + templatePresentation.text = ExternalSystemBundle.message("action.refresh.project.auto.text") + } + + companion object { + internal val AnActionEvent.projectTrackerSettings: ExternalSystemProjectTrackerSettings? + get() = project?.let { ExternalSystemProjectTrackerSettings.getInstance(it) } + } +} diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTest.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTest.kt index 7f23b6bc1d85..e77e7038afe7 100644 --- a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTest.kt +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTest.kt @@ -1,11 +1,14 @@ // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.openapi.externalSystem.autoimport -import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType.* +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus.FAILURE +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus.SUCCESS +import com.intellij.openapi.externalSystem.autoimport.MockProjectAware.RefreshCollisionPassType +import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.EXTERNAL +import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.INTERNAL import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.Parallel.Companion.parallel -import com.sun.media.jfxmedia.logging.Logger.setLevel -import org.apache.log4j.Level import org.junit.Test import java.io.File @@ -453,23 +456,23 @@ class AutoImportTest : AutoImportTestCase() { settingsFile.replaceContentInIoFile("println 'hello'") assertState(refresh = 2, notified = false, event = "external change") - setRefreshStatus(ExternalSystemRefreshStatus.FAILURE) + setRefreshStatus(FAILURE) settingsFile.replaceStringInIoFile("hello", "hi") assertState(refresh = 3, notified = true, event = "external change with failure refresh") refreshProject() assertState(refresh = 4, notified = true, event = "failure project refresh") - setRefreshStatus(ExternalSystemRefreshStatus.SUCCESS) + setRefreshStatus(SUCCESS) refreshProject() assertState(refresh = 5, notified = false, event = "project refresh") settingsFile.replaceString("hi", "hello") assertState(refresh = 5, notified = true, event = "modify") - setRefreshStatus(ExternalSystemRefreshStatus.FAILURE) + setRefreshStatus(FAILURE) refreshProject() assertState(refresh = 6, notified = true, event = "failure project refresh") settingsFile.replaceString("hello", "hi") assertState(refresh = 6, notified = true, event = "try to revert changes after failure refresh") - setRefreshStatus(ExternalSystemRefreshStatus.SUCCESS) + setRefreshStatus(SUCCESS) refreshProject() assertState(refresh = 7, notified = false, event = "project refresh") } @@ -505,38 +508,38 @@ class AutoImportTest : AutoImportTestCase() { fun `test disabling of auto-import`() { var state = simpleTest("settings.groovy") { settingsFile -> - assertState(refresh = 1, enabled = true, notified = false, event = "register project without cache") - disableAutoReloadExternalChanges() - assertState(refresh = 1, enabled = false, notified = false, event = "disable project auto-import") + assertState(refresh = 1, autoReloadType = SELECTIVE, notified = false, event = "register project without cache") + setAutoReloadType(NONE) + assertState(refresh = 1, autoReloadType = NONE, notified = false, event = "disable project auto-import") settingsFile.replaceContentInIoFile("println 'hello'") - assertState(refresh = 1, enabled = false, notified = true, event = "modification with disabled auto-import") + assertState(refresh = 1, autoReloadType = NONE, notified = true, event = "modification with disabled auto-import") } state = simpleTest("settings.groovy", state = state) { settingsFile -> // Open modified project with disabled auto-import for external changes - assertState(refresh = 0, enabled = false, notified = true, event = "register modified project") + assertState(refresh = 0, autoReloadType = NONE, notified = true, event = "register modified project") refreshProject() - assertState(refresh = 1, enabled = false, notified = false, event = "refresh project") + assertState(refresh = 1, autoReloadType = NONE, notified = false, event = "refresh project") // Checkout git branch, that has additional linked project withLinkedProject("module/settings.groovy") { moduleSettingsFile -> - assertState(refresh = 0, enabled = false, notified = true, event = "register project without cache with disabled auto-import") + assertState(refresh = 0, autoReloadType = NONE, notified = true, event = "register project without cache with disabled auto-import") moduleSettingsFile.replaceContentInIoFile("println 'hello'") - assertState(refresh = 0, enabled = false, notified = true, event = "modification with disabled auto-import") + assertState(refresh = 0, autoReloadType = NONE, notified = true, event = "modification with disabled auto-import") } - assertState(refresh = 1, enabled = false, notified = false, event = "remove modified linked project") + assertState(refresh = 1, autoReloadType = NONE, notified = false, event = "remove modified linked project") - enableAutoReloadExternalChanges() - assertState(refresh = 1, enabled = true, notified = false, event = "enable auto-import for project without modifications") - disableAutoReloadExternalChanges() - assertState(refresh = 1, enabled = false, notified = false, event = "disable project auto-import") + setAutoReloadType(SELECTIVE) + assertState(refresh = 1, autoReloadType = SELECTIVE, notified = false, event = "enable auto-import for project without modifications") + setAutoReloadType(NONE) + assertState(refresh = 1, autoReloadType = NONE, notified = false, event = "disable project auto-import") settingsFile.replaceStringInIoFile("hello", "hi") - assertState(refresh = 1, enabled = false, notified = true, event = "modification with disabled auto-import") - enableAutoReloadExternalChanges() - assertState(refresh = 2, enabled = true, notified = false, event = "enable auto-import for modified project") + assertState(refresh = 1, autoReloadType = NONE, notified = true, event = "modification with disabled auto-import") + setAutoReloadType(SELECTIVE) + assertState(refresh = 2, autoReloadType = SELECTIVE, notified = false, event = "enable auto-import for modified project") } simpleTest("settings.groovy", state = state) { - assertState(refresh = 0, enabled = true, notified = false, event = "register project with correct cache") + assertState(refresh = 0, autoReloadType = SELECTIVE, notified = false, event = "register project with correct cache") } } @@ -594,7 +597,6 @@ class AutoImportTest : AutoImportTestCase() { @Test fun `test merging of refreshes with different nature`() { - Logger.getInstance("#com.intellij.openapi.externalSystem.autoimport").setLevel(Level.DEBUG) simpleTest("settings.groovy") { settingsFile -> assertState(1, notified = false, event = "register project without cache") @@ -614,4 +616,102 @@ class AutoImportTest : AutoImportTestCase() { assertState(refresh = 2, notified = false, event = "modification") } } + + @Test + fun `test enabling-disabling internal-external changes importing`() { + simpleModificationTest { + modifySettingsFile(INTERNAL) + assertState(refresh = 0, notified = true, autoReloadType = SELECTIVE, event = "internal modification") + + refreshProject() + assertState(refresh = 1, notified = false, autoReloadType = SELECTIVE, event = "refresh project") + + modifySettingsFile(EXTERNAL) + assertState(refresh = 2, notified = false, autoReloadType = SELECTIVE, event = "external modification") + + setAutoReloadType(ALL) + + modifySettingsFile(INTERNAL) + assertState(refresh = 3, notified = false, autoReloadType = ALL, event = "internal modification with enabled auto-reload") + + modifySettingsFile(EXTERNAL) + assertState(refresh = 4, notified = false, autoReloadType = ALL, event = "external modification with enabled auto-reload") + + setAutoReloadType(NONE) + + modifySettingsFile(INTERNAL) + assertState(refresh = 4, notified = true, autoReloadType = NONE, event = "internal modification with disabled auto-reload") + + modifySettingsFile(EXTERNAL) + assertState(refresh = 4, notified = true, autoReloadType = NONE, event = "external modification with disabled auto-reload") + + setAutoReloadType(SELECTIVE) + assertState(refresh = 4, notified = true, autoReloadType = SELECTIVE, + event = "enable auto-reload external changes with internal and external modifications") + + setAutoReloadType(ALL) + assertState(refresh = 5, notified = false, autoReloadType = ALL, event = "enable auto-reload of any changes") + + setAutoReloadType(NONE) + + modifySettingsFile(INTERNAL) + assertState(refresh = 5, notified = true, autoReloadType = NONE, event = "internal modification with disabled auto-reload") + + modifySettingsFile(EXTERNAL) + assertState(refresh = 5, notified = true, autoReloadType = NONE, event = "external modification with disabled auto-reload") + + setAutoReloadType(ALL) + assertState(refresh = 6, notified = false, autoReloadType = ALL, event = "enable auto-reload of any changes") + } + } + + @Test + fun `test failure auto-reload with enabled auto-reload of any changes`() { + simpleModificationTest { + setAutoReloadType(ALL) + setRefreshStatus(FAILURE) + modifySettingsFile(INTERNAL) + assertState(refresh = 1, notified = true, autoReloadType = ALL, event = "failure modification with enabled auto-reload") + + modifySettingsFile(INTERNAL) + assertState(refresh = 2, notified = true, autoReloadType = ALL, event = "failure modification with enabled auto-reload") + + setRefreshStatus(SUCCESS) + refreshProject() + assertState(refresh = 3, notified = false, autoReloadType = ALL, event = "refresh project") + + setRefreshStatus(FAILURE) + onceDuringRefresh { + setRefreshStatus(SUCCESS) + modifySettingsFile(INTERNAL) + } + modifySettingsFile(INTERNAL) + assertState(refresh = 5, notified = false, autoReloadType = ALL, event = "success modification after failure") + } + } + + @Test + fun `test up-to-date promise after modifications with enabled auto-import`() { + simpleModificationTest { + for (collisionPassType in RefreshCollisionPassType.values()) { + resetAssertionCounters() + + setRefreshCollisionPassType(collisionPassType) + + setAutoReloadType(SELECTIVE) + onceDuringRefresh { + modifySettingsFile(EXTERNAL) + } + modifySettingsFile(EXTERNAL) + assertState(refresh = 2, notified = false, autoReloadType = SELECTIVE, event = "auto-reload inside reload ($collisionPassType)") + + setAutoReloadType(ALL) + onceDuringRefresh { + modifySettingsFile(INTERNAL) + } + modifySettingsFile(INTERNAL) + assertState(refresh = 4, notified = false, autoReloadType = ALL, event = "auto-reload inside reload ($collisionPassType)") + } + } + } } \ No newline at end of file diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTestCase.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTestCase.kt index 66fe3e64e450..70a621759d30 100644 --- a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTestCase.kt +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/AutoImportTestCase.kt @@ -9,6 +9,10 @@ import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.ComponentManager import com.intellij.openapi.editor.Document +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType.* +import com.intellij.openapi.externalSystem.autoimport.MockProjectAware.RefreshCollisionPassType +import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.test.ExternalSystemTestCase import com.intellij.openapi.fileEditor.FileDocumentManager @@ -35,6 +39,7 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { private lateinit var testDisposable: Disposable private val notificationAware get() = ProjectNotificationAware.getInstance(myProject) private val projectTracker get() = AutoImportProjectTracker.getInstance(myProject).also { it.enableAutoImportInTests() } + private val projectTrackerSettings get() = ProjectTrackerSettings.getInstance(myProject) private fun ensureExistsParentDirectory(relativePath: String): VirtualFile { return relativePath.split("/").dropLast(1) @@ -92,6 +97,12 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { localFileSystem.refreshIoFiles(paths.map { File(it) }, false, true, null) } + protected fun VirtualFile.appendLineInIoFile(line: String) = + appendStringInIoFile(line + "\n") + + protected fun VirtualFile.appendStringInIoFile(string: String) = + updateIoFile { appendText(string) } + protected fun VirtualFile.replaceContentInIoFile(content: String) = updateIoFile { writeText(content) } @@ -137,6 +148,9 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { VfsUtil.saveText(this, before + prefix + string + after) } + protected fun VirtualFile.appendLine(line: String) = + appendString(line + "\n") + protected fun VirtualFile.appendString(string: String) = runWriteAction { VfsUtil.saveText(this, VfsUtil.loadText(this) + string) } @@ -182,24 +196,22 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { projectTracker.scheduleProjectRefresh() } - private fun loadState(state: AutoImportProjectTracker.State) = projectTracker.loadState(state) - - protected fun enableAsyncExecution() { projectTracker.isAsyncChangesProcessing = true } - protected fun enableAutoReloadExternalChanges() { - projectTracker.isAutoReloadExternalChanges = true - } - - protected fun disableAutoReloadExternalChanges() { - projectTracker.isAutoReloadExternalChanges = false + protected fun setAutoReloadType(type: AutoReloadType) { + projectTrackerSettings.autoReloadType = type } protected fun initialize() = projectTracker.initializeComponent() - protected fun getState() = projectTracker.state + protected fun getState() = projectTracker.state to projectTrackerSettings.state + + private fun loadState(state: Pair) { + projectTracker.loadState(state.first) + projectTrackerSettings.loadState(state.second) + } protected fun assertProjectAware(projectAware: MockProjectAware, refresh: Int? = null, @@ -220,12 +232,13 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { assertEquals("$message on $event", expected, actual) } - protected fun assertProjectTracker(isAutoReload: Boolean, event: String) { - val message = when (isAutoReload) { - true -> "Auto reload must be enabled" - false -> "Auto reload must be disabled" + protected fun assertProjectTrackerSettings(autoReloadType: AutoReloadType, event: String) { + val message = when (autoReloadType) { + ALL -> "Auto reload must be enabled" + SELECTIVE -> "Auto reload must be enabled" + NONE -> "Auto reload must be disabled" } - assertEquals("$message on $event", isAutoReload, projectTracker.isAutoReloadExternalChanges) + assertEquals("$message on $event", autoReloadType, projectTrackerSettings.autoReloadType) } protected fun assertActivationStatus(vararg projects: ExternalSystemProjectId, event: String) { @@ -260,6 +273,7 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { override fun setUp() { super.setUp() testDisposable = Disposer.newDisposable() + myProject.replaceService(ExternalSystemProjectTrackerSettings::class.java, ProjectTrackerSettings(), testDisposable) myProject.replaceService(ExternalSystemProjectTracker::class.java, AutoImportProjectTracker(myProject), testDisposable) } @@ -268,28 +282,48 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { super.tearDown() } - protected fun simpleTest(fileRelativePath: String, - content: String? = null, - state: AutoImportProjectTracker.State = AutoImportProjectTracker.State(), - test: SimpleTestBench.(VirtualFile) -> Unit): AutoImportProjectTracker.State { - return myProject.replaceService(ExternalSystemProjectTracker::class.java, AutoImportProjectTracker(myProject)) { - val systemId = ProjectSystemId("External System") - val projectId = ExternalSystemProjectId(systemId, projectPath) - val projectAware = MockProjectAware(projectId) - loadState(state) - initialize() - val file = findOrCreateVirtualFile(fileRelativePath) - content?.let { file.replaceContent(it) } - projectAware.settingsFiles.add(file.path) - register(projectAware) - SimpleTestBench(projectAware).test(file) - val newState = getState() - remove(projectAware.projectId) - newState + protected fun simpleModificationTest(test: SimpleModificationTestBench.() -> Unit) { + simpleTest("settings.groovy", "") { + assertState( + refresh = 1, + notified = false, + subscribe = 2, + unsubscribe = 0, + autoReloadType = SELECTIVE, + event = "register project without cache" + ) + resetAssertionCounters() + SimpleModificationTestBench(projectAware, it).test() } } - protected inner class SimpleTestBench(private val projectAware: MockProjectAware) { + protected fun simpleTest( + fileRelativePath: String, + content: String? = null, + state: Pair = + AutoImportProjectTracker.State() to ProjectTrackerSettings.State(), + test: SimpleTestBench.(VirtualFile) -> Unit + ): Pair { + return myProject.replaceService(ExternalSystemProjectTrackerSettings::class.java, ProjectTrackerSettings()) { + myProject.replaceService(ExternalSystemProjectTracker::class.java, AutoImportProjectTracker(myProject)) { + val systemId = ProjectSystemId("External System") + val projectId = ExternalSystemProjectId(systemId, projectPath) + val projectAware = MockProjectAware(projectId) + loadState(state) + initialize() + val file = findOrCreateVirtualFile(fileRelativePath) + content?.let { file.replaceContent(it) } + projectAware.settingsFiles.add(file.path) + register(projectAware) + SimpleTestBench(projectAware).test(file) + val newState = getState() + remove(projectAware.projectId) + newState + } + } + } + + protected open inner class SimpleTestBench(val projectAware: MockProjectAware) { fun forceRefreshProject() = forceRefreshProject(projectAware.projectId) @@ -301,8 +335,16 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { fun registerSettingsFile(relativePath: String) = projectAware.settingsFiles.add(getPath(relativePath)) - fun setRefreshStatus(status: ExternalSystemRefreshStatus) { - projectAware.refreshStatus = status + fun onceDuringRefresh(action: () -> Unit) = projectAware.onceDuringRefresh(action) + + fun setRefreshStatus(status: ExternalSystemRefreshStatus) = projectAware.refreshStatus.set(status) + + fun setRefreshCollisionPassType(type: RefreshCollisionPassType) = projectAware.refreshCollisionPassType.set(type) + + fun resetAssertionCounters() { + projectAware.refreshCounter.set(0) + projectAware.subscribeCounter.set(0) + projectAware.unsubscribeCounter.set(0) } fun withLinkedProject(fileRelativePath: String, test: SimpleTestBench.(VirtualFile) -> Unit) { @@ -318,11 +360,11 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { fun assertState(refresh: Int? = null, subscribe: Int? = null, unsubscribe: Int? = null, - enabled: Boolean = true, + autoReloadType: AutoReloadType = SELECTIVE, notified: Boolean, event: String) { assertProjectAware(projectAware, refresh, subscribe, unsubscribe, event) - assertProjectTracker(enabled, event = event) + assertProjectTrackerSettings(autoReloadType, event = event) when (notified) { true -> assertNotificationAware(projectAware.projectId, event = event) else -> assertNotificationAware(event = event) @@ -344,4 +386,16 @@ abstract class AutoImportTestCase : ExternalSystemTestCase() { } } } + + protected inner class SimpleModificationTestBench( + projectAware: MockProjectAware, + private val settingsFile: VirtualFile + ) : SimpleTestBench(projectAware) { + fun modifySettingsFile(modificationType: ModificationType = ModificationType.INTERNAL) { + when (modificationType) { + ModificationType.INTERNAL -> settingsFile.appendLine("println 'hello'") + ModificationType.EXTERNAL -> settingsFile.appendLineInIoFile("println 'hello'") + } + } + } } \ No newline at end of file diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/MockProjectAware.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/MockProjectAware.kt index 59f70201e014..456f1b3f707d 100644 --- a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/MockProjectAware.kt +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/MockProjectAware.kt @@ -2,9 +2,17 @@ package com.intellij.openapi.externalSystem.autoimport import com.intellij.openapi.Disposable +import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus.SUCCESS +import com.intellij.openapi.externalSystem.autoimport.MockProjectAware.RefreshCollisionPassType.* +import com.intellij.openapi.observable.operations.AnonymousParallelOperationTrace +import com.intellij.openapi.observable.operations.AnonymousParallelOperationTrace.Companion.task import com.intellij.openapi.util.Disposer -import java.util.concurrent.CopyOnWriteArrayList +import com.intellij.util.ConcurrencyUtil.once +import com.intellij.util.EventDispatcher +import java.util.* import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicReference +import kotlin.collections.LinkedHashSet class MockProjectAware(override val projectId: ExternalSystemProjectId) : ExternalSystemProjectAware { @@ -12,38 +20,79 @@ class MockProjectAware(override val projectId: ExternalSystemProjectId) : Extern val unsubscribeCounter = AtomicInteger(0) val refreshCounter = AtomicInteger(0) - var refreshStatus = ExternalSystemRefreshStatus.SUCCESS + val refreshCollisionPassType = AtomicReference(DUPLICATE) + val refreshStatus = AtomicReference(SUCCESS) - private val listeners = CopyOnWriteArrayList() - private val inRefreshActions = CopyOnWriteArrayList<() -> Unit>() + private val eventDispatcher = EventDispatcher.create(Listener::class.java) + private val refresh = AnonymousParallelOperationTrace(debugName = "$projectId MockProjectAware.refreshProject") override val settingsFiles = LinkedHashSet() override fun subscribe(listener: ExternalSystemProjectRefreshListener, parentDisposable: Disposable) { - if (listeners.add(listener)) { - subscribeCounter.incrementAndGet() - } - Disposer.register(parentDisposable, Disposable { - if (listeners.remove(listener)) { - unsubscribeCounter.incrementAndGet() - } - }) + eventDispatcher.addListener(listener.asListener(), parentDisposable) + subscribeCounter.incrementAndGet() + Disposer.register(parentDisposable, Disposable { unsubscribeCounter.incrementAndGet() }) } override fun refreshProject() { - listeners.forEach { it.beforeProjectRefresh() } - refreshCounter.incrementAndGet() - inRefreshActions.forEach { it() } - listeners.forEach { it.afterProjectRefresh(refreshStatus) } + when (refreshCollisionPassType.get()!!) { + DUPLICATE -> { + doRefreshProject() + } + CANCEL -> { + val task = once { doRefreshProject() } + refresh.afterOperation { task.run() } + if (refresh.isOperationCompleted()) task.run() + } + IGNORE -> { + if (refresh.isOperationCompleted()) { + doRefreshProject() + } + } + } + } + + private fun doRefreshProject() { + val refreshStatus = refreshStatus.get() + eventDispatcher.multicaster.beforeProjectRefresh() + refresh.task { + refreshCounter.incrementAndGet() + eventDispatcher.multicaster.insideProjectRefresh() + } + eventDispatcher.multicaster.afterProjectRefresh(refreshStatus) } fun onceDuringRefresh(action: () -> Unit) { - val inRefreshAction = object : (() -> Unit) { - override fun invoke() { - action() - inRefreshActions.remove(this) + val disposable = Disposer.newDisposable() + duringRefresh(disposable) { + Disposer.dispose(disposable) + action() + } + } + + fun duringRefresh(parentDisposable: Disposable, action: () -> Unit) { + eventDispatcher.addListener(object : Listener { + override fun insideProjectRefresh() = action() + }, parentDisposable) + } + + private fun ExternalSystemProjectRefreshListener.asListener(): Listener { + return object : Listener, ExternalSystemProjectRefreshListener { + override fun beforeProjectRefresh() { + this@asListener.beforeProjectRefresh() + } + + override fun afterProjectRefresh(status: ExternalSystemRefreshStatus) { + this@asListener.afterProjectRefresh(status) } } - inRefreshActions.add(inRefreshAction) } + + interface Listener : ExternalSystemProjectRefreshListener, EventListener { + @JvmDefault + fun insideProjectRefresh() { + } + } + + enum class RefreshCollisionPassType { DUPLICATE, CANCEL, IGNORE } } \ No newline at end of file diff --git a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/ProjectStatusTest.kt b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/ProjectStatusTest.kt index cd3bd555758a..3a463c8da713 100644 --- a/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/ProjectStatusTest.kt +++ b/platform/external-system-impl/testSrc/com/intellij/openapi/externalSystem/autoimport/ProjectStatusTest.kt @@ -130,4 +130,32 @@ class ProjectStatusTest { status.markModified(120, INTERNAL) as Dirty assertEquals(INTERNAL, status.getModificationType()) } + + @Test + fun `test tracking status after failed import`() { + val status = ProjectStatus() + + status.markModified(10, INTERNAL) as Modified + status.markSynchronized(20) as Synchronized + status.markBroken(30) as Broken + + assertFalse(status.isUpToDate()) + assertFalse(status.isDirty()) + + status.markModified(40) as Dirty + status.markReverted(50) as Dirty + status.markSynchronized(60) as Synchronized + status.markBroken(70) as Broken + + assertFalse(status.isUpToDate()) + assertFalse(status.isDirty()) + + status.markReverted(80) as Broken + status.markSynchronized(90) as Synchronized + status.markModified(110) as Modified + status.markBroken(100) as Dirty + + assertFalse(status.isUpToDate()) + assertTrue(status.isDirty()) + } } \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionButton.java b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionButton.java index f1427926fcda..181d83765ef0 100644 --- a/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionButton.java +++ b/platform/platform-impl/src/com/intellij/openapi/actionSystem/impl/ActionButton.java @@ -614,8 +614,5 @@ public class ActionButton extends JComponent implements ActionButtonComponent, A } // Contains actions IDs which descriptions are permitted for displaying in the ActionButton tooltip - private static final Set WHITE_LIST = ContainerUtil.immutableSet( - "ExternalSystem.ProjectRefreshAction", - "ExternalSystem.ToggleAutoReload" - ); + private static final Set WHITE_LIST = ContainerUtil.immutableSet("ExternalSystem.ProjectRefreshAction"); } diff --git a/platform/platform-resources/src/META-INF/ExternalSystemExtensions.xml b/platform/platform-resources/src/META-INF/ExternalSystemExtensions.xml index 842a588007f1..a855cffc73a5 100644 --- a/platform/platform-resources/src/META-INF/ExternalSystemExtensions.xml +++ b/platform/platform-resources/src/META-INF/ExternalSystemExtensions.xml @@ -28,6 +28,8 @@ serviceImplementation="com.intellij.openapi.externalSystem.service.project.manage.ProjectDataManagerImpl"/> + - + + + + + diff --git a/plugins/maven/src/main/resources/META-INF/plugin.xml b/plugins/maven/src/main/resources/META-INF/plugin.xml index 05eb36048c6a..e36d17963972 100644 --- a/plugins/maven/src/main/resources/META-INF/plugin.xml +++ b/plugins/maven/src/main/resources/META-INF/plugin.xml @@ -574,6 +574,7 @@ +