8ffe891f2e30caac779214ced7dadb5df64bdde3
[idea/community.git] / platform / execution-impl / src / com / intellij / execution / runToolbar / RunToolbarSlotManager.kt
1 // Copyright 2000-2021 JetBrains s.r.o. and contributors. 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.execution.runToolbar
3
4 import com.intellij.CommonBundle
5 import com.intellij.execution.IS_RUN_MANAGER_INITIALIZED
6 import com.intellij.execution.RunManager
7 import com.intellij.execution.RunManagerListener
8 import com.intellij.execution.RunnerAndConfigurationSettings
9 import com.intellij.execution.compound.CompoundRunConfiguration
10 import com.intellij.execution.impl.ExecutionManagerImpl
11 import com.intellij.execution.runToolbar.data.*
12 import com.intellij.execution.runners.ExecutionEnvironment
13 import com.intellij.ide.ActivityTracker
14 import com.intellij.lang.LangBundle
15 import com.intellij.openapi.components.service
16 import com.intellij.openapi.diagnostic.Logger
17 import com.intellij.openapi.project.Project
18 import com.intellij.openapi.ui.Messages
19 import com.intellij.openapi.util.CheckedDisposable
20 import com.intellij.openapi.util.Disposer
21 import com.intellij.ui.AppUIUtil
22 import com.intellij.util.messages.Topic
23 import java.util.*
24 import javax.swing.SwingUtilities
25
26 class RunToolbarSlotManager(val project: Project) {
27   companion object {
28     private val LOG = Logger.getInstance(RunToolbarSlotManager::class.java)
29     fun getInstance(project: Project): RunToolbarSlotManager = project.service()
30
31     @JvmField
32     @Topic.ProjectLevel
33     val RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC = Topic("RunToolbarWidgetSlotConfigurationMapChanged",
34                                                          RWSlotsConfigurationListener::class.java)
35   }
36
37   private val runToolbarSettings = RunToolbarSettings.getInstance(project)
38
39   internal val slotListeners = RWSlotController()
40   internal val activeListener = RWAddedController()
41   internal val stateListeners = RWStateController()
42
43   internal var mainSlotData = SlotDate(UUID.randomUUID().toString())
44
45   val activeProcesses = RWActiveProcesses()
46   private val dataIds = mutableListOf<String>()
47
48   private val slotsData = mutableMapOf<String, SlotDate>()
49
50   private var activeDisposable: CheckedDisposable? = null
51
52   private val processController = RWProcessController(project)
53
54   internal var active: Boolean = false
55     set(value) {
56       if (field == value) return
57
58       field = value
59
60       if (value) {
61         if (RunToolbarProcess.logNeeded) LOG.info(
62           "ACTIVE SM settings: new on top ${runToolbarSettings.getMoveNewOnTop()}; update by selected ${getUpdateMainBySelected()} RunToolbar")
63         clear()
64
65         val disp = Disposer.newCheckedDisposable()
66         Disposer.register(project, disp)
67         activeDisposable = disp
68
69         val settingsData =  runToolbarSettings.getConfigurations()
70         val slotOrder = settingsData.first
71         val configurations = settingsData.second
72
73         slotOrder.filter { configurations[it] != null }.forEachIndexed { index, s ->
74           if (index == 0) {
75             mainSlotData.updateId(s)
76             mainSlotData.configuration = configurations[s]
77             slotsData[mainSlotData.id] = mainSlotData
78           }
79           else {
80             addSlot(configurations[s], s)
81           }
82         }
83
84         if (RunToolbarProcess.logNeeded) LOG.info("SM restoreRunConfigurations: ${configurations.values} RunToolbar")
85
86         val con = project.messageBus.connect(disp)
87
88         con.subscribe(RunManagerListener.TOPIC, object : RunManagerListener {
89           override fun runConfigurationSelected(settings: RunnerAndConfigurationSettings?) {
90             if (!getUpdateMainBySelected() || mainSlotData.configuration == settings) return
91
92             mainSlotData.environment?.let {
93               val slot = addSlot(settings)
94               if (RunToolbarProcess.logNeeded) LOG.info("SM runConfigurationSelected: $settings first slot added RunToolbar")
95               moveToTop(slot.id)
96             } ?: kotlin.run {
97               mainSlotData.configuration = settings
98               if (RunToolbarProcess.logNeeded) LOG.info("SM runConfigurationSelected: $settings change main configuration RunToolbar")
99               update()
100             }
101           }
102
103           override fun runConfigurationRemoved(settings: RunnerAndConfigurationSettings) {
104             var changed = false
105             slotsData.filter { it.value == settings && it.value.environment == null }.forEach {
106               changed = true
107               it.value.configuration = RunManager.getInstance(project).selectedConfiguration
108             }
109             if (changed) {
110               update()
111             }
112           }
113         })
114
115         val executions = processController.getActiveExecutions()
116         executions.filter { it.isRunning() == true }.forEach { addNewProcess(it) }
117         activeListener.enabled()
118
119         update()
120
121         SwingUtilities.invokeLater {
122           ActivityTracker.getInstance().inc()
123         }
124       }
125       else {
126         activeDisposable?.let {
127           if (!it.isDisposed)
128             Disposer.dispose(it)
129           activeDisposable = null
130         }
131
132         activeListener.disabled()
133         clear()
134         if (RunToolbarProcess.logNeeded) LOG.info(
135           "INACTIVE SM RunToolbar")
136
137       }
138       slotListeners.rebuildPopup()
139       publishConfigurations(getConfigurationMap())
140     }
141
142   private fun getUpdateMainBySelected(): Boolean {
143     return runToolbarSettings.getUpdateMainBySelected()
144   }
145
146   private fun getMoveNewOnTop(executionEnvironment: ExecutionEnvironment): Boolean {
147     if (!runToolbarSettings.getMoveNewOnTop()) return false
148     val suppressValue = executionEnvironment.getUserData(RunToolbarData.RUN_TOOLBAR_SUPPRESS_MAIN_SLOT_USER_DATA_KEY) ?: false
149     return !suppressValue
150   }
151
152   private fun clear() {
153     dataIds.clear()
154
155     slotsData.clear()
156     slotsData[mainSlotData.id] = mainSlotData
157
158     activeProcesses.clear()
159     state = RWSlotManagerState.INACTIVE
160   }
161
162
163   private fun traceState() {
164     if (!RunToolbarProcess.logNeeded) return
165
166     val separator = " "
167     val ids = dataIds.indices.mapNotNull { "${it + 1}: ${slotsData[dataIds[it]]}" }.joinToString(", ")
168     LOG.info("SM state: $state" +
169              "${separator}== slots: 0: ${mainSlotData}, $ids" +
170              "${separator}== slotsData: ${slotsData.values} RunToolbar")
171   }
172
173
174   init {
175     SwingUtilities.invokeLater {
176       if (project.isDisposed) return@invokeLater
177
178       slotsData[mainSlotData.id] = mainSlotData
179
180       activeListener.addListener(RunToolbarShortcutHelper(project))
181
182       Disposer.register(project) {
183         activeListener.clear()
184         stateListeners.clear()
185         slotListeners.clear()
186       }
187     }
188   }
189
190   private fun update() {
191     saveSlotsConfiguration()
192     updateState()
193
194     if (!RunToolbarProcess.logNeeded) return
195     LOG.trace("!!!!!UPDATE RunToolbar")
196   }
197
198   internal fun startWaitingForAProcess(slotDate: RunToolbarData, settings: RunnerAndConfigurationSettings, executorId: String) {
199     slotsData.values.forEach {
200       val waitingForAProcesses = it.waitingForAProcesses
201       if (slotDate == it) {
202         waitingForAProcesses.start(project, settings, executorId)
203       }
204       else {
205         if (waitingForAProcesses.isWaitingForASubProcess(settings, executorId)) {
206           waitingForAProcesses.clear()
207         }
208       }
209     }
210   }
211
212   internal fun getMainOrFirstActiveProcess(): RunToolbarProcess? {
213     return mainSlotData.environment?.getRunToolbarProcess() ?: activeProcesses.processes.keys.firstOrNull()
214   }
215
216   internal fun slotsCount(): Int {
217     return dataIds.size
218   }
219
220   private var state: RWSlotManagerState = RWSlotManagerState.INACTIVE
221     set(value) {
222       if (value == field) return
223       field = value
224       traceState()
225       stateListeners.stateChanged(value)
226     }
227
228   private fun updateState() {
229     state = when (activeProcesses.getActiveCount()) {
230       0 -> RWSlotManagerState.INACTIVE
231       1 -> {
232         mainSlotData.environment?.let {
233           RWSlotManagerState.SINGLE_MAIN
234         } ?: RWSlotManagerState.SINGLE_PLAIN
235       }
236       else -> {
237         mainSlotData.environment?.let {
238           RWSlotManagerState.MULTIPLE_WITH_MAIN
239         } ?: RWSlotManagerState.MULTIPLE
240       }
241     }
242   }
243
244   internal fun getState(): RWSlotManagerState {
245     return state
246   }
247
248   private fun getAppropriateSettings(env: ExecutionEnvironment): Iterable<SlotDate> {
249     val sortedSlots = mutableListOf<SlotDate>()
250     sortedSlots.add(mainSlotData)
251     sortedSlots.addAll(dataIds.mapNotNull { slotsData[it] }.toList())
252
253     return sortedSlots.filter { it.configuration == env.runnerAndConfigurationSettings }
254   }
255
256   internal fun processNotStarted(env: ExecutionEnvironment) {
257     val config = env.runnerAndConfigurationSettings ?: return
258
259     val appropriateSettings = getAppropriateSettings(env)
260     val emptySlotsWithConfiguration = appropriateSettings.filter { it.environment == null }
261
262     emptySlotsWithConfiguration.map { it.waitingForAProcesses }.firstOrNull {
263       it.isWaitingForASingleProcess(config, env.executor.id)
264     }?.clear() ?: run {
265       slotsData.values.filter { it.configuration?.configuration is CompoundRunConfiguration }.firstOrNull { slotsData ->
266         slotsData.waitingForAProcesses.isWaitingForASubProcess(config, env.executor.id)
267       }?.clear()
268     }
269   }
270
271   internal fun processStarted(env: ExecutionEnvironment) {
272     addNewProcess(env)
273     update()
274     SwingUtilities.invokeLater {
275       ActivityTracker.getInstance().inc()
276     }
277   }
278
279   private fun addNewProcess(env: ExecutionEnvironment) {
280     val appropriateSettings = getAppropriateSettings(env)
281     val emptySlotsWithConfiguration = appropriateSettings.filter { it.environment == null }
282
283     var newSlot = false
284     val slot = appropriateSettings.firstOrNull { it.environment?.executionId == env.executionId }
285                ?: emptySlotsWithConfiguration.firstOrNull { slotData ->
286                  env.runnerAndConfigurationSettings?.let {
287                    slotData.waitingForAProcesses.isWaitingForASingleProcess(it, env.executor.id)
288                  } ?: false
289                }
290                ?: emptySlotsWithConfiguration.firstOrNull()
291                ?: kotlin.run {
292                  newSlot = true
293                  addSlot(env.runnerAndConfigurationSettings)
294                }
295
296     slot.environment = env
297     activeProcesses.updateActiveProcesses(slotsData)
298
299     if (newSlot) {
300       val isCompoundProcess = slotsData.values.filter { it.configuration?.configuration is CompoundRunConfiguration }.firstOrNull { slotsData ->
301         env.runnerAndConfigurationSettings?.let {
302           slotsData.waitingForAProcesses.checkAndUpdate(it, env.executor.id)
303         } ?: false
304       } != null
305
306       if (!isCompoundProcess) {
307         if (getMoveNewOnTop(env)) {
308           moveToTop(slot.id)
309         }
310       }
311     }
312   }
313
314   fun processTerminating(env: ExecutionEnvironment) {
315     slotsData.values.firstOrNull { it.environment?.executionId == env.executionId }?.let {
316       it.environment = env
317     }
318     activeProcesses.updateActiveProcesses(slotsData)
319     updateState()
320     SwingUtilities.invokeLater {
321       ActivityTracker.getInstance().inc()
322     }
323   }
324
325   fun processTerminated(executionId: Long) {
326     slotsData.values.firstOrNull { it.environment?.executionId == executionId }?.let { slotDate ->
327       val removable = slotDate.environment?.runnerAndConfigurationSettings?.let {
328         !RunManager.getInstance(project).hasSettings(it)
329       } ?: true
330
331       if (removable) {
332         if (slotDate == mainSlotData && slotsData.size == 1) {
333           slotDate.clear()
334           slotDate.configuration = RunManager.getInstance(project).selectedConfiguration
335         }
336         else {
337           removeSlot(slotDate.id)
338         }
339       }
340       else {
341         slotDate.environment = null
342       }
343     }
344
345     if (RunToolbarProcess.logNeeded) LOG.info("SM process stopped: $executionId RunToolbar")
346     activeProcesses.updateActiveProcesses(slotsData)
347     updateState()
348
349     SwingUtilities.invokeLater {
350       ActivityTracker.getInstance().inc()
351     }
352   }
353
354   internal fun addAndSaveSlot(): SlotDate {
355     val slot = addSlot()
356     saveSlotsConfiguration()
357     return slot
358   }
359
360   private fun addSlot(configuration: RunnerAndConfigurationSettings? = null, id: String = UUID.randomUUID().toString()): SlotDate {
361     val slot = SlotDate(id)
362     slot.configuration = configuration
363     dataIds.add(slot.id)
364     slotsData[slot.id] = slot
365
366     slotListeners.slotAdded()
367
368     return slot
369   }
370
371   internal fun getData(index: Int): SlotDate? {
372     return if (index >= 0 && index < dataIds.size) {
373       dataIds[index].let {
374         slotsData[it]
375       }
376     }
377     else null
378   }
379
380
381   internal fun moveToTop(id: String) {
382     if (mainSlotData.id == id) return
383
384     slotsData[id]?.let { newMain ->
385       val oldMain = mainSlotData
386       mainSlotData = newMain
387
388       dataIds.remove(id)
389       dataIds.add(0, oldMain.id)
390     }
391
392     update()
393   }
394
395   internal fun removeSlot(id: String) {
396     val index = dataIds.indexOf(id)
397
398     fun remove() {
399       if (id == mainSlotData.id) {
400         if (dataIds.isNotEmpty()) {
401           val firstSlotId = dataIds[0]
402           slotsData[firstSlotId]?.let {
403             mainSlotData = it
404             slotsData.remove(id)
405             dataIds.remove(it.id)
406           }
407         }
408       }
409       else {
410         slotsData.remove(id)
411         dataIds.remove(id)
412       }
413
414       SwingUtilities.invokeLater {
415         slotListeners.slotRemoved(index)
416         ActivityTracker.getInstance().inc()
417       }
418     }
419
420     (if (index >= 0) getData(index) else if (mainSlotData.id == id) mainSlotData else null)?.let { slotDate ->
421       slotDate.environment?.let {
422         if (it.isRunning() != true) {
423           remove()
424         }
425         else if (Messages.showOkCancelDialog(
426             project,
427             LangBundle.message("run.toolbar.remove.active.process.slot.message"),
428             LangBundle.message("run.toolbar.remove.active.process.slot.title", it.runnerAndConfigurationSettings?.name ?: ""),
429             LangBundle.message("run.toolbar.remove.active.process.slot.ok"),
430             CommonBundle.getCancelButtonText(),
431             Messages.getQuestionIcon()/*, object : DialogWrapper.DoNotAskOption.Adapter() {
432               override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
433
434               }
435             }*/) == Messages.OK) {
436           it.contentToReuse?.let {
437             ExecutionManagerImpl.stopProcess(it)
438           }
439
440           remove()
441         }
442       } ?: run {
443         remove()
444       }
445     } ?: slotListeners.rebuildPopup()
446
447     update()
448   }
449
450   internal fun configurationChanged(slotId: String, configuration: RunnerAndConfigurationSettings?) {
451     AppUIUtil.invokeLaterIfProjectAlive(project) {
452       project.messageBus.syncPublisher(RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC).configurationChanged(slotId, configuration)
453     }
454     saveSlotsConfiguration()
455   }
456
457   private fun saveSlotsConfiguration() {
458     if (IS_RUN_MANAGER_INITIALIZED.get(project) == true) {
459       val runManager = RunManager.getInstance(project)
460       mainSlotData.configuration?.let {
461         if (runManager.hasSettings(it) &&
462             it != runManager.selectedConfiguration &&
463             mainSlotData.environment?.getRunToolbarProcess()?.isTemporaryProcess() != true) {
464           runManager.selectedConfiguration = mainSlotData.configuration
465           if (RunToolbarProcess.logNeeded) LOG.info(
466             "MANAGER saveSlotsConfiguration. change selected configuration by main: ${mainSlotData.configuration} RunToolbar")
467         }
468       }
469     }
470
471     val slotOrder = getSlotOrder()
472     val configurations = getConfigurationMap(slotOrder)
473     if (RunToolbarProcess.logNeeded) LOG.info("MANAGER saveSlotsConfiguration: ${configurations} RunToolbar")
474
475     runToolbarSettings.setConfigurations(configurations, slotOrder)
476     publishConfigurations(configurations)
477   }
478
479   private fun getSlotOrder(): List<String> {
480     val list = mutableListOf<String>()
481     list.add(mainSlotData.id)
482     list.addAll(dataIds)
483     return list
484   }
485
486   private fun getConfigurationMap(slotOrder: List<String>): Map<String, RunnerAndConfigurationSettings?> {
487     return slotOrder.associateWith { slotsData[it]?.configuration }
488   }
489
490   fun getConfigurationMap(): Map<String, RunnerAndConfigurationSettings?> {
491     return getConfigurationMap(getSlotOrder())
492   }
493
494   private fun publishConfigurations(slotConfigurations: Map<String, RunnerAndConfigurationSettings?>) {
495     AppUIUtil.invokeLaterIfProjectAlive(project) {
496       project.messageBus.syncPublisher(RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC).slotsConfigurationChanged(slotConfigurations)
497     }
498   }
499 }
500
501 internal open class SlotDate(override var id: String) : RunToolbarData {
502   companion object {
503     var index = 0
504   }
505
506   fun updateId(value: String) {
507     id = value
508   }
509
510   override var configuration: RunnerAndConfigurationSettings? = null
511     get() = environment?.runnerAndConfigurationSettings ?: field
512
513   override var environment: ExecutionEnvironment? = null
514     set(value) {
515       if (field != value)
516         field = value
517       value?.let {
518         configuration = it.runnerAndConfigurationSettings
519       } ?: run {
520         waitingForAProcesses.clear()
521       }
522     }
523
524   override val waitingForAProcesses = RWWaitingForAProcesses()
525
526   override fun clear() {
527     environment = null
528     waitingForAProcesses.clear()
529   }
530
531   override fun toString(): String {
532     return "$id-${environment?.let { "$it [${it.executor.actionName} ${it.executionId}]" } ?: configuration?.configuration?.name ?: "configuration null"}"
533   }
534 }