IDEA-287497 Support running current file in the additional running options popup
[idea/community.git] / platform / execution-impl / src / com / intellij / execution / actions / RunConfigurationsComboBoxAction.java
1 // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2 package com.intellij.execution.actions;
3
4 import com.intellij.execution.*;
5 import com.intellij.execution.executors.DefaultRunExecutor;
6 import com.intellij.execution.executors.ExecutorGroup;
7 import com.intellij.execution.impl.EditConfigurationsDialog;
8 import com.intellij.execution.impl.RunManagerImpl;
9 import com.intellij.icons.AllIcons;
10 import com.intellij.ide.DataManager;
11 import com.intellij.idea.ActionsBundle;
12 import com.intellij.openapi.actionSystem.*;
13 import com.intellij.openapi.actionSystem.ex.ActionUtil;
14 import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
15 import com.intellij.openapi.application.ApplicationManager;
16 import com.intellij.openapi.project.DumbAware;
17 import com.intellij.openapi.project.DumbAwareAction;
18 import com.intellij.openapi.project.IndexNotReadyException;
19 import com.intellij.openapi.project.Project;
20 import com.intellij.openapi.util.NlsSafe;
21 import com.intellij.openapi.util.registry.Registry;
22 import com.intellij.ui.SizedIcon;
23 import com.intellij.ui.components.panels.NonOpaquePanel;
24 import com.intellij.ui.scale.JBUIScale;
25 import com.intellij.util.PlatformUtils;
26 import com.intellij.util.containers.ContainerUtil;
27 import com.intellij.util.ui.EmptyIcon;
28 import com.intellij.util.ui.JBUI;
29 import com.intellij.util.ui.UIUtil;
30 import org.jetbrains.annotations.ApiStatus;
31 import org.jetbrains.annotations.NotNull;
32 import org.jetbrains.annotations.Nullable;
33
34 import javax.swing.*;
35 import javax.swing.border.Border;
36 import java.awt.*;
37 import java.awt.event.ActionEvent;
38 import java.awt.event.InputEvent;
39 import java.awt.event.MouseEvent;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.function.Function;
43
44 public class RunConfigurationsComboBoxAction extends ComboBoxAction implements DumbAware {
45   private static final String BUTTON_MODE = "ButtonMode";
46
47   public static final Icon CHECKED_ICON = JBUIScale.scaleIcon(new SizedIcon(AllIcons.Actions.Checked, 16, 16));
48   public static final Icon CHECKED_SELECTED_ICON = JBUIScale.scaleIcon(new SizedIcon(AllIcons.Actions.Checked_selected, 16, 16));
49   public static final Icon EMPTY_ICON = EmptyIcon.ICON_16;
50
51   public static boolean hasRunCurrentFileItem(@NotNull Project project) {
52     if (RunManager.getInstance(project).isRunWidgetActive()) {
53       // Run Widget shows up only in Rider. In other IDEs it's a secret feature backed by the "ide.run.widget" Registry key.
54       // The 'Run Current File' feature doesn't look great together with the Run Widget.
55       return false;
56     }
57
58     if (PlatformUtils.isIntelliJ()) return true;
59     if (PlatformUtils.isPhpStorm()) return true;
60     if (PlatformUtils.isWebStorm()) return true;
61     if (PlatformUtils.isRubyMine()) return true;
62     if (PlatformUtils.isPyCharmPro()) return true;
63     if (PlatformUtils.isPyCharmCommunity()) return true;
64
65     return Registry.is("run.current.file.item.in.run.configurations.combobox");
66   }
67
68   @Override
69   public @NotNull ActionUpdateThread getActionUpdateThread() {
70     return ActionUpdateThread.BGT;
71   }
72
73   @Override
74   public void update(@NotNull AnActionEvent e) {
75     Presentation presentation = e.getPresentation();
76     Project project = e.getData(CommonDataKeys.PROJECT);
77     if (ActionPlaces.isMainMenuOrActionSearch(e.getPlace())) {
78       presentation.setDescription(ExecutionBundle.messagePointer("choose.run.configuration.action.description"));
79     }
80     try {
81       if (project == null || project.isDisposed() || !project.isOpen()) {
82         updatePresentation(null, null, null, presentation, e.getPlace());
83         presentation.setEnabled(false);
84       }
85       else {
86         updatePresentation(getSelectedExecutionTarget(e),
87                            getSelectedConfiguration(e),
88                            project,
89                            presentation,
90                            e.getPlace());
91         presentation.setEnabled(true);
92       }
93     }
94     catch (IndexNotReadyException e1) {
95       presentation.setEnabled(false);
96     }
97   }
98
99   protected @Nullable ExecutionTarget getSelectedExecutionTarget(AnActionEvent e) {
100     Project project = e.getProject();
101     return project == null ? null : ExecutionTargetManager.getActiveTarget(project);
102   }
103
104   protected @Nullable RunnerAndConfigurationSettings getSelectedConfiguration(AnActionEvent e) {
105     Project project = e.getProject();
106     return project == null ? null : RunManager.getInstance(project).getSelectedConfiguration();
107   }
108
109   protected static void updatePresentation(@Nullable ExecutionTarget target,
110                                          @Nullable RunnerAndConfigurationSettings settings,
111                                          @Nullable Project project,
112                                          @NotNull Presentation presentation,
113                                          String actionPlace) {
114     presentation.putClientProperty(BUTTON_MODE, null);
115     if (project != null && target != null && settings != null) {
116       String name = Executor.shortenNameIfNeeded(settings.getName());
117       if (target != DefaultExecutionTarget.INSTANCE && !target.isExternallyManaged()) {
118         name += " | " + target.getDisplayName();
119       } else {
120         if (!ExecutionTargetManager.canRun(settings.getConfiguration(), target)) {
121           name += " | " + ExecutionBundle.message("run.configurations.combo.action.nothing.to.run.on");
122         }
123       }
124       presentation.setText(name, false);
125       if (!ApplicationManager.getApplication().isUnitTestMode()) {
126         setConfigurationIcon(presentation, settings, project);
127       }
128     }
129     else {
130       if (project != null && hasRunCurrentFileItem(project)) {
131         presentation.setText(ExecutionBundle.messagePointer("run.configurations.combo.run.current.file.selected"));
132         presentation.setIcon(null);
133         return;
134       }
135
136       presentation.putClientProperty(BUTTON_MODE, Boolean.TRUE);
137       presentation.setText(ExecutionBundle.messagePointer("action.presentation.RunConfigurationsComboBoxAction.text"));
138       presentation.setDescription(ActionsBundle.actionDescription(IdeActions.ACTION_EDIT_RUN_CONFIGURATIONS));
139       if (ActionPlaces.TOUCHBAR_GENERAL.equals(actionPlace))
140         presentation.setIcon(AllIcons.General.Add);
141       else
142         presentation.setIcon(null);
143     }
144   }
145
146   protected static void setConfigurationIcon(final Presentation presentation,
147                                            final RunnerAndConfigurationSettings settings,
148                                            final Project project) {
149     try {
150       presentation.setIcon(RunManagerEx.getInstanceEx(project).getConfigurationIcon(settings, true));
151     }
152     catch (IndexNotReadyException ignored) {
153     }
154   }
155
156   @Override
157   public void actionPerformed(@NotNull AnActionEvent e) {
158     if (ActionPlaces.TOUCHBAR_GENERAL.equals(e.getPlace())) {
159       final Presentation presentation = e.getPresentation();
160       if (Boolean.TRUE.equals(presentation.getClientProperty(BUTTON_MODE))) {
161         InputEvent inputEvent = e.getInputEvent();
162         Component component = inputEvent != null ? inputEvent.getComponent() : null;
163         if (component != null) {
164           performWhenButton(component, ActionPlaces.TOUCHBAR_GENERAL);
165         }
166         return;
167       }
168     }
169     super.actionPerformed(e);
170   }
171
172   @Override
173   protected boolean shouldShowDisabledActions() {
174     return true;
175   }
176
177   @NotNull
178   @Override
179   public JComponent createCustomComponent(@NotNull final Presentation presentation, @NotNull String place) {
180     ComboBoxButton button = new RunConfigurationsComboBoxButton(presentation);
181     NonOpaquePanel panel = new NonOpaquePanel(new BorderLayout());
182     Border border = UIUtil.isUnderDefaultMacTheme() ?
183                     JBUI.Borders.empty(0, 2) : JBUI.Borders.empty(0, 5, 0, 4);
184
185     panel.setBorder(border);
186     panel.add(button);
187     return panel;
188   }
189
190   private static void performWhenButton(@NotNull Component src, String place) {
191     ActionManager manager = ActionManager.getInstance();
192     manager.tryToExecute(manager.getAction(IdeActions.ACTION_EDIT_RUN_CONFIGURATIONS),
193       new MouseEvent(src, MouseEvent.MOUSE_PRESSED, System.currentTimeMillis(), 0, 0, 0, 0,false, 0),
194       src, place, true
195     );
196   }
197
198   /**
199    * @deprecated It is a temporary function just to reuse existing code. Will be soon deleted.
200    */
201   @SuppressWarnings("DeprecatedIsStillUsed")
202   @Deprecated
203   @ApiStatus.Internal
204   @NotNull
205   public DefaultActionGroup createPopupActionGroupOpen(final JComponent button) {
206     return createPopupActionGroup(button);
207   }
208
209   @Override
210   @NotNull
211   protected DefaultActionGroup createPopupActionGroup(final JComponent button) {
212     final DefaultActionGroup allActionsGroup = new DefaultActionGroup();
213     final Project project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(button));
214     if (project == null) {
215       return allActionsGroup;
216     }
217
218     AnAction editRunConfigurationAction = getEditRunConfigurationAction();
219     if(editRunConfigurationAction != null) {
220       allActionsGroup.add(editRunConfigurationAction);
221     }
222     allActionsGroup.add(new SaveTemporaryAction());
223     allActionsGroup.addSeparator();
224
225     addTargetGroup(project, allActionsGroup);
226
227     allActionsGroup.add(new RunCurrentFileAction(executor -> true));
228     allActionsGroup.addSeparator(ExecutionBundle.message("run.configurations.popup.existing.configurations.separator.text"));
229
230     for (Map<String, List<RunnerAndConfigurationSettings>> structure : RunManagerImpl.getInstanceImpl(project).getConfigurationsGroupedByTypeAndFolder(true).values()) {
231       final DefaultActionGroup actionGroup = new DefaultActionGroup();
232       for (Map.Entry<String, List<RunnerAndConfigurationSettings>> entry : structure.entrySet()) {
233         @NlsSafe String folderName = entry.getKey();
234         DefaultActionGroup group = folderName == null ? actionGroup : DefaultActionGroup.createPopupGroup(() -> folderName);
235         group.getTemplatePresentation().setIcon(AllIcons.Nodes.Folder);
236         for (RunnerAndConfigurationSettings settings : entry.getValue()) {
237           group.add(createFinalAction(settings, project));
238         }
239         if (group != actionGroup) {
240           actionGroup.add(group);
241         }
242       }
243
244       allActionsGroup.add(actionGroup);
245       allActionsGroup.addSeparator();
246     }
247     return allActionsGroup;
248   }
249
250   protected void addTargetGroup(Project project, DefaultActionGroup allActionsGroup) {
251     RunnerAndConfigurationSettings selected = RunManager.getInstance(project).getSelectedConfiguration();
252     if (selected != null) {
253       ExecutionTarget activeTarget = ExecutionTargetManager.getActiveTarget(project);
254       for (ExecutionTarget eachTarget : ExecutionTargetManager.getTargetsToChooseFor(project, selected.getConfiguration())) {
255         allActionsGroup.add(new SelectTargetAction(project, eachTarget, eachTarget.equals(activeTarget)));
256       }
257       allActionsGroup.addSeparator();
258     }
259   }
260
261   protected @Nullable AnAction getEditRunConfigurationAction() {
262     return ActionManager.getInstance().getAction(IdeActions.ACTION_EDIT_RUN_CONFIGURATIONS);
263   }
264
265   protected AnAction createFinalAction(@NotNull final RunnerAndConfigurationSettings configuration, @NotNull final Project project) {
266     return new SelectConfigAction(configuration, project, executor -> true);
267   }
268
269   public class RunConfigurationsComboBoxButton extends ComboBoxButton {
270
271     public RunConfigurationsComboBoxButton(@NotNull Presentation presentation) {
272       super(presentation);
273     }
274
275     @Override
276     public Dimension getPreferredSize() {
277       Dimension d = super.getPreferredSize();
278       d.width = Math.max(d.width, JBUIScale.scale(75));
279       return d;
280     }
281
282     @Override
283     protected void doShiftClick() {
284       DataContext context = DataManager.getInstance().getDataContext(this);
285       final Project project = CommonDataKeys.PROJECT.getData(context);
286       if (project != null && !ActionUtil.isDumbMode(project)) {
287         new EditConfigurationsDialog(project).show();
288         return;
289       }
290       super.doShiftClick();
291     }
292
293     @Override
294     protected void fireActionPerformed(ActionEvent event) {
295       if (Boolean.TRUE.equals(getPresentation().getClientProperty(BUTTON_MODE))) {
296         performWhenButton(this, ActionPlaces.UNKNOWN);
297         return;
298       }
299
300       super.fireActionPerformed(event);
301     }
302
303     @Override
304     protected boolean isArrowVisible(@NotNull Presentation presentation) {
305       return !Boolean.TRUE.equals(presentation.getClientProperty(BUTTON_MODE));
306     }
307   }
308
309
310
311   private static final class SaveTemporaryAction extends DumbAwareAction {
312     SaveTemporaryAction() {
313       Presentation presentation = getTemplatePresentation();
314       presentation.setIcon(AllIcons.Actions.MenuSaveall);
315     }
316
317     @Override
318     public void actionPerformed(@NotNull final AnActionEvent e) {
319       final Project project = e.getData(CommonDataKeys.PROJECT);
320       if (project != null) {
321         RunnerAndConfigurationSettings settings = chooseTempSettings(project);
322         if (settings != null) {
323           final RunManager runManager = RunManager.getInstance(project);
324           runManager.makeStable(settings);
325         }
326       }
327     }
328
329     @Override
330     public void update(@NotNull final AnActionEvent e) {
331       final Presentation presentation = e.getPresentation();
332       final Project project = e.getData(CommonDataKeys.PROJECT);
333       if (project == null) {
334         disable(presentation);
335         return;
336       }
337       RunnerAndConfigurationSettings settings = chooseTempSettings(project);
338       if (settings == null) {
339         disable(presentation);
340       }
341       else {
342         presentation.setText(ExecutionBundle.messagePointer("save.temporary.run.configuration.action.name", Executor.shortenNameIfNeeded(settings.getName())));
343         //noinspection DialogTitleCapitalization
344         presentation.setDescription(presentation.getText());
345         presentation.setEnabledAndVisible(true);
346       }
347     }
348
349     private static void disable(final Presentation presentation) {
350       presentation.setEnabledAndVisible(false);
351     }
352
353     @Nullable
354     private static RunnerAndConfigurationSettings chooseTempSettings(@NotNull Project project) {
355       RunnerAndConfigurationSettings selectedConfiguration = RunManager.getInstance(project).getSelectedConfiguration();
356       if (selectedConfiguration != null && selectedConfiguration.isTemporary()) {
357         return selectedConfiguration;
358       }
359       return ContainerUtil.getFirstItem(RunManager.getInstance(project).getTempConfigurationsList());
360     }
361   }
362
363
364   private static void addExecutorActions(@NotNull DefaultActionGroup group,
365                                          @NotNull Function<? super Executor, ? extends ExecutorRegistryImpl.ExecutorAction> actionCreator,
366                                          @NotNull Function<? super Executor, Boolean> executorFilter) {
367     for (Executor executor : Executor.EXECUTOR_EXTENSION_NAME.getExtensionList()) {
368       if (executor instanceof ExecutorGroup) {
369         for (Executor childExecutor : ((ExecutorGroup<?>)executor).childExecutors()) {
370           if (executorFilter.apply(childExecutor)) {
371             group.addAction(actionCreator.apply(childExecutor));
372           }
373         }
374       }
375       else {
376         if (executorFilter.apply(executor)) {
377           group.addAction(actionCreator.apply(executor));
378         }
379       }
380     }
381   }
382
383   @ApiStatus.Internal
384   public static class RunCurrentFileAction extends DefaultActionGroup implements DumbAware {
385     private final @NotNull Function<? super Executor, Boolean> myExecutorFilter;
386
387     public RunCurrentFileAction(@NotNull Function<? super Executor, Boolean> executorFilter) {
388       super(ExecutionBundle.messagePointer("run.configurations.combo.run.current.file.item.in.dropdown"),
389             ExecutionBundle.messagePointer("run.configurations.combo.run.current.file.description"),
390             null);
391       myExecutorFilter = executorFilter;
392       setPopup(true);
393       getTemplatePresentation().setPerformGroup(true);
394
395       addSubActions();
396     }
397
398     private void addSubActions() {
399       // Add actions similar to com.intellij.execution.actions.ChooseRunConfigurationPopup.ConfigurationActionsStep#buildActions
400       addExecutorActions(this, ExecutorRegistryImpl.RunCurrentFileExecutorAction::new, myExecutorFilter);
401       addSeparator();
402       addAction(new ExecutorRegistryImpl.EditRunConfigAndRunCurrentFileExecutorAction(DefaultRunExecutor.getRunExecutorInstance()));
403     }
404
405     @Override
406     public void update(@NotNull AnActionEvent e) {
407       e.getPresentation().setEnabledAndVisible(e.getProject() != null && hasRunCurrentFileItem(e.getProject()));
408     }
409
410     @Override
411     public void actionPerformed(@NotNull AnActionEvent e) {
412       Project project = e.getProject();
413       if (project == null) return;
414
415       RunManager.getInstance(project).setSelectedConfiguration(null);
416       updatePresentation(null, null, project, e.getPresentation(), e.getPlace());
417     }
418   }
419
420
421   private static final class SelectTargetAction extends AnAction {
422     private final Project myProject;
423     private final ExecutionTarget myTarget;
424
425     SelectTargetAction(final Project project, final ExecutionTarget target, boolean selected) {
426       myProject = project;
427       myTarget = target;
428
429       String name = target.getDisplayName();
430       Presentation presentation = getTemplatePresentation();
431       presentation.setText(name, false);
432       presentation.setDescription(ExecutionBundle.message("select.0", name));
433
434       presentation.setIcon(selected ? CHECKED_ICON : EMPTY_ICON);
435       presentation.setSelectedIcon(selected ? CHECKED_SELECTED_ICON : EMPTY_ICON);
436     }
437
438     @Override
439     public void actionPerformed(@NotNull AnActionEvent e) {
440       ExecutionTargetManager.setActiveTarget(myProject, myTarget);
441       updatePresentation(ExecutionTargetManager.getActiveTarget(myProject),
442                          RunManager.getInstance(myProject).getSelectedConfiguration(),
443                          myProject,
444                          e.getPresentation(),
445                          e.getPlace());
446     }
447
448     @Override
449     public boolean isDumbAware() {
450       RunnerAndConfigurationSettings configuration = RunManager.getInstance(myProject).getSelectedConfiguration();
451       return configuration == null || configuration.getType().isDumbAware();
452     }
453   }
454
455   @ApiStatus.Internal
456   public static final class SelectConfigAction extends DefaultActionGroup implements DumbAware {
457     private final RunnerAndConfigurationSettings myConfiguration;
458     private final Project myProject;
459     private final @NotNull Function<? super Executor, Boolean> myExecutorFilter;
460
461     public SelectConfigAction(final RunnerAndConfigurationSettings configuration, final Project project, @NotNull Function<? super Executor, Boolean> executorFilter) {
462       myConfiguration = configuration;
463       myProject = project;
464       myExecutorFilter = executorFilter;
465
466       setPopup(true);
467       getTemplatePresentation().setPerformGroup(true);
468
469       String name = Executor.shortenNameIfNeeded(configuration.getName());
470       if (name.isEmpty()) {
471         name = " ";
472       }
473       final Presentation presentation = getTemplatePresentation();
474       presentation.setText(name, false);
475       presentation.setDescription(ExecutionBundle.message("select.0.1", configuration.getType().getConfigurationTypeDescription(), name));
476       updateIcon(presentation);
477
478       // Secondary menu for the existing run configurations is not directly related to the 'Run Current File' feature.
479       // We may reconsider changing this to `if (!RunManager.getInstance(project).isRunWidgetActive()) { addSubActions(); }`
480       if (hasRunCurrentFileItem(project)) {
481         addSubActions();
482       }
483     }
484
485     private void updateIcon(final Presentation presentation) {
486       setConfigurationIcon(presentation, myConfiguration, myProject);
487     }
488
489     private void addSubActions() {
490       // Add actions similar to com.intellij.execution.actions.ChooseRunConfigurationPopup.ConfigurationActionsStep#buildActions
491       addExecutorActions(this,
492                          executor -> new ExecutorRegistryImpl.RunSpecifiedConfigExecutorAction(executor, myConfiguration, false),
493                          myExecutorFilter);
494       addSeparator();
495
496       Executor runExecutor = DefaultRunExecutor.getRunExecutorInstance();
497       addAction(new ExecutorRegistryImpl.RunSpecifiedConfigExecutorAction(runExecutor, myConfiguration, true));
498
499       if (myConfiguration.isTemporary()) {
500         String actionName = ExecutionBundle.message("choose.run.popup.save");
501         String description = ExecutionBundle.message("choose.run.popup.save.description");
502         addAction(new AnAction(actionName, description, AllIcons.Actions.MenuSaveall) {
503           @Override
504           public void actionPerformed(@NotNull AnActionEvent e) {
505             RunManager.getInstance(myProject).makeStable(myConfiguration);
506           }
507         });
508       }
509
510       String actionName = ExecutionBundle.message("choose.run.popup.delete");
511       String description = ExecutionBundle.message("choose.run.popup.delete.description");
512       addAction(new AnAction(actionName, description, AllIcons.Actions.Cancel) {
513         @Override
514         public void actionPerformed(@NotNull AnActionEvent e) {
515           ChooseRunConfigurationPopup.deleteConfiguration(myProject, myConfiguration, null);
516         }
517       });
518     }
519
520     @Override
521     public void actionPerformed(@NotNull final AnActionEvent e) {
522       RunManager.getInstance(myProject).setSelectedConfiguration(myConfiguration);
523       updatePresentation(ExecutionTargetManager.getActiveTarget(myProject), myConfiguration, myProject, e.getPresentation(), e.getPlace());
524     }
525
526     @Override
527     public void update(@NotNull final AnActionEvent e) {
528       super.update(e);
529       updateIcon(e.getPresentation());
530     }
531   }
532 }