cleanup
[idea/community.git] / platform / external-system-impl / src / com / intellij / openapi / externalSystem / autoimport / AutoImportProjectTracker.kt
1 // 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.
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 private val LOG = Logger.getInstance("#com.intellij.openapi.externalSystem.autoimport")
40
41 @State(name = "ExternalSystemProjectTracker", storages = [Storage(CACHE_FILE)])
42 class AutoImportProjectTracker(private val project: Project) : ExternalSystemProjectTracker, PersistentStateComponent<AutoImportProjectTracker.State> {
43   @Suppress("unused")
44   private val debugThrowable = Throwable("Initialized with project=(${project.isDisposed}, ${Disposer.isDisposed(project)}, $project)")
45
46   private val LOG = Logger.getInstance("#com.intellij.openapi.externalSystem.autoimport")
47   private val AUTO_REPARSE_DELAY = DaemonCodeAnalyzerSettings.getInstance().autoReparseDelay
48   private val AUTO_RELOAD_DELAY = 2000
49
50   private val settings get() = ProjectTrackerSettings.getInstance(project)
51   private val projectStates = ConcurrentHashMap<State.Id, State.Project>()
52   private val projectDataMap = ConcurrentHashMap<ExternalSystemProjectId, ProjectData>()
53   private val isDisabled = AtomicBooleanProperty(ApplicationManager.getApplication().isUnitTestMode)
54   private val asyncChangesProcessingProperty = AtomicBooleanProperty(!ApplicationManager.getApplication().isHeadlessEnvironment)
55   private val projectChangeOperation = AnonymousParallelOperationTrace(debugName = "Project change operation")
56   private val projectRefreshOperation = CompoundParallelOperationTrace<String>(debugName = "Project refresh operation")
57   private val dispatcher = MergingUpdateQueue("AutoImportProjectTracker.dispatcher", AUTO_REPARSE_DELAY, false, null, project)
58   private val delayDispatcher = MergingUpdateQueue("AutoImportProjectTracker.delayDispatcher", AUTO_RELOAD_DELAY, false, null, project)
59   private val backgroundExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("AutoImportProjectTracker.backgroundExecutor", 1)
60
61   override var isAutoReloadExternalChanges by PropertyView(
62     settings.autoReloadTypeProperty,
63     { it == AutoReloadType.SELECTIVE },
64     { if (it) AutoReloadType.SELECTIVE else AutoReloadType.NONE }
65   )
66
67   var isAsyncChangesProcessing by asyncChangesProcessingProperty
68
69   private fun createProjectChangesListener() =
70     object : ProjectBatchFileChangeListener(project) {
71       override fun batchChangeStarted(activityName: String?) =
72         projectChangeOperation.startTask()
73
74       override fun batchChangeCompleted() =
75         projectChangeOperation.finishTask()
76     }
77
78   private fun createProjectRefreshListener(projectData: ProjectData) =
79     object : ExternalSystemProjectRefreshListener {
80       val id = "ProjectTracker: ${projectData.projectAware.projectId.readableName}"
81
82       override fun beforeProjectRefresh() {
83         projectRefreshOperation.startTask(id)
84         projectData.status.markSynchronized(currentTime())
85         projectData.isActivated = true
86       }
87
88       override fun afterProjectRefresh(status: ExternalSystemRefreshStatus) {
89         if (status != SUCCESS) projectData.status.markBroken(currentTime())
90         projectRefreshOperation.finishTask(id)
91       }
92     }
93
94   override fun scheduleProjectRefresh() {
95     LOG.debug("Schedule project refresh")
96     dispatcher.queue(PriorityEatUpdate(0) {
97       refreshProject(smart = false)
98     })
99   }
100
101   override fun scheduleProjectNotificationUpdate() {
102     LOG.debug("Schedule notification status update")
103     dispatcher.queue(PriorityEatUpdate(2) {
104       updateProjectNotification()
105     })
106   }
107
108   fun scheduleChangeProcessing() {
109     LOG.debug("Schedule change processing")
110     dispatcher.queue(PriorityEatUpdate(1) {
111       processChanges()
112     })
113   }
114
115   private fun delay(action: () -> Unit) {
116     delayDispatcher.queue(Update.create(this, action))
117   }
118
119   private fun processChanges() {
120     when (settings.autoReloadType) {
121       AutoReloadType.ALL -> when (getModificationType()) {
122         INTERNAL -> delay { refreshProject(smart = true) }
123         EXTERNAL -> delay { refreshProject(smart = true) }
124         null -> updateProjectNotification()
125       }
126       AutoReloadType.SELECTIVE -> when (getModificationType()) {
127         INTERNAL -> updateProjectNotification()
128         EXTERNAL -> delay { refreshProject(smart = true) }
129         null -> updateProjectNotification()
130       }
131       AutoReloadType.NONE -> updateProjectNotification()
132     }
133   }
134
135   private fun refreshProject(smart: Boolean) {
136     LOG.debug("Incremental project refresh")
137     if (isDisabled.get() || Registry.`is`("external.system.auto.import.disabled")) return
138     if (!projectChangeOperation.isOperationCompleted()) return
139     if (smart && !projectRefreshOperation.isOperationCompleted()) return
140     var isSkippedProjectRefresh = true
141     for (projectData in projectDataMap.values) {
142       val projectId = projectData.projectAware.projectId.readableName
143       val isAllowAutoReload = !smart || projectData.isActivated
144       if (isAllowAutoReload && !projectData.isUpToDate()) {
145         isSkippedProjectRefresh = false
146         LOG.debug("$projectId: Project refresh")
147         projectData.projectAware.refreshProject()
148       }
149       else {
150         LOG.debug("$projectId: Skip project refresh")
151       }
152     }
153     if (isSkippedProjectRefresh) {
154       updateProjectNotification()
155     }
156   }
157
158   private fun updateProjectNotification() {
159     LOG.debug("Notification status update")
160     if (isDisabled.get() || Registry.`is`("external.system.auto.import.disabled")) return
161     val notificationAware = ProjectNotificationAware.getInstance(project)
162     for ((projectId, data) in projectDataMap) {
163       when (data.isUpToDate()) {
164         true -> notificationAware.notificationExpire(projectId)
165         else -> notificationAware.notificationNotify(data.projectAware)
166       }
167     }
168   }
169
170   private fun getModificationType(): ModificationType? {
171     return projectDataMap.values
172       .asSequence()
173       .mapNotNull { it.getModificationType() }
174       .asStream()
175       .reduce(ModificationType::merge)
176       .orElse(null)
177   }
178
179   override fun register(projectAware: ExternalSystemProjectAware) {
180     val projectId = projectAware.projectId
181     val activationProperty = AtomicBooleanProperty(false)
182     val projectStatus = ProjectStatus(debugName = projectId.readableName)
183     val parentDisposable = Disposer.newDisposable(projectId.readableName)
184     val settingsTracker = ProjectSettingsTracker(project, this, backgroundExecutor, projectAware, parentDisposable)
185     val projectData = ProjectData(projectStatus, activationProperty, projectAware, settingsTracker, parentDisposable)
186     val notificationAware = ProjectNotificationAware.getInstance(project)
187
188     registerProjectAwareDisposable(projectAware)
189     projectDataMap[projectId] = projectData
190
191     val id = "ProjectSettingsTracker: ${projectData.projectAware.projectId.readableName}"
192     settingsTracker.beforeApplyChanges { projectRefreshOperation.startTask(id) }
193     settingsTracker.afterApplyChanges { projectRefreshOperation.finishTask(id) }
194     activationProperty.afterSet({ scheduleChangeProcessing() }, parentDisposable)
195
196     Disposer.register(project, parentDisposable)
197     projectAware.subscribe(createProjectRefreshListener(projectData), parentDisposable)
198     Disposer.register(parentDisposable, Disposable { notificationAware.notificationExpire(projectId) })
199
200     loadState(projectId, projectData)
201   }
202
203   private fun registerProjectAwareDisposable(projectAware: ExternalSystemProjectAware) {
204     val projectId = projectAware.projectId
205     val projectAwareDisposable: Disposable?
206     if (projectAware is Disposable) {
207       projectAwareDisposable = projectAware
208     }
209     else {
210       projectAwareDisposable = ExternalSystemApiUtil.getManager(projectId.systemId)?.run {
211         val disposable = ExtensionPointUtil.createExtensionDisposable(this, ExternalSystemManager.EP_NAME)
212         Disposer.register(project, disposable)
213         return@run disposable
214       }
215     }
216     if (projectAwareDisposable != null) {
217       Disposer.register(projectAwareDisposable, Disposable { remove(projectId) })
218     }
219   }
220
221   override fun activate(id: ExternalSystemProjectId) {
222     val projectData = projectDataMap(id) { get(it) } ?: return
223     projectData.isActivated = true
224   }
225
226   override fun remove(id: ExternalSystemProjectId) {
227     val projectData = projectDataMap(id) { remove(it) } ?: return
228     Disposer.dispose(projectData.parentDisposable)
229   }
230
231   override fun markDirty(id: ExternalSystemProjectId) {
232     val projectData = projectDataMap(id) { get(it) } ?: return
233     projectData.status.markDirty(currentTime())
234   }
235
236   private fun projectDataMap(
237     id: ExternalSystemProjectId,
238     action: MutableMap<ExternalSystemProjectId, ProjectData>.(ExternalSystemProjectId) -> ProjectData?
239   ): ProjectData? {
240     val projectData = projectDataMap.action(id)
241     if (projectData == null) {
242       LOG.warn(String.format("Project isn't registered by id=%s", id), Throwable())
243     }
244     return projectData
245   }
246
247   override fun getState(): State {
248     val projectSettingsTrackerStates = projectDataMap.asSequence()
249       .map { (id, data) -> id.getState() to data.getState() }
250       .toMap()
251     return State(projectSettingsTrackerStates)
252   }
253
254   override fun loadState(state: State) {
255     projectStates.putAll(state.projectSettingsTrackerStates)
256     projectDataMap.forEach { (id, data) -> loadState(id, data) }
257   }
258
259   private fun loadState(projectId: ExternalSystemProjectId, projectData: ProjectData) {
260     val projectState = projectStates.remove(projectId.getState())
261     val settingsTrackerState = projectState?.settingsTracker
262     if (settingsTrackerState == null || projectState.isDirty) {
263       projectData.status.markDirty(currentTime(), EXTERNAL)
264       scheduleChangeProcessing()
265       return
266     }
267     projectData.settingsTracker.loadState(settingsTrackerState)
268     projectData.settingsTracker.refreshChanges()
269   }
270
271   override fun initializeComponent() {
272     LOG.debug("Project tracker initialization")
273     ApplicationManager.getApplication().messageBus.connect(project).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 }