f618377725f620989fac3551888a279523f44d57
[idea/community.git] / platform / external-system-impl / src / com / intellij / openapi / externalSystem / autoimport / AutoImportProjectTracker.kt
1 // 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.
2 package com.intellij.openapi.externalSystem.autoimport
3
4 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings
5 import com.intellij.ide.file.BatchFileChangeListener
6 import com.intellij.openapi.Disposable
7 import com.intellij.openapi.application.ApplicationManager
8 import com.intellij.openapi.components.PersistentStateComponent
9 import com.intellij.openapi.components.State
10 import com.intellij.openapi.components.Storage
11 import com.intellij.openapi.components.StoragePathMacros.CACHE_FILE
12 import com.intellij.openapi.diagnostic.Logger
13 import com.intellij.openapi.externalSystem.autoimport.ExternalSystemProjectTrackerSettings.AutoReloadType
14 import com.intellij.openapi.extensions.ExtensionPointUtil
15 import com.intellij.openapi.externalSystem.ExternalSystemManager
16 import com.intellij.openapi.externalSystem.autoimport.ExternalSystemRefreshStatus.SUCCESS
17 import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType
18 import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.EXTERNAL
19 import com.intellij.openapi.externalSystem.autoimport.ProjectStatus.ModificationType.INTERNAL
20 import com.intellij.openapi.externalSystem.autoimport.update.PriorityEatUpdate
21 import com.intellij.openapi.externalSystem.model.ProjectSystemId
22 import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
23 import com.intellij.openapi.observable.operations.AnonymousParallelOperationTrace
24 import com.intellij.openapi.observable.operations.CompoundParallelOperationTrace
25 import com.intellij.openapi.observable.properties.AtomicBooleanProperty
26 import com.intellij.openapi.observable.properties.BooleanProperty
27 import com.intellij.openapi.observable.properties.PropertyView
28 import com.intellij.openapi.project.Project
29 import com.intellij.openapi.util.Disposer
30 import com.intellij.openapi.util.registry.Registry
31 import com.intellij.util.LocalTimeCounter.currentTime
32 import com.intellij.util.concurrency.AppExecutorUtil
33 import com.intellij.util.ui.update.MergingUpdateQueue
34 import com.intellij.util.ui.update.Update
35 import org.jetbrains.annotations.TestOnly
36 import java.util.concurrent.ConcurrentHashMap
37 import kotlin.streams.asStream
38
39 @State(name = "ExternalSystemProjectTracker", storages = [Storage(CACHE_FILE)])
40 class AutoImportProjectTracker(private val project: Project) : ExternalSystemProjectTracker, PersistentStateComponent<AutoImportProjectTracker.State> {
41
42   @Suppress("unused")
43   private val debugThrowable = Throwable("Initialized with project=(${project.isDisposed}, ${Disposer.isDisposed(project)}, $project)")
44
45   private val LOG = Logger.getInstance("#com.intellij.openapi.externalSystem.autoimport")
46   private val AUTO_REPARSE_DELAY = DaemonCodeAnalyzerSettings.getInstance().autoReparseDelay
47   private val AUTO_RELOAD_DELAY = 2000
48
49   private val settings get() = ProjectTrackerSettings.getInstance(project)
50   private val projectStates = ConcurrentHashMap<State.Id, State.Project>()
51   private val projectDataMap = ConcurrentHashMap<ExternalSystemProjectId, ProjectData>()
52   private val isDisabled = AtomicBooleanProperty(ApplicationManager.getApplication().isUnitTestMode)
53   private val asyncChangesProcessingProperty = AtomicBooleanProperty(!ApplicationManager.getApplication().isHeadlessEnvironment)
54   private val projectChangeOperation = AnonymousParallelOperationTrace(debugName = "Project change operation")
55   private val projectRefreshOperation = CompoundParallelOperationTrace<String>(debugName = "Project refresh operation")
56   private val dispatcher = MergingUpdateQueue("AutoImportProjectTracker.dispatcher", AUTO_REPARSE_DELAY, false, null, project)
57   private val delayDispatcher = MergingUpdateQueue("AutoImportProjectTracker.delayDispatcher", AUTO_RELOAD_DELAY, false, null, project)
58   private val backgroundExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("AutoImportProjectTracker.backgroundExecutor", 1)
59
60   override var isAutoReloadExternalChanges by PropertyView(
61     settings.autoReloadTypeProperty,
62     { it == AutoReloadType.SELECTIVE },
63     { if (it) AutoReloadType.SELECTIVE else AutoReloadType.NONE }
64   )
65
66   var isAsyncChangesProcessing by asyncChangesProcessingProperty
67
68   private fun createProjectChangesListener() =
69     object : ProjectBatchFileChangeListener(project) {
70       override fun batchChangeStarted(activityName: String?) =
71         projectChangeOperation.startTask()
72
73       override fun batchChangeCompleted() =
74         projectChangeOperation.finishTask()
75     }
76
77   private fun createProjectRefreshListener(projectData: ProjectData) =
78     object : ExternalSystemProjectRefreshListener {
79       val id = "ProjectTracker: ${projectData.projectAware.projectId.readableName}"
80
81       override fun beforeProjectRefresh() {
82         projectRefreshOperation.startTask(id)
83         projectData.status.markSynchronized(currentTime())
84         projectData.isActivated = true
85       }
86
87       override fun afterProjectRefresh(status: ExternalSystemRefreshStatus) {
88         if (status != SUCCESS) projectData.status.markBroken(currentTime())
89         projectRefreshOperation.finishTask(id)
90       }
91     }
92
93   override fun scheduleProjectRefresh() {
94     LOG.debug("Schedule project refresh")
95     dispatcher.queue(PriorityEatUpdate(0) {
96       refreshProject(smart = false)
97     })
98   }
99
100   override fun scheduleProjectNotificationUpdate() {
101     LOG.debug("Schedule notification status update")
102     dispatcher.queue(PriorityEatUpdate(2) {
103       updateProjectNotification()
104     })
105   }
106
107   fun scheduleChangeProcessing() {
108     LOG.debug("Schedule change processing")
109     dispatcher.queue(PriorityEatUpdate(1) {
110       processChanges()
111     })
112   }
113
114   private fun delay(action: () -> Unit) {
115     delayDispatcher.queue(Update.create(this, action))
116   }
117
118   private fun processChanges() {
119     when (settings.autoReloadType) {
120       AutoReloadType.ALL -> when (getModificationType()) {
121         INTERNAL -> delay { refreshProject(smart = true) }
122         EXTERNAL -> delay { refreshProject(smart = true) }
123         null -> updateProjectNotification()
124       }
125       AutoReloadType.SELECTIVE -> when (getModificationType()) {
126         INTERNAL -> updateProjectNotification()
127         EXTERNAL -> delay { refreshProject(smart = true) }
128         null -> updateProjectNotification()
129       }
130       AutoReloadType.NONE -> updateProjectNotification()
131     }
132   }
133
134   private fun refreshProject(smart: Boolean) {
135     LOG.debug("Incremental project refresh")
136     if (isDisabled.get() || Registry.`is`("external.system.auto.import.disabled")) return
137     if (!projectChangeOperation.isOperationCompleted()) return
138     if (smart && !projectRefreshOperation.isOperationCompleted()) return
139     var isSkippedProjectRefresh = true
140     for (projectData in projectDataMap.values) {
141       val projectId = projectData.projectAware.projectId.readableName
142       val isAllowAutoReload = !smart || projectData.isActivated
143       if (isAllowAutoReload && !projectData.isUpToDate()) {
144         isSkippedProjectRefresh = false
145         LOG.debug("$projectId: Project refresh")
146         projectData.projectAware.refreshProject()
147       }
148       else {
149         LOG.debug("$projectId: Skip project refresh")
150       }
151     }
152     if (isSkippedProjectRefresh) {
153       updateProjectNotification()
154     }
155   }
156
157   private fun updateProjectNotification() {
158     LOG.debug("Notification status update")
159     if (isDisabled.get() || Registry.`is`("external.system.auto.import.disabled")) return
160     val notificationAware = ProjectNotificationAware.getInstance(project)
161     for ((projectId, data) in projectDataMap) {
162       when (data.isUpToDate()) {
163         true -> notificationAware.notificationExpire(projectId)
164         else -> notificationAware.notificationNotify(data.projectAware)
165       }
166     }
167   }
168
169   private fun getModificationType(): ModificationType? {
170     return projectDataMap.values
171       .asSequence()
172       .mapNotNull { it.getModificationType() }
173       .asStream()
174       .reduce(ModificationType::merge)
175       .orElse(null)
176   }
177
178   override fun register(projectAware: ExternalSystemProjectAware) {
179     val projectId = projectAware.projectId
180     val activationProperty = AtomicBooleanProperty(false)
181     val projectStatus = ProjectStatus(debugName = projectId.readableName)
182     val parentDisposable = Disposer.newDisposable(projectId.readableName)
183     val settingsTracker = ProjectSettingsTracker(project, this, backgroundExecutor, projectAware, parentDisposable)
184     val projectData = ProjectData(projectStatus, activationProperty, projectAware, settingsTracker, parentDisposable)
185     val notificationAware = ProjectNotificationAware.getInstance(project)
186
187     registerProjectAwareDisposable(projectAware)
188     projectDataMap[projectId] = projectData
189
190     val id = "ProjectSettingsTracker: ${projectData.projectAware.projectId.readableName}"
191     settingsTracker.beforeApplyChanges { projectRefreshOperation.startTask(id) }
192     settingsTracker.afterApplyChanges { projectRefreshOperation.finishTask(id) }
193     activationProperty.afterSet({ scheduleChangeProcessing() }, parentDisposable)
194
195     Disposer.register(project, parentDisposable)
196     projectAware.subscribe(createProjectRefreshListener(projectData), parentDisposable)
197     Disposer.register(parentDisposable, Disposable { notificationAware.notificationExpire(projectId) })
198
199     loadState(projectId, projectData)
200   }
201
202   private fun registerProjectAwareDisposable(projectAware: ExternalSystemProjectAware) {
203     val projectId = projectAware.projectId
204     val projectAwareDisposable: Disposable?
205     if (projectAware is Disposable) {
206       projectAwareDisposable = projectAware
207     }
208     else {
209       projectAwareDisposable = ExternalSystemApiUtil.getManager(projectId.systemId)?.run {
210         val disposable = ExtensionPointUtil.createExtensionDisposable(this, ExternalSystemManager.EP_NAME)
211         Disposer.register(project, disposable)
212         return@run disposable
213       }
214     }
215     if (projectAwareDisposable != null) {
216       Disposer.register(projectAwareDisposable, Disposable { remove(projectId) })
217     }
218   }
219
220   override fun activate(id: ExternalSystemProjectId) {
221     val projectData = projectDataMap(id) { get(it) } ?: return
222     projectData.isActivated = true
223   }
224
225   override fun remove(id: ExternalSystemProjectId) {
226     val projectData = projectDataMap(id) { remove(it) } ?: return
227     Disposer.dispose(projectData.parentDisposable)
228   }
229
230   override fun markDirty(id: ExternalSystemProjectId) {
231     val projectData = projectDataMap(id) { get(it) } ?: return
232     projectData.status.markDirty(currentTime())
233   }
234
235   private fun projectDataMap(
236     id: ExternalSystemProjectId,
237     action: MutableMap<ExternalSystemProjectId, ProjectData>.(ExternalSystemProjectId) -> ProjectData?
238   ): ProjectData? {
239     val projectData = projectDataMap.action(id)
240     if (projectData == null) {
241       LOG.warn(String.format("Project isn't registered by id=%s", id), Throwable())
242     }
243     return projectData
244   }
245
246   override fun getState(): State {
247     val projectSettingsTrackerStates = projectDataMap.asSequence()
248       .map { (id, data) -> id.getState() to data.getState() }
249       .toMap()
250     return State(projectSettingsTrackerStates)
251   }
252
253   override fun loadState(state: State) {
254     projectStates.putAll(state.projectSettingsTrackerStates)
255     projectDataMap.forEach { (id, data) -> loadState(id, data) }
256   }
257
258   private fun loadState(projectId: ExternalSystemProjectId, projectData: ProjectData) {
259     val projectState = projectStates.remove(projectId.getState())
260     val settingsTrackerState = projectState?.settingsTracker
261     if (settingsTrackerState == null || projectState.isDirty) {
262       projectData.status.markDirty(currentTime(), EXTERNAL)
263       scheduleChangeProcessing()
264       return
265     }
266     projectData.settingsTracker.loadState(settingsTrackerState)
267     projectData.settingsTracker.refreshChanges()
268   }
269
270   override fun initializeComponent() {
271     LOG.debug("Project tracker initialization")
272     val connections = ApplicationManager.getApplication().messageBus.connect(project)
273     connections.subscribe(BatchFileChangeListener.TOPIC, createProjectChangesListener())
274     dispatcher.setRestartTimerOnAdd(true)
275     dispatcher.isPassThrough = !isAsyncChangesProcessing
276     dispatcher.activate()
277     delayDispatcher.setRestartTimerOnAdd(true)
278     delayDispatcher.isPassThrough = !isAsyncChangesProcessing
279     delayDispatcher.activate()
280   }
281
282   @TestOnly
283   fun getActivatedProjects() =
284     projectDataMap.values
285       .filter { it.isActivated }
286       .map { it.projectAware.projectId }
287       .toSet()
288
289   /**
290    * Enables auto-import in tests
291    * Note: project tracker automatically enabled out of tests
292    */
293   @TestOnly
294   fun enableAutoImportInTests() {
295     isDisabled.set(false)
296   }
297
298   init {
299     val notificationAware = ProjectNotificationAware.getInstance(project)
300     projectRefreshOperation.beforeOperation { LOG.debug("Project refresh started") }
301     projectRefreshOperation.beforeOperation { notificationAware.notificationExpire() }
302     projectRefreshOperation.afterOperation { scheduleChangeProcessing() }
303     projectRefreshOperation.afterOperation { LOG.debug("Project refresh finished") }
304     projectChangeOperation.beforeOperation { LOG.debug("Project change started") }
305     projectChangeOperation.beforeOperation { notificationAware.notificationExpire() }
306     projectChangeOperation.afterOperation { scheduleChangeProcessing() }
307     projectChangeOperation.afterOperation { LOG.debug("Project change finished") }
308     settings.autoReloadTypeProperty.afterChange { scheduleChangeProcessing() }
309     asyncChangesProcessingProperty.afterChange { dispatcher.isPassThrough = !it }
310     asyncChangesProcessingProperty.afterChange { delayDispatcher.isPassThrough = !it }
311   }
312
313   private fun ProjectData.getState() = State.Project(status.isDirty(), settingsTracker.getState())
314
315   private fun ProjectSystemId.getState() = id
316
317   private fun ExternalSystemProjectId.getState() = State.Id(systemId.getState(), externalProjectPath)
318
319   private data class ProjectData(
320     val status: ProjectStatus,
321     val activationProperty: BooleanProperty,
322     val projectAware: ExternalSystemProjectAware,
323     val settingsTracker: ProjectSettingsTracker,
324     val parentDisposable: Disposable
325   ) {
326     var isActivated by activationProperty
327
328     fun isUpToDate() = status.isUpToDate() && settingsTracker.isUpToDate()
329
330     fun getModificationType(): ModificationType? {
331       val trackerModificationType = status.getModificationType()
332       val settingsTrackerModificationType = settingsTracker.getModificationType()
333       return when {
334         trackerModificationType == null -> settingsTrackerModificationType
335         settingsTrackerModificationType == null -> trackerModificationType
336         else -> settingsTrackerModificationType.merge(trackerModificationType)
337       }
338     }
339   }
340
341   data class State(var projectSettingsTrackerStates: Map<Id, Project> = emptyMap()) {
342     data class Id(var systemId: String? = null, var externalProjectPath: String? = null)
343     data class Project(
344       var isDirty: Boolean = false,
345       var settingsTracker: ProjectSettingsTracker.State? = null
346     )
347   }
348
349   companion object {
350     @TestOnly
351     @JvmStatic
352     fun getInstance(project: Project): AutoImportProjectTracker {
353       return ExternalSystemProjectTracker.getInstance(project) as AutoImportProjectTracker
354     }
355   }
356 }