IDEA-238567 added 3 state auto-reload option

IDEA-238228 added storing of auto-reload option in workspace.xml

GitOrigin-RevId: 0a4ce55c040f553fc65ef1e957ed281e2f4a46c6
This commit is contained in:
Sergei Vorobyov
2020-05-05 20:54:00 +00:00
committed by intellij-monorepo-bot
parent 8ca9c829a1
commit 2604c5a552
18 changed files with 538 additions and 149 deletions
@@ -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
@@ -11,6 +11,7 @@ interface ExternalSystemProjectTracker {
/**
* Enables/disables auto reload external changes for all projects
*/
@Deprecated("Use ExternalSystemProjectTrackerSettings.autoReloadType instead")
var isAutoReloadExternalChanges: Boolean
/**
@@ -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)
}
}
}
@@ -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<State.Id, State.Project>()
private val projectDataMap = ConcurrentHashMap<ExternalSystemProjectId, ProjectData>()
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<String>(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<Id, Project> = emptyMap()
) {
data class State(var projectSettingsTrackerStates: Map<Id, Project> = emptyMap()) {
data class Id(var systemId: String? = null, var externalProjectPath: String? = null)
data class Project(
var isDirty: Boolean = false,
@@ -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")
}
}
}
@@ -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()
}
}
}
}
@@ -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)
}
}
@@ -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<ProjectTrackerSettings.State> {
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
}
}
}
@@ -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")
}
}
@@ -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) }
}
}
@@ -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)")
}
}
}
}
@@ -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<AutoImportProjectTracker.State, ProjectTrackerSettings.State>) {
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, ProjectTrackerSettings.State> =
AutoImportProjectTracker.State() to ProjectTrackerSettings.State(),
test: SimpleTestBench.(VirtualFile) -> Unit
): Pair<AutoImportProjectTracker.State, ProjectTrackerSettings.State> {
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'")
}
}
}
}
@@ -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<ExternalSystemProjectRefreshListener>()
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<String>()
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 }
}
@@ -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())
}
}
@@ -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<String> WHITE_LIST = ContainerUtil.immutableSet(
"ExternalSystem.ProjectRefreshAction",
"ExternalSystem.ToggleAutoReload"
);
private static final Set<String> WHITE_LIST = ContainerUtil.immutableSet("ExternalSystem.ProjectRefreshAction");
}
@@ -28,6 +28,8 @@
serviceImplementation="com.intellij.openapi.externalSystem.service.project.manage.ProjectDataManagerImpl"/>
<projectService serviceInterface="com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTracker"
serviceImplementation="com.intellij.openapi.externalSystem.autoimport.AutoImportProjectTracker"/>
<projectService serviceInterface="com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings"
serviceImplementation="com.intellij.openapi.externalSystem.autoimport.ProjectTrackerSettings"/>
<projectService serviceImplementation="com.intellij.openapi.externalSystem.autoimport.ProjectNotificationAware"/>
<editorFloatingToolbarProvider implementation="com.intellij.openapi.externalSystem.autoimport.ProjectRefreshFloatingProvider"/>
<projectService serviceInterface="com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManager"
@@ -19,7 +19,15 @@
class="com.intellij.openapi.externalSystem.action.ShowExternalSystemSettingsAction"
icon="AllIcons.General.Settings">
</action>
<action id="ExternalSystem.ToggleAutoReload" class="com.intellij.openapi.externalSystem.autoimport.ToggleAutoReloadAction"/>
<group id="ExternalSystem.ToggleAutoReload"
class="com.intellij.openapi.externalSystem.autoimport.ToggleAutoReloadActionGroup">
<action id="ExternalSystem.ToggleAutoReload.All"
class="com.intellij.openapi.externalSystem.autoimport.UseAllAutoReloadTypeAction"/>
<action id="ExternalSystem.ToggleAutoReload.Selective"
class="com.intellij.openapi.externalSystem.autoimport.UseSelectiveAutoReloadTypeAction"/>
<action id="ExternalSystem.ToggleAutoReload.None"
class="com.intellij.openapi.externalSystem.autoimport.UseNoneAutoReloadTypeAction"/>
</group>
<action id="ExternalSystem.OpenTasksActivationManager"
class="com.intellij.openapi.externalSystem.action.OpenTasksActivationManagerAction">
</action>
@@ -574,6 +574,7 @@
<reference id="Maven.RunBuild"/>
<reference id="Maven.ExecuteGoal"/>
<reference id="Maven.ToggleOffline"/>
<reference id="ExternalSystem.ToggleAutoReload"/>
<reference id="Maven.ToggleSkipTests"/>
<reference id="Maven.CollapseAll"/>
<separator/>