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