cleanup
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / actionSystem / impl / ActionManagerImpl.java
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.actionSystem.impl;
3
4 import com.intellij.AbstractBundle;
5 import com.intellij.BundleBase;
6 import com.intellij.DynamicBundle;
7 import com.intellij.diagnostic.LoadingState;
8 import com.intellij.diagnostic.PluginException;
9 import com.intellij.diagnostic.StartUpMeasurer;
10 import com.intellij.icons.AllIcons;
11 import com.intellij.ide.ActivityTracker;
12 import com.intellij.ide.DataManager;
13 import com.intellij.ide.plugins.IdeaPluginDescriptor;
14 import com.intellij.ide.plugins.IdeaPluginDescriptorImpl;
15 import com.intellij.ide.plugins.PluginManagerCore;
16 import com.intellij.ide.ui.customization.ActionUrl;
17 import com.intellij.ide.ui.customization.CustomActionsSchema;
18 import com.intellij.idea.IdeaLogger;
19 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionIdProvider;
20 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionsCollectorImpl;
21 import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionsEventLogGroup;
22 import com.intellij.internal.statistic.eventLog.events.EventFields;
23 import com.intellij.internal.statistic.eventLog.events.EventPair;
24 import com.intellij.internal.statistic.eventLog.events.ObjectEventData;
25 import com.intellij.lang.Language;
26 import com.intellij.openapi.Disposable;
27 import com.intellij.openapi.actionSystem.*;
28 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
29 import com.intellij.openapi.actionSystem.ex.ActionPopupMenuListener;
30 import com.intellij.openapi.actionSystem.ex.ActionUtil;
31 import com.intellij.openapi.actionSystem.ex.AnActionListener;
32 import com.intellij.openapi.application.*;
33 import com.intellij.openapi.application.impl.LaterInvocator;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.Editor;
36 import com.intellij.openapi.editor.actionSystem.EditorAction;
37 import com.intellij.openapi.editor.actionSystem.EditorActionHandlerBean;
38 import com.intellij.openapi.extensions.ExtensionPointListener;
39 import com.intellij.openapi.extensions.ExtensionPointName;
40 import com.intellij.openapi.extensions.PluginDescriptor;
41 import com.intellij.openapi.extensions.PluginId;
42 import com.intellij.openapi.keymap.Keymap;
43 import com.intellij.openapi.keymap.KeymapManager;
44 import com.intellij.openapi.keymap.KeymapUtil;
45 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
46 import com.intellij.openapi.keymap.impl.DefaultKeymap;
47 import com.intellij.openapi.progress.ProcessCanceledException;
48 import com.intellij.openapi.progress.ProgressIndicator;
49 import com.intellij.openapi.project.Project;
50 import com.intellij.openapi.project.ProjectType;
51 import com.intellij.openapi.util.ActionCallback;
52 import com.intellij.openapi.util.Disposer;
53 import com.intellij.openapi.util.IconLoader;
54 import com.intellij.openapi.util.NlsActions;
55 import com.intellij.openapi.util.registry.Registry;
56 import com.intellij.openapi.util.text.StringUtil;
57 import com.intellij.openapi.wm.IdeFocusManager;
58 import com.intellij.openapi.wm.IdeFrame;
59 import com.intellij.psi.PsiDocumentManager;
60 import com.intellij.psi.PsiFile;
61 import com.intellij.util.ArrayUtilRt;
62 import com.intellij.util.ReflectionUtil;
63 import com.intellij.util.containers.CollectionFactory;
64 import com.intellij.util.containers.ContainerUtil;
65 import com.intellij.util.containers.MultiMap;
66 import com.intellij.util.messages.MessageBusConnection;
67 import com.intellij.util.ui.UIUtil;
68 import it.unimi.dsi.fastutil.objects.Object2IntMap;
69 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
70 import org.jdom.Element;
71 import org.jetbrains.annotations.ApiStatus;
72 import org.jetbrains.annotations.NotNull;
73 import org.jetbrains.annotations.Nullable;
74
75 import javax.swing.Timer;
76 import javax.swing.*;
77 import java.awt.*;
78 import java.awt.event.ActionEvent;
79 import java.awt.event.ActionListener;
80 import java.awt.event.InputEvent;
81 import java.awt.event.WindowEvent;
82 import java.util.List;
83 import java.util.*;
84 import java.util.function.Supplier;
85
86 public final class ActionManagerImpl extends ActionManagerEx implements Disposable {
87   private static final ExtensionPointName<ActionConfigurationCustomizer> EP =
88     new ExtensionPointName<>("com.intellij.actionConfigurationCustomizer");
89   private static final ExtensionPointName<EditorActionHandlerBean> EDITOR_ACTION_HANDLER_EP =
90     new ExtensionPointName<>("com.intellij.editorActionHandler");
91
92   private static final String ACTION_ELEMENT_NAME = "action";
93   private static final String GROUP_ELEMENT_NAME = "group";
94   private static final String CLASS_ATTR_NAME = "class";
95   private static final String ID_ATTR_NAME = "id";
96   private static final String INTERNAL_ATTR_NAME = "internal";
97   private static final String ICON_ATTR_NAME = "icon";
98   private static final String ADD_TO_GROUP_ELEMENT_NAME = "add-to-group";
99   private static final String SHORTCUT_ELEMENT_NAME = "keyboard-shortcut";
100   private static final String MOUSE_SHORTCUT_ELEMENT_NAME = "mouse-shortcut";
101   private static final String DESCRIPTION = "description";
102   private static final String TEXT_ATTR_NAME = "text";
103   private static final String KEY_ATTR_NAME = "key";
104   private static final String POPUP_ATTR_NAME = "popup";
105   private static final String COMPACT_ATTR_NAME = "compact";
106   private static final String SEARCHABLE_ATTR_NAME = "searchable";
107   private static final String SEPARATOR_ELEMENT_NAME = "separator";
108   private static final String REFERENCE_ELEMENT_NAME = "reference";
109   private static final String ABBREVIATION_ELEMENT_NAME = "abbreviation";
110   private static final String GROUPID_ATTR_NAME = "group-id";
111   private static final String ANCHOR_ELEMENT_NAME = "anchor";
112   private static final String FIRST = "first";
113   private static final String LAST = "last";
114   private static final String BEFORE = "before";
115   private static final String AFTER = "after";
116   private static final String SECONDARY = "secondary";
117   private static final String RELATIVE_TO_ACTION_ATTR_NAME = "relative-to-action";
118   private static final String FIRST_KEYSTROKE_ATTR_NAME = "first-keystroke";
119   private static final String SECOND_KEYSTROKE_ATTR_NAME = "second-keystroke";
120   private static final String REMOVE_SHORTCUT_ATTR_NAME = "remove";
121   private static final String REPLACE_SHORTCUT_ATTR_NAME = "replace-all";
122   private static final String KEYMAP_ATTR_NAME = "keymap";
123   private static final String KEYSTROKE_ATTR_NAME = "keystroke";
124   private static final String REF_ATTR_NAME = "ref";
125   private static final String VALUE_ATTR_NAME = "value";
126   private static final String ACTIONS_BUNDLE = "messages.ActionsBundle";
127   private static final String USE_SHORTCUT_OF_ATTR_NAME = "use-shortcut-of";
128   private static final String OVERRIDES_ATTR_NAME = "overrides";
129   private static final String KEEP_CONTENT_ATTR_NAME = "keep-content";
130   private static final String PROJECT_TYPE = "project-type";
131   private static final String UNREGISTER_ELEMENT_NAME = "unregister";
132   private static final String OVERRIDE_TEXT_ELEMENT_NAME = "override-text";
133   private static final String SYNONYM_ELEMENT_NAME = "synonym";
134   private static final String PLACE_ATTR_NAME = "place";
135   private static final String USE_TEXT_OF_PLACE_ATTR_NAME = "use-text-of-place";
136   private static final String RESOURCE_BUNDLE_ATTR_NAME = "resource-bundle";
137
138   private static final Logger LOG = Logger.getInstance(ActionManagerImpl.class);
139   private static final int DEACTIVATED_TIMER_DELAY = 5000;
140   private static final int TIMER_DELAY = 500;
141   private static final int UPDATE_DELAY_AFTER_TYPING = 500;
142
143   private final Object myLock = new Object();
144   private final Map<String, AnAction> myId2Action = CollectionFactory.createSmallMemoryFootprintMap();
145   private final MultiMap<PluginId, String> myPlugin2Id = new MultiMap<>();
146   private final Object2IntMap<String> myId2Index = new Object2IntOpenHashMap<>();
147   private final Map<Object, String> myAction2Id = CollectionFactory.createSmallMemoryFootprintMap();
148   private final MultiMap<String, String> myId2GroupId = new MultiMap<>();
149   private final List<String> myNotRegisteredInternalActionIds = new ArrayList<>();
150   private final List<AnActionListener> myActionListeners = ContainerUtil.createLockFreeCopyOnWriteList();
151   private final List<ActionPopupMenuListener> myActionPopupMenuListeners = ContainerUtil.createLockFreeCopyOnWriteList();
152   private final List<Object/*ActionPopupMenuImpl|JBPopup*/> myPopups = new ArrayList<>();
153   private MyTimer myTimer;
154   private int myRegisteredActionsCount;
155   private String myLastPreformedActionId;
156   private String myPrevPerformedActionId;
157   private long myLastTimeEditorWasTypedIn;
158   private boolean myTransparentOnlyUpdate;
159   private final Map<OverridingAction, AnAction> myBaseActions = new HashMap<>();
160   private int myAnonymousGroupIdCounter;
161
162   ActionManagerImpl() {
163     Application app = ApplicationManager.getApplication();
164     if (!app.isUnitTestMode()) {
165       LoadingState.COMPONENTS_LOADED.checkOccurred();
166       if (!app.isHeadlessEnvironment() && !app.isCommandLine()) {
167         LOG.assertTrue(!app.isDispatchThread());
168       }
169     }
170
171     for (IdeaPluginDescriptorImpl plugin : PluginManagerCore.getLoadedPlugins(null)) {
172       registerPluginActions(plugin, plugin.getActionDescriptionElements(), true);
173     }
174
175     EP.forEachExtensionSafe(customizer -> customizer.customize(this));
176     DynamicActionConfigurationCustomizer.EP_NAME.forEachExtensionSafe(customizer -> customizer.registerActions(this));
177     DynamicActionConfigurationCustomizer.EP_NAME.addExtensionPointListener(new ExtensionPointListener<>() {
178       @Override
179       public void extensionAdded(@NotNull DynamicActionConfigurationCustomizer extension, @NotNull PluginDescriptor pluginDescriptor) {
180         extension.registerActions(ActionManagerImpl.this);
181       }
182
183       @Override
184       public void extensionRemoved(@NotNull DynamicActionConfigurationCustomizer extension, @NotNull PluginDescriptor pluginDescriptor) {
185         extension.unregisterActions(ActionManagerImpl.this);
186       }
187     }, this);
188     EDITOR_ACTION_HANDLER_EP.addChangeListener(this::updateAllHandlers, this);
189   }
190
191   @NotNull
192   private static AnActionListener publisher() {
193     return ApplicationManager.getApplication().getMessageBus().syncPublisher(AnActionListener.TOPIC);
194   }
195
196   @Nullable
197   static AnAction convertStub(@NotNull ActionStub stub) {
198     AnAction anAction = instantiate(stub.getClassName(), stub.getPlugin(), AnAction.class);
199     if (anAction == null) {
200       return null;
201     }
202
203     stub.initAction(anAction);
204     updateIconFromStub(stub, anAction);
205
206     return anAction;
207   }
208
209   @Nullable
210   private static <T> T instantiate(@NotNull String stubClassName, @NotNull PluginDescriptor pluginDescriptor, Class<T> expectedClass) {
211     Object obj;
212     try {
213       if (expectedClass == ActionGroup.class) {
214         obj = ApplicationManager.getApplication().instantiateExtensionWithPicoContainerOnlyIfNeeded(stubClassName, pluginDescriptor);
215       }
216       else {
217         obj = ReflectionUtil.newInstance(Class.forName(stubClassName, true, pluginDescriptor.getPluginClassLoader()), false);
218       }
219     }
220     catch (ProcessCanceledException e) {
221       throw e;
222     }
223     catch (PluginException e) {
224       LOG.error(e);
225       return null;
226     }
227     catch (Throwable e) {
228       LOG.error(new PluginException(e, pluginDescriptor.getPluginId()));
229       return null;
230     }
231
232     if (!expectedClass.isInstance(obj)) {
233       LOG.error(new PluginException("class with name '" +
234                                     stubClassName + "' must be an instance of '" + expectedClass.getName() + "'; got " + obj, pluginDescriptor.getPluginId()));
235       return null;
236     }
237     //noinspection unchecked
238     return (T)obj;
239   }
240
241   private static void updateIconFromStub(@NotNull ActionStubBase stub, AnAction anAction) {
242     String iconPath = stub.getIconPath();
243     if (iconPath == null) {
244       return;
245     }
246
247     setIconFromClass(anAction.getClass(), stub.getPlugin(), iconPath, anAction.getTemplatePresentation());
248   }
249
250   @Nullable
251   private static ActionGroup convertGroupStub(@NotNull ActionGroupStub stub, @NotNull ActionManager actionManager) {
252     IdeaPluginDescriptor plugin = stub.getPlugin();
253     ActionGroup group = instantiate(stub.getActionClass(), plugin, ActionGroup.class);
254     if (group == null) {
255       return null;
256     }
257
258     stub.initGroup(group, actionManager);
259     updateIconFromStub(stub, group);
260     return group;
261   }
262
263   private static void processAbbreviationNode(@NotNull Element e, @NotNull String id) {
264     final String abbr = e.getAttributeValue(VALUE_ATTR_NAME);
265     if (!StringUtil.isEmpty(abbr)) {
266       final AbbreviationManagerImpl abbreviationManager = (AbbreviationManagerImpl)AbbreviationManager.getInstance();
267       abbreviationManager.register(abbr, id, true);
268     }
269   }
270
271   @Nullable
272   private static ResourceBundle getActionsResourceBundle(@NotNull IdeaPluginDescriptor plugin, @Nullable String bundleName) {
273     String resBundleName = bundleName != null ? bundleName :
274                            plugin.getPluginId() != PluginManagerCore.CORE_ID ? plugin.getResourceBundleBaseName() :
275                            ACTIONS_BUNDLE;
276     return resBundleName == null ? null : DynamicBundle.INSTANCE.getResourceBundle(resBundleName, plugin.getPluginClassLoader());
277   }
278
279   private static boolean isSecondary(Element element) {
280     return "true".equalsIgnoreCase(element.getAttributeValue(SECONDARY));
281   }
282
283   private static void setIcon(@Nullable String iconPath,
284                               @NotNull String className,
285                               @NotNull PluginDescriptor pluginDescriptor,
286                               @NotNull Presentation presentation) {
287     if (iconPath == null) {
288       return;
289     }
290
291     try {
292       Class<?> actionClass = Class.forName(className, true, pluginDescriptor.getPluginClassLoader());
293       setIconFromClass(actionClass, pluginDescriptor, iconPath, presentation);
294     }
295     catch (ClassNotFoundException | NoClassDefFoundError e) {
296       LOG.error(e);
297       reportActionError(pluginDescriptor.getPluginId(), "class with name \"" + className + "\" not found");
298     }
299   }
300
301   private static void setIconFromClass(@NotNull Class<?> actionClass,
302                                        @NotNull PluginDescriptor pluginDescriptor,
303                                        @NotNull String iconPath,
304                                        @NotNull Presentation presentation) {
305     //noinspection deprecation
306     presentation.setIcon(new IconLoader.LazyIcon() {
307       @NotNull
308       @Override
309       protected Icon compute() {
310         // try to find icon in idea class path
311         Icon icon = IconLoader.findIcon(iconPath, actionClass, true, false);
312         if (icon == null) {
313           icon = IconLoader.findIcon(iconPath, pluginDescriptor.getPluginClassLoader());
314         }
315
316         if (icon == null) {
317           reportActionError(pluginDescriptor.getPluginId(), "Icon cannot be found in '" + iconPath + "', action '" + actionClass + "'");
318           return AllIcons.Nodes.Unknown;
319         }
320
321         return icon;
322       }
323
324       @Override
325       public String toString() {
326         return "LazyIcon@ActionManagerImpl (path: " + iconPath + ", action class: " + actionClass + ")";
327       }
328     });
329   }
330
331   @SuppressWarnings("HardCodedStringLiteral")
332   private static @NlsActions.ActionDescription String computeDescription(ResourceBundle bundle, String id, String elementType, String descriptionValue) {
333     if (bundle != null) {
334       final String key = elementType + "." + id + ".description";
335       return AbstractBundle.messageOrDefault(bundle, key, StringUtil.notNullize(descriptionValue));
336     }
337     else {
338       return descriptionValue;
339     }
340   }
341
342   @SuppressWarnings("HardCodedStringLiteral")
343   private static @NlsActions.ActionText String computeActionText(ResourceBundle bundle, String id, String elementType, String textValue) {
344     return AbstractBundle.messageOrDefault(bundle, elementType + "." + id + "." + TEXT_ATTR_NAME, StringUtil.notNullize(textValue));
345   }
346
347   private static boolean checkRelativeToAction(String relativeToActionId,
348                                                @NotNull Anchor anchor,
349                                                @NotNull String actionName,
350                                                @Nullable PluginId pluginId) {
351     if ((Anchor.BEFORE == anchor || Anchor.AFTER == anchor) && relativeToActionId == null) {
352       reportActionError(pluginId, actionName + ": \"relative-to-action\" cannot be null if anchor is \"after\" or \"before\"");
353       return false;
354     }
355     return true;
356   }
357
358   @Nullable
359   private static Anchor parseAnchor(String anchorStr, @Nullable String actionName, @Nullable PluginId pluginId) {
360     if (anchorStr == null) {
361       return Anchor.LAST;
362     }
363
364     if (FIRST.equalsIgnoreCase(anchorStr)) {
365       return Anchor.FIRST;
366     }
367     else if (LAST.equalsIgnoreCase(anchorStr)) {
368       return Anchor.LAST;
369     }
370     else if (BEFORE.equalsIgnoreCase(anchorStr)) {
371       return Anchor.BEFORE;
372     }
373     else if (AFTER.equalsIgnoreCase(anchorStr)) {
374       return Anchor.AFTER;
375     }
376     else {
377       reportActionError(pluginId, actionName + ": anchor should be one of the following constants: \"first\", \"last\", \"before\" or \"after\"");
378       return null;
379     }
380   }
381
382   private static void processMouseShortcutNode(Element element, String actionId, PluginId pluginId, @NotNull KeymapManager keymapManager) {
383     String keystrokeString = element.getAttributeValue(KEYSTROKE_ATTR_NAME);
384     if (keystrokeString == null || keystrokeString.trim().isEmpty()) {
385       reportActionError(pluginId, "\"keystroke\" attribute must be specified for action with id=" + actionId);
386       return;
387     }
388     MouseShortcut shortcut;
389     try {
390       shortcut = KeymapUtil.parseMouseShortcut(keystrokeString);
391     }
392     catch (Exception ex) {
393       reportActionError(pluginId, "\"keystroke\" attribute has invalid value for action with id=" + actionId);
394       return;
395     }
396
397     String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
398     if (keymapName == null || keymapName.isEmpty()) {
399       reportActionError(pluginId, "attribute \"keymap\" should be defined");
400       return;
401     }
402     Keymap keymap = keymapManager.getKeymap(keymapName);
403     if (keymap == null) {
404       reportKeymapNotFoundWarning(pluginId, keymapName);
405       return;
406     }
407     processRemoveAndReplace(element, actionId, keymap, shortcut);
408   }
409
410   private static void reportActionError(@Nullable PluginId pluginId, @NotNull String message) {
411     reportActionError(pluginId, message, null);
412   }
413
414   private static void reportActionError(@Nullable PluginId pluginId, @NotNull String message, @Nullable Throwable cause) {
415     if (pluginId != null) {
416       LOG.error(new PluginException(message, cause, pluginId));
417     }
418     else if (cause != null) {
419       LOG.error(message, cause);
420     }
421     else {
422       LOG.error(message);
423     }
424   }
425
426   private static void reportKeymapNotFoundWarning(@Nullable PluginId pluginId, @NotNull String keymapName) {
427     if (DefaultKeymap.isBundledKeymapHidden(keymapName)) return;
428     String message = "keymap \"" + keymapName + "\" not found";
429     LOG.warn(pluginId == null ? message : new PluginException(message, null, pluginId).getMessage());
430   }
431
432   private static String getPluginInfo(@Nullable PluginId id) {
433     if (id != null) {
434       IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(id);
435       if (plugin != null) {
436         String name = plugin.getName();
437         if (name == null) {
438           name = id.getIdString();
439         }
440         return " Plugin: " + name;
441       }
442     }
443     return "";
444   }
445
446   @NotNull
447   private static DataContext getContextBy(Component contextComponent) {
448     final DataManager dataManager = DataManager.getInstance();
449     return contextComponent != null ? dataManager.getDataContext(contextComponent) : dataManager.getDataContext();
450   }
451
452   @Override
453   public void dispose() {
454     if (myTimer != null) {
455       myTimer.stop();
456       myTimer = null;
457     }
458   }
459
460   @Override
461   public void addTimerListener(int delay, @NotNull final TimerListener listener) {
462     _addTimerListener(listener, false);
463   }
464
465   @Override
466   public void removeTimerListener(@NotNull TimerListener listener) {
467     _removeTimerListener(listener, false);
468   }
469
470   @Override
471   public void addTransparentTimerListener(int delay, @NotNull TimerListener listener) {
472     _addTimerListener(listener, true);
473   }
474
475   @Override
476   public void removeTransparentTimerListener(@NotNull TimerListener listener) {
477     _removeTimerListener(listener, true);
478   }
479
480   private void _addTimerListener(final TimerListener listener, boolean transparent) {
481     if (ApplicationManager.getApplication().isUnitTestMode()) return;
482     if (myTimer == null) {
483       myTimer = new MyTimer();
484       myTimer.start();
485     }
486
487     myTimer.addTimerListener(listener, transparent);
488   }
489
490   private void _removeTimerListener(TimerListener listener, boolean transparent) {
491     if (ApplicationManager.getApplication().isUnitTestMode()) return;
492     if (LOG.assertTrue(myTimer != null)) {
493       myTimer.removeTimerListener(listener, transparent);
494     }
495   }
496
497   @NotNull
498   public ActionPopupMenu createActionPopupMenu(@NotNull String place, @NotNull ActionGroup group, @Nullable PresentationFactory presentationFactory) {
499     return new ActionPopupMenuImpl(place, group, this, presentationFactory);
500   }
501
502   @NotNull
503   @Override
504   public ActionPopupMenu createActionPopupMenu(@NotNull String place, @NotNull ActionGroup group) {
505     return new ActionPopupMenuImpl(place, group, this, null);
506   }
507
508   @NotNull
509   @Override
510   public ActionToolbar createActionToolbar(@NotNull final String place, @NotNull final ActionGroup group, final boolean horizontal) {
511     return createActionToolbar(place, group, horizontal, false);
512   }
513
514   @NotNull
515   @Override
516   public ActionToolbar createActionToolbar(@NotNull final String place, @NotNull final ActionGroup group, final boolean horizontal, final boolean decorateButtons) {
517     return new ActionToolbarImpl(place, group, horizontal, decorateButtons);
518   }
519
520   public void registerPluginActions(@NotNull IdeaPluginDescriptorImpl plugin, @Nullable List<Element> actionDescriptionElements, boolean initialStartup) {
521     if (actionDescriptionElements == null) {
522       return;
523     }
524
525     long startTime = StartUpMeasurer.getCurrentTime();
526     for (Element e : actionDescriptionElements) {
527       Element parent = e.getParentElement();
528       String bundleName = parent == null ? null : parent.getAttributeValue(RESOURCE_BUNDLE_ATTR_NAME);
529       processActionsChildElement(e, plugin, getActionsResourceBundle(plugin, bundleName));
530     }
531     StartUpMeasurer.addPluginCost(plugin.getPluginId().getIdString(), "Actions", StartUpMeasurer.getCurrentTime() - startTime);
532   }
533
534   @Override
535   @Nullable
536   public AnAction getAction(@NotNull String id) {
537     return getActionImpl(id, false);
538   }
539
540   @Nullable
541   private AnAction getActionImpl(@NotNull String id, boolean canReturnStub) {
542     AnAction action;
543     synchronized (myLock) {
544       action = myId2Action.get(id);
545       if (canReturnStub || !(action instanceof ActionStubBase)) {
546         return action;
547       }
548     }
549     AnAction converted = action instanceof ActionStub ? convertStub((ActionStub)action) : convertGroupStub((ActionGroupStub)action, this);
550     if (converted == null) {
551       unregisterAction(id);
552       return null;
553     }
554
555     synchronized (myLock) {
556       action = myId2Action.get(id);
557       if (action instanceof ActionStubBase) {
558         action = replaceStub((ActionStubBase)action, converted);
559       }
560       return action;
561     }
562   }
563
564   @NotNull
565   private AnAction replaceStub(@NotNull ActionStubBase stub, AnAction anAction) {
566     LOG.assertTrue(myAction2Id.containsKey(stub));
567     myAction2Id.remove(stub);
568
569     LOG.assertTrue(myId2Action.containsKey(stub.getId()));
570
571     AnAction action = myId2Action.remove(stub.getId());
572     LOG.assertTrue(action != null);
573     LOG.assertTrue(action.equals(stub));
574
575     myAction2Id.put(anAction, stub.getId());
576     updateHandlers(anAction);
577
578     return addToMap(stub.getId(), anAction, stub.getPlugin().getPluginId(), stub instanceof ActionStub ? ((ActionStub)stub).getProjectType() : null);
579   }
580
581   @Override
582   public String getId(@NotNull AnAction action) {
583     if (action instanceof ActionStubBase) {
584       return ((ActionStubBase)action).getId();
585     }
586     synchronized (myLock) {
587       return myAction2Id.get(action);
588     }
589   }
590
591   @Override
592   public @NotNull List<String> getActionIdList(@NotNull String idPrefix) {
593     List<String> result = new ArrayList<>();
594     synchronized (myLock) {
595       for (String id : myId2Action.keySet()) {
596         if (id.startsWith(idPrefix)) {
597           result.add(id);
598         }
599       }
600     }
601     return result;
602   }
603
604   @Override
605   public String @NotNull [] getActionIds(@NotNull String idPrefix) {
606     return ArrayUtilRt.toStringArray(getActionIdList(idPrefix));
607   }
608
609   @Override
610   public boolean isGroup(@NotNull String actionId) {
611     return getActionImpl(actionId, true) instanceof ActionGroup;
612   }
613
614   @NotNull
615   @Override
616   public JComponent createButtonToolbar(@NotNull final String actionPlace, @NotNull final ActionGroup messageActionGroup) {
617     return new ButtonToolbarImpl(actionPlace, messageActionGroup);
618   }
619
620   @Override
621   public AnAction getActionOrStub(@NotNull String id) {
622     return getActionImpl(id, true);
623   }
624
625   /**
626    * @return instance of ActionGroup or ActionStub. The method never returns real subclasses of {@code AnAction}.
627    */
628   @Nullable
629   private AnAction processActionElement(@NotNull Element element,
630                                         @NotNull IdeaPluginDescriptorImpl plugin,
631                                         @Nullable ResourceBundle bundle) {
632     String className = element.getAttributeValue(CLASS_ATTR_NAME);
633     if (className == null || className.isEmpty()) {
634       reportActionError(plugin.getPluginId(), "action element should have specified \"class\" attribute");
635       return null;
636     }
637
638     // read ID and register loaded action
639     String id = obtainActionId(element, className);
640     if (Boolean.parseBoolean(element.getAttributeValue(INTERNAL_ATTR_NAME)) &&
641         !ApplicationManager.getApplication().isInternal()) {
642       myNotRegisteredInternalActionIds.add(id);
643       return null;
644     }
645
646     String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
647     String projectType = element.getAttributeValue(PROJECT_TYPE);
648
649     String textValue = element.getAttributeValue(TEXT_ATTR_NAME);
650     String descriptionValue = element.getAttributeValue(DESCRIPTION);
651
652     ActionStub stub = new ActionStub(className, id, plugin, iconPath, projectType, () -> {
653       Supplier<String> text = () -> computeActionText(bundle, id, ACTION_ELEMENT_NAME, textValue);
654       if (text.get() == null) {
655         reportActionError(plugin.getPluginId(), "'text' attribute is mandatory (actionId=" + id +
656                                                 ", plugin=" + plugin + ")");
657       }
658       Presentation presentation = new Presentation();
659       presentation.setText(text);
660       presentation.setDescription(() -> computeDescription(bundle, id, ACTION_ELEMENT_NAME, descriptionValue));
661       return presentation;
662     });
663
664     KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
665     // process all links and key bindings if any
666     for (Element e : element.getChildren()) {
667       if (ADD_TO_GROUP_ELEMENT_NAME.equals(e.getName())) {
668         processAddToGroupNode(stub, e, plugin.getPluginId(), isSecondary(e));
669       }
670       else if (SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
671         processKeyboardShortcutNode(e, id, plugin.getPluginId(), keymapManager);
672       }
673       else if (MOUSE_SHORTCUT_ELEMENT_NAME.equals(e.getName())) {
674         processMouseShortcutNode(e, id, plugin.getPluginId(), keymapManager);
675       }
676       else if (ABBREVIATION_ELEMENT_NAME.equals(e.getName())) {
677         processAbbreviationNode(e, id);
678       }
679       else if (OVERRIDE_TEXT_ELEMENT_NAME.equals(e.getName())) {
680         processOverrideTextNode(stub, stub.getId(), e, plugin.getPluginId(), bundle);
681       }
682       else if (SYNONYM_ELEMENT_NAME.equals(e.getName())) {
683         processSynonymNode(stub, e, plugin.getPluginId(), bundle);
684       }
685       else {
686         reportActionError(plugin.getPluginId(), "unexpected name of element \"" + e.getName() + "\"");
687         return null;
688       }
689     }
690     String shortcutOfActionId = element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME);
691     if (shortcutOfActionId != null) {
692       keymapManager.bindShortcuts(shortcutOfActionId, id);
693     }
694
695     registerOrReplaceActionInner(element, id, stub, plugin);
696     return stub;
697   }
698
699   private static String obtainActionId(Element element, String className) {
700     String id = element.getAttributeValue(ID_ATTR_NAME);
701     return StringUtil.isEmpty(id) ? StringUtil.getShortName(className) : id;
702   }
703
704   private void registerOrReplaceActionInner(@NotNull Element element,
705                                             @NotNull String id,
706                                             @NotNull AnAction action,
707                                             @NotNull IdeaPluginDescriptor plugin) {
708     synchronized (myLock) {
709       if (Boolean.parseBoolean(element.getAttributeValue(OVERRIDES_ATTR_NAME))) {
710         if (getActionOrStub(id) == null) {
711           LOG.error(element.getName() + " '" + id + "' doesn't override anything");
712           return;
713         }
714         AnAction prev = replaceAction(id, action, plugin.getPluginId());
715         if (action instanceof DefaultActionGroup && prev instanceof DefaultActionGroup) {
716           if (Boolean.parseBoolean(element.getAttributeValue(KEEP_CONTENT_ATTR_NAME))) {
717             ((DefaultActionGroup)action).copyFromGroup((DefaultActionGroup)prev);
718           }
719         }
720       }
721       else {
722         registerAction(id, action, plugin.getPluginId(), element.getAttributeValue(PROJECT_TYPE));
723       }
724       ActionsCollectorImpl.onActionLoadedFromXml(action, id, plugin);
725     }
726   }
727
728   private AnAction processGroupElement(@NotNull Element element,
729                                        @NotNull IdeaPluginDescriptorImpl plugin,
730                                        @Nullable ResourceBundle bundle) {
731     if (!GROUP_ELEMENT_NAME.equals(element.getName())) {
732       reportActionError(plugin.getPluginId(), "unexpected name of element \"" + element.getName() + "\"");
733       return null;
734     }
735     String className = element.getAttributeValue(CLASS_ATTR_NAME);
736     if (className == null) { // use default group if class isn't specified
737       className = "true".equals(element.getAttributeValue(COMPACT_ATTR_NAME))
738                   ? DefaultCompactActionGroup.class.getName()
739                   : DefaultActionGroup.class.getName();
740     }
741     try {
742       String id = element.getAttributeValue(ID_ATTR_NAME);
743       if (id != null && id.isEmpty()) {
744         reportActionError(plugin.getPluginId(), "ID of the group cannot be an empty string");
745         return null;
746       }
747
748       ActionGroup group;
749       boolean customClass = false;
750       if (DefaultActionGroup.class.getName().equals(className)) {
751         group = new DefaultActionGroup();
752       }
753       else if (DefaultCompactActionGroup.class.getName().equals(className)) {
754         group = new DefaultCompactActionGroup();
755       }
756       else if (id == null) {
757         Object obj = ApplicationManager.getApplication().instantiateExtensionWithPicoContainerOnlyIfNeeded(className, plugin);
758         if (!(obj instanceof ActionGroup)) {
759           reportActionError(plugin.getPluginId(), "class with name \"" + className + "\" should be instance of " + ActionGroup.class.getName());
760           return null;
761         }
762         if (element.getChildren().size() != element.getChildren(ADD_TO_GROUP_ELEMENT_NAME).size() ) {  //
763           if (!(obj instanceof DefaultActionGroup)) {
764             reportActionError(plugin.getPluginId(), "class with name \"" + className + "\" should be instance of " + DefaultActionGroup.class.getName() +
765                                         " because there are children specified");
766             return null;
767           }
768         }
769         customClass = true;
770         group = (ActionGroup)obj;
771       }
772       else {
773         group = new ActionGroupStub(id, className, plugin);
774         customClass = true;
775       }
776       // read ID and register loaded group
777       if (Boolean.parseBoolean(element.getAttributeValue(INTERNAL_ATTR_NAME)) && !ApplicationManager.getApplication().isInternal()) {
778         myNotRegisteredInternalActionIds.add(id);
779         return null;
780       }
781
782       if (id == null) {
783         id = "<anonymous-group-" + myAnonymousGroupIdCounter++ + ">";
784       }
785
786       registerOrReplaceActionInner(element, id, group, plugin);
787       Presentation presentation = group.getTemplatePresentation();
788       String finalId = id;
789
790       // text
791       Supplier<String> text = () -> computeActionText(bundle, finalId, GROUP_ELEMENT_NAME, element.getAttributeValue(TEXT_ATTR_NAME));
792       // don't override value which was set in API with empty value from xml descriptor
793       if (!StringUtil.isEmpty(text.get()) || presentation.getText() == null) {
794         presentation.setText(text);
795       }
796
797       // description
798       Supplier<String> description = () -> computeDescription(bundle, finalId, GROUP_ELEMENT_NAME, element.getAttributeValue(DESCRIPTION));
799       // don't override value which was set in API with empty value from xml descriptor
800       if (!StringUtil.isEmpty(description.get()) || presentation.getDescription() == null) {
801         presentation.setDescription(description);
802       }
803
804       // icon
805       String iconPath = element.getAttributeValue(ICON_ATTR_NAME);
806       if (group instanceof ActionGroupStub) {
807         ((ActionGroupStub)group).setIconPath(iconPath);
808       }
809       else {
810         setIcon(iconPath, className, plugin, presentation);
811       }
812
813       // popup
814       String popup = element.getAttributeValue(POPUP_ATTR_NAME);
815       if (popup != null) {
816         group.setPopup(Boolean.parseBoolean(popup));
817         if (group instanceof ActionGroupStub) {
818           ((ActionGroupStub)group).setPopupDefinedInXml(true);
819         }
820       }
821
822       String searchable = element.getAttributeValue(SEARCHABLE_ATTR_NAME);
823       if (searchable != null) {
824         group.setSearchable(Boolean.parseBoolean(searchable));
825       }
826
827       String shortcutOfActionId = element.getAttributeValue(USE_SHORTCUT_OF_ATTR_NAME);
828       if (customClass && shortcutOfActionId != null) {
829         KeymapManagerEx.getInstanceEx().bindShortcuts(shortcutOfActionId, id);
830       }
831
832       // process all group's children. There are other groups, actions, references and links
833       for (Element child : element.getChildren()) {
834         String name = child.getName();
835         if (ACTION_ELEMENT_NAME.equals(name)) {
836           AnAction action = processActionElement(child, plugin, bundle);
837           if (action != null) {
838             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
839           }
840         }
841         else if (SEPARATOR_ELEMENT_NAME.equals(name)) {
842           processSeparatorNode((DefaultActionGroup)group, child, plugin.getPluginId(), bundle);
843         }
844         else if (GROUP_ELEMENT_NAME.equals(name)) {
845           AnAction action = processGroupElement(child, plugin, bundle);
846           if (action != null) {
847             addToGroupInner(group, action, Constraints.LAST, false);
848           }
849         }
850         else if (ADD_TO_GROUP_ELEMENT_NAME.equals(name)) {
851           processAddToGroupNode(group, child, plugin.getPluginId(), isSecondary(child));
852         }
853         else if (REFERENCE_ELEMENT_NAME.equals(name)) {
854           AnAction action = processReferenceElement(child, plugin.getPluginId());
855           if (action != null) {
856             addToGroupInner(group, action, Constraints.LAST, isSecondary(child));
857           }
858         }
859         else if (OVERRIDE_TEXT_ELEMENT_NAME.equals(name)) {
860           processOverrideTextNode(group, id, child, plugin.getPluginId(), bundle);
861         }
862         else {
863           reportActionError(plugin.getPluginId(), "unexpected name of element \"" + name + "\n");
864           return null;
865         }
866       }
867       return group;
868     }
869     catch (Exception e) {
870       String message = "cannot create class \"" + className + "\"";
871       reportActionError(plugin.getPluginId(), message, e);
872       return null;
873     }
874   }
875
876   private void processReferenceNode(@NotNull Element element, @Nullable PluginId pluginId, @Nullable ResourceBundle bundle) {
877     AnAction action = processReferenceElement(element, pluginId);
878     if (action == null) {
879       return;
880     }
881
882     for (Element child : element.getChildren()) {
883       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
884         processAddToGroupNode(action, child, pluginId, isSecondary(child));
885       }
886       else if (SYNONYM_ELEMENT_NAME.equals(child.getName())) {
887         processSynonymNode(action, child, pluginId, bundle);
888       }
889     }
890   }
891
892   /**
893    * @param element description of link
894    */
895   private void processAddToGroupNode(AnAction action, Element element, PluginId pluginId, boolean secondary) {
896     String name = action instanceof ActionStub ? ((ActionStub)action).getClassName() : action.getClass().getName();
897     String id = action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action);
898     String actionName = name + " (" + id + ")";
899
900     if (!ADD_TO_GROUP_ELEMENT_NAME.equals(element.getName())) {
901       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
902       return;
903     }
904
905     // parent group
906     final AnAction parentGroup = getParentGroup(element.getAttributeValue(GROUPID_ATTR_NAME), actionName, pluginId);
907     if (parentGroup == null) {
908       return;
909     }
910
911     // anchor attribute
912     final Anchor anchor = parseAnchor(element.getAttributeValue(ANCHOR_ELEMENT_NAME), actionName, pluginId);
913     if (anchor == null) {
914       return;
915     }
916
917     final String relativeToActionId = element.getAttributeValue(RELATIVE_TO_ACTION_ATTR_NAME);
918     if (!checkRelativeToAction(relativeToActionId, anchor, actionName, pluginId)) {
919       return;
920     }
921     addToGroupInner(parentGroup, action, new Constraints(anchor, relativeToActionId), secondary);
922   }
923
924   private void addToGroupInner(AnAction group, AnAction action, Constraints constraints, boolean secondary) {
925     String actionId = action instanceof ActionStub ? ((ActionStub)action).getId() : myAction2Id.get(action);
926     ((DefaultActionGroup)group).addAction(action, constraints, this).setAsSecondary(secondary);
927     myId2GroupId.putValue(actionId, myAction2Id.get(group));
928   }
929
930   @Nullable
931   public DefaultActionGroup getParentGroup(final String groupId,
932                                            @Nullable final String actionName,
933                                            @Nullable final PluginId pluginId) {
934     if (groupId == null || groupId.isEmpty()) {
935       reportActionError(pluginId, actionName + ": attribute \"group-id\" should be defined");
936       return null;
937     }
938     AnAction parentGroup = getActionImpl(groupId, true);
939     if (parentGroup == null) {
940       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" isn't registered; action will be added to the \"Other\" group", null);
941       parentGroup = getActionImpl(IdeActions.GROUP_OTHER_MENU, true);
942     }
943     if (!(parentGroup instanceof DefaultActionGroup)) {
944       reportActionError(pluginId, actionName + ": group with id \"" + groupId + "\" should be instance of " + DefaultActionGroup.class.getName() +
945                                   " but was " + (parentGroup != null ? parentGroup.getClass() : "[null]"));
946       return null;
947     }
948     return (DefaultActionGroup)parentGroup;
949   }
950
951   private static void processOverrideTextNode(AnAction action, String id, Element element, PluginId pluginId,
952                                               @Nullable ResourceBundle bundle) {
953     if (!OVERRIDE_TEXT_ELEMENT_NAME.equals(element.getName())) {
954       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
955       return;
956     }
957     String place = element.getAttributeValue(PLACE_ATTR_NAME);
958     if (place == null) {
959       reportActionError(pluginId, id + ": override-text specified without place");
960       return;
961     }
962     String useTextOfPlace = element.getAttributeValue(USE_TEXT_OF_PLACE_ATTR_NAME);
963     if (useTextOfPlace != null) {
964       action.copyActionTextOverride(useTextOfPlace, place, id);
965     }
966     else {
967       String text = element.getAttributeValue(TEXT_ATTR_NAME, "");
968       if (text.isEmpty() && bundle != null) {
969         String prefix = action instanceof ActionGroup ? "group" : "action";
970         String key = prefix + "." + id + "." + place + ".text";
971         action.addTextOverride(place, () -> BundleBase.message(bundle, key));
972       }
973       else {
974         action.addTextOverride(place, () -> text);
975       }
976     }
977   }
978
979   private static void processSynonymNode(AnAction action, Element element, PluginId pluginId, @Nullable ResourceBundle bundle) {
980     if (!SYNONYM_ELEMENT_NAME.equals(element.getName())) {
981       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
982       return;
983     }
984     String text = element.getAttributeValue(TEXT_ATTR_NAME, "");
985     if (!text.isEmpty()) {
986       action.addSynonym(() -> text);
987     }
988     else {
989       String key = element.getAttributeValue(KEY_ATTR_NAME);
990       if (key != null && bundle != null) {
991         action.addSynonym(() -> BundleBase.message(bundle, key));
992       }
993       else {
994         reportActionError(pluginId, "Can't process synonym: neither text nor resource bundle key is specified");
995       }
996     }
997   }
998
999   /**
1000    * @param parentGroup group which is the parent of the separator. It can be {@code null} in that
1001    *                    case separator will be added to group described in the <add-to-group ....> subelement.
1002    * @param element     XML element which represent separator.
1003    */
1004   private void processSeparatorNode(@Nullable DefaultActionGroup parentGroup, @NotNull Element element, PluginId pluginId, @Nullable ResourceBundle bundle) {
1005     if (!SEPARATOR_ELEMENT_NAME.equals(element.getName())) {
1006       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"");
1007       return;
1008     }
1009     @SuppressWarnings("HardCodedStringLiteral")
1010     String text = element.getAttributeValue(TEXT_ATTR_NAME);
1011     String key = element.getAttributeValue(KEY_ATTR_NAME);
1012     Separator separator =
1013       text != null ? new Separator(text) : key != null ? createSeparator(bundle, key) : Separator.getInstance();
1014     if (parentGroup != null) {
1015       parentGroup.add(separator, this);
1016     }
1017     // try to find inner <add-to-parent...> tag
1018     for (Element child : element.getChildren()) {
1019       if (ADD_TO_GROUP_ELEMENT_NAME.equals(child.getName())) {
1020         processAddToGroupNode(separator, child, pluginId, isSecondary(child));
1021       }
1022     }
1023   }
1024
1025   @NotNull
1026   private static Separator createSeparator(@Nullable ResourceBundle bundle, @NotNull String key) {
1027     String text = bundle != null ? AbstractBundle.messageOrNull(bundle, key) : null;
1028     return text != null ? new Separator(text) : Separator.getInstance();
1029   }
1030
1031   private void processUnregisterNode(Element element, PluginId pluginId) {
1032     String id = element.getAttributeValue(ID_ATTR_NAME);
1033     if (id == null) {
1034       reportActionError(pluginId, "'id' attribute is required for 'unregister' elements");
1035       return;
1036     }
1037     AnAction action = getAction(id);
1038     if (action == null) {
1039       reportActionError(pluginId, "Trying to unregister non-existing action " + id);
1040       return;
1041     }
1042
1043     AbbreviationManager.getInstance().removeAllAbbreviations(id);
1044     unregisterAction(id);
1045   }
1046
1047   private static void processKeyboardShortcutNode(Element element,
1048                                                   String actionId,
1049                                                   PluginId pluginId,
1050                                                   @NotNull KeymapManagerEx keymapManager) {
1051     String firstStrokeString = element.getAttributeValue(FIRST_KEYSTROKE_ATTR_NAME);
1052     if (firstStrokeString == null) {
1053       reportActionError(pluginId, "\"first-keystroke\" attribute must be specified for action with id=" + actionId);
1054       return;
1055     }
1056     KeyStroke firstKeyStroke = getKeyStroke(firstStrokeString);
1057     if (firstKeyStroke == null) {
1058       reportActionError(pluginId, "\"first-keystroke\" attribute has invalid value for action with id=" + actionId);
1059       return;
1060     }
1061
1062     KeyStroke secondKeyStroke = null;
1063     String secondStrokeString = element.getAttributeValue(SECOND_KEYSTROKE_ATTR_NAME);
1064     if (secondStrokeString != null) {
1065       secondKeyStroke = getKeyStroke(secondStrokeString);
1066       if (secondKeyStroke == null) {
1067         reportActionError(pluginId, "\"second-keystroke\" attribute has invalid value for action with id=" + actionId);
1068         return;
1069       }
1070     }
1071
1072     String keymapName = element.getAttributeValue(KEYMAP_ATTR_NAME);
1073     if (keymapName == null || keymapName.trim().isEmpty()) {
1074       reportActionError(pluginId, "attribute \"keymap\" should be defined");
1075       return;
1076     }
1077     Keymap keymap = keymapManager.getKeymap(keymapName);
1078     if (keymap == null) {
1079       reportKeymapNotFoundWarning(pluginId, keymapName);
1080       return;
1081     }
1082     final KeyboardShortcut shortcut = new KeyboardShortcut(firstKeyStroke, secondKeyStroke);
1083     processRemoveAndReplace(element, actionId, keymap, shortcut);
1084   }
1085
1086   private static void processRemoveAndReplace(@NotNull Element element, String actionId, @NotNull Keymap keymap, @NotNull Shortcut shortcut) {
1087     boolean remove = Boolean.parseBoolean(element.getAttributeValue(REMOVE_SHORTCUT_ATTR_NAME));
1088     boolean replace = Boolean.parseBoolean(element.getAttributeValue(REPLACE_SHORTCUT_ATTR_NAME));
1089     if (remove) {
1090       keymap.removeShortcut(actionId, shortcut);
1091     }
1092     if (replace) {
1093       keymap.removeAllActionShortcuts(actionId);
1094     }
1095     if (!remove) {
1096       keymap.addShortcut(actionId, shortcut);
1097     }
1098   }
1099
1100   private @Nullable AnAction processReferenceElement(Element element, PluginId pluginId) {
1101     if (!REFERENCE_ELEMENT_NAME.equals(element.getName())) {
1102       reportActionError(pluginId, "unexpected name of element \"" + element.getName() + "\"", null);
1103       return null;
1104     }
1105
1106     String ref = getReferenceActionId(element);
1107     if (ref == null || ref.isEmpty()) {
1108       reportActionError(pluginId, "ID of reference element should be defined", null);
1109       return null;
1110     }
1111
1112     AnAction action = getActionImpl(ref, true);
1113     if (action == null) {
1114       if (!myNotRegisteredInternalActionIds.contains(ref)) {
1115         reportActionError(pluginId, "action specified by reference isn't registered (ID=" + ref + ")", null);
1116       }
1117       return null;
1118     }
1119     return action;
1120   }
1121
1122   private static String getReferenceActionId(@NotNull Element element) {
1123     String ref = element.getAttributeValue(REF_ATTR_NAME);
1124     if (ref == null) {
1125       // support old style references by id
1126       ref = element.getAttributeValue(ID_ATTR_NAME);
1127     }
1128     return ref;
1129   }
1130
1131   private void processActionsChildElement(@NotNull Element child,
1132                                           @NotNull IdeaPluginDescriptorImpl plugin,
1133                                           @Nullable ResourceBundle bundle) {
1134     String name = child.getName();
1135     switch (name) {
1136       case ACTION_ELEMENT_NAME:
1137         processActionElement(child, plugin, bundle);
1138         break;
1139       case GROUP_ELEMENT_NAME:
1140         processGroupElement(child, plugin, bundle);
1141         break;
1142       case SEPARATOR_ELEMENT_NAME:
1143         processSeparatorNode(null, child, plugin.getPluginId(), bundle);
1144         break;
1145       case REFERENCE_ELEMENT_NAME:
1146         processReferenceNode(child, plugin.getPluginId(), bundle);
1147         break;
1148       case UNREGISTER_ELEMENT_NAME:
1149         processUnregisterNode(child, plugin.getPluginId());
1150         break;
1151       default:
1152         reportActionError(plugin.getPluginId(), "unexpected name of element \"" + name + "\n");
1153         break;
1154     }
1155   }
1156
1157   @ApiStatus.Internal
1158   public static @Nullable String checkUnloadActions(PluginId pluginId, @NotNull IdeaPluginDescriptorImpl pluginDescriptor) {
1159     List<Element> elements = pluginDescriptor.getActionDescriptionElements();
1160     if (elements == null) {
1161       return null;
1162     }
1163
1164     for (Element element : elements) {
1165       if (!element.getName().equals(ACTION_ELEMENT_NAME) &&
1166           !(element.getName().equals(GROUP_ELEMENT_NAME) && canUnloadGroup(element)) &&
1167           !element.getName().equals(REFERENCE_ELEMENT_NAME)) {
1168         return "Plugin " + pluginId + " is not unload-safe because of action element " + element.getName();
1169       }
1170     }
1171     return null;
1172   }
1173
1174   private static boolean canUnloadGroup(@NotNull Element element) {
1175     if (element.getAttributeValue(ID_ATTR_NAME) == null) {
1176       return false;
1177     }
1178     for (Element child : element.getChildren()) {
1179       if (child.getName().equals(GROUP_ELEMENT_NAME) && !canUnloadGroup(child)) return false;
1180     }
1181     return true;
1182   }
1183
1184   public void unloadActions(@NotNull IdeaPluginDescriptorImpl pluginDescriptor) {
1185     List<Element> elements = pluginDescriptor.getActionDescriptionElements();
1186     if (elements == null) {
1187       return;
1188     }
1189
1190     for (Element element : ContainerUtil.reverse(elements)) {
1191       switch (element.getName()) {
1192         case ACTION_ELEMENT_NAME:
1193           unloadActionElement(element);
1194           break;
1195         case GROUP_ELEMENT_NAME:
1196           unloadGroupElement(element);
1197           break;
1198         case REFERENCE_ELEMENT_NAME:
1199           PluginId pluginId = pluginDescriptor.getPluginId();
1200           AnAction action = processReferenceElement(element, pluginId);
1201           if (action == null) return;
1202           String actionId = getReferenceActionId(element);
1203
1204           for (Element child : element.getChildren(ADD_TO_GROUP_ELEMENT_NAME)) {
1205             String groupId = child.getAttributeValue(GROUPID_ATTR_NAME);
1206             final DefaultActionGroup parentGroup = getParentGroup(groupId, actionId, pluginId);
1207             if (parentGroup == null) return;
1208             parentGroup.remove(action);
1209             myId2GroupId.remove(actionId, groupId);
1210           }
1211           break;
1212       }
1213     }
1214   }
1215
1216   private void unloadGroupElement(Element element) {
1217     String id = element.getAttributeValue(ID_ATTR_NAME);
1218     if (id == null) {
1219       throw new IllegalStateException("Cannot unload groups with no ID");
1220     }
1221     for (Element groupChild : element.getChildren()) {
1222       if (groupChild.getName().equals(ACTION_ELEMENT_NAME)) {
1223         unloadActionElement(groupChild);
1224       }
1225       else if (groupChild.getName().equals(GROUP_ELEMENT_NAME)) {
1226         unloadGroupElement(groupChild);
1227       }
1228     }
1229     unregisterAction(id);
1230   }
1231
1232   private void unloadActionElement(@NotNull Element element) {
1233     String className = element.getAttributeValue(CLASS_ATTR_NAME);
1234     String id = obtainActionId(element, className);
1235     unregisterAction(id);
1236   }
1237
1238   @Override
1239   public void registerAction(@NotNull String actionId, @NotNull AnAction action, @Nullable PluginId pluginId) {
1240     registerAction(actionId, action, pluginId, null);
1241   }
1242
1243   public void registerAction(@NotNull String actionId,
1244                              @NotNull AnAction action,
1245                              @Nullable PluginId pluginId,
1246                              @Nullable String projectType) {
1247     synchronized (myLock) {
1248       if (addToMap(actionId, action, pluginId, projectType) == null) return;
1249       if (myAction2Id.containsKey(action)) {
1250         reportActionError(pluginId, "action was already registered for another ID. ID is " + myAction2Id.get(action) +
1251                                     getPluginInfo(pluginId));
1252         return;
1253       }
1254       myId2Index.put(actionId, myRegisteredActionsCount++);
1255       myAction2Id.put(action, actionId);
1256       if (pluginId != null && !(action instanceof ActionGroup)) {
1257         myPlugin2Id.putValue(pluginId, actionId);
1258       }
1259       action.registerCustomShortcutSet(new ProxyShortcutSet(actionId), null);
1260       notifyCustomActionsSchema(actionId);
1261       updateHandlers(action);
1262     }
1263   }
1264
1265   private static void notifyCustomActionsSchema(@NotNull String registeredID) {
1266     CustomActionsSchema schema = ApplicationManager.getApplication().getServiceIfCreated(CustomActionsSchema.class);
1267     if (schema == null) return;
1268     for (ActionUrl url : schema.getActions()) {
1269       if (registeredID.equals(url.getComponent())) {
1270         schema.incrementModificationStamp();
1271         break;
1272       }
1273     }
1274   }
1275
1276   private AnAction addToMap(String actionId, AnAction action, PluginId pluginId, String projectType) {
1277     if (projectType != null || myId2Action.containsKey(actionId)) {
1278       return registerChameleon(actionId, action, pluginId, projectType);
1279     }
1280     else {
1281       myId2Action.put(actionId, action);
1282       return action;
1283     }
1284   }
1285
1286   private AnAction registerChameleon(String actionId, AnAction action, PluginId pluginId, String projectType) {
1287     ProjectType type = projectType == null ? null : new ProjectType(projectType);
1288     // make sure id+projectType is unique
1289     AnAction o = myId2Action.get(actionId);
1290     ChameleonAction chameleonAction;
1291     if (o == null) {
1292       chameleonAction = new ChameleonAction(action, type);
1293       myId2Action.put(actionId, chameleonAction);
1294       return chameleonAction;
1295     }
1296     if (o instanceof ChameleonAction) {
1297       chameleonAction = (ChameleonAction)o;
1298     }
1299     else {
1300       chameleonAction = new ChameleonAction(o, type);
1301       myId2Action.put(actionId, chameleonAction);
1302     }
1303     AnAction old = chameleonAction.addAction(action, type);
1304     if (old != null) {
1305       reportActionError(pluginId,
1306                         "action with the ID \"" + actionId + "\" was already registered. Action being registered is " + action +
1307                         "; Registered action is " +
1308                         myId2Action.get(actionId) + getPluginInfo(pluginId));
1309       return null;
1310     }
1311     return chameleonAction;
1312   }
1313
1314   @Override
1315   public void registerAction(@NotNull String actionId, @NotNull AnAction action) {
1316     registerAction(actionId, action, null);
1317   }
1318
1319   @Override
1320   public void unregisterAction(@NotNull String actionId) {
1321     unregisterAction(actionId, true);
1322   }
1323
1324   private void unregisterAction(@NotNull String actionId, boolean removeFromGroups) {
1325     synchronized (myLock) {
1326       if (!myId2Action.containsKey(actionId)) {
1327         if (LOG.isDebugEnabled()) {
1328           LOG.debug("action with ID " + actionId + " wasn't registered");
1329         }
1330         return;
1331       }
1332       AnAction actionToRemove = myId2Action.remove(actionId);
1333       myAction2Id.remove(actionToRemove);
1334       myId2Index.removeInt(actionId);
1335
1336       for (Map.Entry<PluginId, Collection<String>> entry : myPlugin2Id.entrySet()) {
1337         entry.getValue().remove(actionId);
1338       }
1339
1340       if (removeFromGroups) {
1341         CustomActionsSchema customActionSchema = ApplicationManager.getApplication().getServiceIfCreated(CustomActionsSchema.class);
1342         for (String groupId : myId2GroupId.get(actionId)) {
1343           if (customActionSchema != null) {
1344             customActionSchema.invalidateCustomizedActionGroup(groupId);
1345           }
1346           DefaultActionGroup group = (DefaultActionGroup)getActionOrStub(groupId);
1347           if (group == null) {
1348             LOG.error("Trying to remove action " + actionId + " from non-existing group " + groupId);
1349             continue;
1350           }
1351           group.remove(actionToRemove, actionId);
1352           if (!(group instanceof ActionGroupStub)) {
1353             //group can be used as a stub in other actions
1354             for (String parentOfGroup : myId2GroupId.get(groupId)) {
1355               DefaultActionGroup parentOfGroupAction = (DefaultActionGroup) getActionOrStub(parentOfGroup);
1356               if (parentOfGroupAction == null) {
1357                 LOG.error("Trying to remove action " + actionId + " from non-existing group " + parentOfGroup);
1358                 continue;
1359               }
1360               for (AnAction stub : parentOfGroupAction.getChildActionsOrStubs()) {
1361                 if (stub instanceof ActionGroupStub && ((ActionGroupStub)stub).getId() == groupId) {
1362                   ((ActionGroupStub)stub).remove(actionToRemove, actionId);
1363                 }
1364               }
1365             }
1366           }
1367         }
1368       }
1369       if (actionToRemove instanceof ActionGroup) {
1370         for (Map.Entry<String, Collection<String>> entry : myId2GroupId.entrySet()) {
1371           entry.getValue().remove(actionId);
1372         }
1373       }
1374       updateHandlers(actionToRemove);
1375     }
1376   }
1377
1378   @NotNull
1379   @Override
1380   public Comparator<String> getRegistrationOrderComparator() {
1381     return Comparator.comparingInt(myId2Index::getInt);
1382   }
1383
1384   @Override
1385   public String @NotNull [] getPluginActions(@NotNull PluginId pluginName) {
1386     return ArrayUtilRt.toStringArray(myPlugin2Id.get(pluginName));
1387   }
1388
1389   public void addActionPopup(@NotNull Object menu) {
1390     myPopups.add(menu);
1391     if (menu instanceof ActionPopupMenu) {
1392       for (ActionPopupMenuListener listener : myActionPopupMenuListeners) {
1393         listener.actionPopupMenuCreated((ActionPopupMenu)menu);
1394       }
1395     }
1396   }
1397
1398   void removeActionPopup(@NotNull Object menu) {
1399     final boolean removed = myPopups.remove(menu);
1400     if (removed && menu instanceof ActionPopupMenu) {
1401       for (ActionPopupMenuListener listener : myActionPopupMenuListeners) {
1402         listener.actionPopupMenuReleased((ActionPopupMenu)menu);
1403       }
1404     }
1405   }
1406
1407   @Override
1408   public void queueActionPerformedEvent(@NotNull final AnAction action, @NotNull DataContext context, @NotNull AnActionEvent event) {
1409     if (myPopups.isEmpty()) {
1410       fireAfterActionPerformed(action, context, event);
1411     }
1412   }
1413
1414   public boolean isToolWindowContextMenuVisible() {
1415     for (Object popup : myPopups) {
1416       if (popup instanceof ActionPopupMenuImpl &&
1417           ((ActionPopupMenuImpl)popup).isToolWindowContextMenu()) {
1418         return true;
1419       }
1420     }
1421     return false;
1422   }
1423
1424   @Override
1425   public boolean isActionPopupStackEmpty() {
1426     return myPopups.isEmpty();
1427   }
1428
1429   @Override
1430   public boolean isTransparentOnlyActionsUpdateNow() {
1431     return myTransparentOnlyUpdate;
1432   }
1433
1434   @Override
1435   public void addActionPopupMenuListener(@NotNull ActionPopupMenuListener listener, @NotNull Disposable parentDisposable) {
1436     myActionPopupMenuListeners.add(listener);
1437     Disposer.register(parentDisposable, () -> myActionPopupMenuListeners.remove(listener));
1438   }
1439
1440   @Override
1441   public void replaceAction(@NotNull String actionId, @NotNull AnAction newAction) {
1442     Class<?> callerClass = ReflectionUtil.getGrandCallerClass();
1443     PluginId pluginId = callerClass != null ? PluginManagerCore.getPluginByClassName(callerClass.getName()) : null;
1444     replaceAction(actionId, newAction, pluginId);
1445   }
1446
1447   private AnAction replaceAction(@NotNull String actionId, @NotNull AnAction newAction, @Nullable PluginId pluginId) {
1448     AnAction oldAction = newAction instanceof OverridingAction ? getAction(actionId) : getActionOrStub(actionId);
1449     if (oldAction != null) {
1450       if (newAction instanceof OverridingAction) {
1451         myBaseActions.put((OverridingAction)newAction, oldAction);
1452       }
1453       boolean isGroup = oldAction instanceof ActionGroup;
1454       if (isGroup != newAction instanceof ActionGroup) {
1455         throw new IllegalStateException("cannot replace a group with an action and vice versa: " + actionId);
1456       }
1457       for (String groupId : myId2GroupId.get(actionId)) {
1458         DefaultActionGroup group = (DefaultActionGroup)getActionOrStub(groupId);
1459         if (group == null) {
1460           throw new IllegalStateException("Trying to replace action which has been added to a non-existing group " + groupId);
1461         }
1462         group.replaceAction(oldAction, newAction);
1463       }
1464       unregisterAction(actionId, false);
1465     }
1466     registerAction(actionId, newAction, pluginId);
1467     return oldAction;
1468   }
1469
1470   /**
1471    * Returns the action overridden by the specified overriding action (with overrides="true" in plugin.xml).
1472    */
1473   public AnAction getBaseAction(OverridingAction overridingAction) {
1474     return myBaseActions.get(overridingAction);
1475   }
1476
1477   public Collection<String> getParentGroupIds(String actionId) {
1478     return myId2GroupId.get(actionId);
1479   }
1480
1481   @Override
1482   public void addAnActionListener(AnActionListener listener) {
1483     myActionListeners.add(listener);
1484   }
1485
1486   @Override
1487   public void removeAnActionListener(AnActionListener listener) {
1488     myActionListeners.remove(listener);
1489   }
1490
1491   @Override
1492   public void fireBeforeActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
1493     myPrevPerformedActionId = myLastPreformedActionId;
1494     myLastPreformedActionId = getId(action);
1495     if (myLastPreformedActionId == null && action instanceof ActionIdProvider) {
1496       myLastPreformedActionId = ((ActionIdProvider)action).getId();
1497     }
1498     //noinspection AssignmentToStaticFieldFromInstanceMethod
1499     IdeaLogger.ourLastActionId = myLastPreformedActionId;
1500     final PsiFile file = CommonDataKeys.PSI_FILE.getData(dataContext);
1501     final Language language = file != null ? file.getLanguage() : null;
1502     final List<EventPair> customData = new ArrayList<>();
1503     customData.add(EventFields.CurrentFile.with(language));
1504     Project project = CommonDataKeys.PROJECT.getData(dataContext);
1505     customData.add(EventFields.Language.with(getHostFileLanguage(dataContext, project)));
1506     if (action instanceof FusAwareAction) {
1507       List<EventPair> additionalUsageData = ((FusAwareAction)action).getAdditionalUsageData(event);
1508       customData.add(ActionsEventLogGroup.ADDITIONAL.with(new ObjectEventData(additionalUsageData.toArray(new EventPair[0]))));
1509     }
1510     ActionsCollectorImpl.recordActionInvoked(project, action, event, customData);
1511     for (AnActionListener listener : myActionListeners) {
1512       listener.beforeActionPerformed(action, dataContext, event);
1513     }
1514     publisher().beforeActionPerformed(action, dataContext, event);
1515   }
1516
1517   private static @Nullable Language getHostFileLanguage(@NotNull DataContext dataContext, @Nullable Project project) {
1518     if (project == null) return null;
1519     Editor editor = CommonDataKeys.HOST_EDITOR.getData(dataContext);
1520     if (editor == null) return null;
1521     PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
1522     return file != null ? file.getLanguage() : null;
1523   }
1524
1525   @Override
1526   public void fireAfterActionPerformed(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull AnActionEvent event) {
1527     myPrevPerformedActionId = myLastPreformedActionId;
1528     myLastPreformedActionId = getId(action);
1529     //noinspection AssignmentToStaticFieldFromInstanceMethod
1530     IdeaLogger.ourLastActionId = myLastPreformedActionId;
1531     for (AnActionListener listener : myActionListeners) {
1532       try {
1533         listener.afterActionPerformed(action, dataContext, event);
1534       }
1535       catch (AbstractMethodError ignored) {
1536       }
1537     }
1538     publisher().afterActionPerformed(action, dataContext, event);
1539   }
1540
1541   @Override
1542   public KeyboardShortcut getKeyboardShortcut(@NotNull String actionId) {
1543     AnAction action = ActionManager.getInstance().getAction(actionId);
1544     final ShortcutSet shortcutSet = action.getShortcutSet();
1545     final Shortcut[] shortcuts = shortcutSet.getShortcuts();
1546     for (final Shortcut shortcut : shortcuts) {
1547       // Shortcut can be MouseShortcut here.
1548       // For example IdeaVIM often assigns them
1549       if (shortcut instanceof KeyboardShortcut) {
1550         final KeyboardShortcut kb = (KeyboardShortcut)shortcut;
1551         if (kb.getSecondKeyStroke() == null) {
1552           return (KeyboardShortcut)shortcut;
1553         }
1554       }
1555     }
1556
1557     return null;
1558   }
1559
1560   @Override
1561   public void fireBeforeEditorTyping(char c, @NotNull DataContext dataContext) {
1562     myLastTimeEditorWasTypedIn = System.currentTimeMillis();
1563     for (AnActionListener listener : myActionListeners) {
1564       listener.beforeEditorTyping(c, dataContext);
1565     }
1566     publisher().beforeEditorTyping(c, dataContext);
1567   }
1568
1569   @Override
1570   public void fireAfterEditorTyping(char c, @NotNull DataContext dataContext) {
1571     for (AnActionListener listener : myActionListeners) {
1572       listener.afterEditorTyping(c, dataContext);
1573     }
1574     publisher().afterEditorTyping(c, dataContext);
1575   }
1576
1577   @Override
1578   public String getLastPreformedActionId() {
1579     return myLastPreformedActionId;
1580   }
1581
1582   @Override
1583   public String getPrevPreformedActionId() {
1584     return myPrevPerformedActionId;
1585   }
1586
1587   public @NotNull Set<String> getActionIds() {
1588     synchronized (myLock) {
1589       return new HashSet<>(myId2Action.keySet());
1590     }
1591   }
1592
1593   public void preloadActions(@NotNull ProgressIndicator indicator) {
1594     List<String> ids;
1595     synchronized (myLock) {
1596       ids = new ArrayList<>(myId2Action.keySet());
1597     }
1598     for (String id : ids) {
1599       indicator.checkCanceled();
1600       getActionImpl(id, false);
1601       // don't preload ActionGroup.getChildren() because that would un-stub child actions
1602       // and make it impossible to replace the corresponding actions later
1603       // (via unregisterAction+registerAction, as some app components do)
1604     }
1605   }
1606
1607   @NotNull
1608   @Override
1609   public ActionCallback tryToExecute(@NotNull AnAction action,
1610                                      @NotNull InputEvent inputEvent,
1611                                      @Nullable Component contextComponent,
1612                                      @Nullable String place,
1613                                      boolean now) {
1614     assert ApplicationManager.getApplication().isDispatchThread();
1615
1616     ActionCallback result = new ActionCallback();
1617     Runnable doRunnable = () -> tryToExecuteNow(action, inputEvent, contextComponent, place, result);
1618     if (now) {
1619       doRunnable.run();
1620     }
1621     else {
1622       //noinspection SSBasedInspection
1623       SwingUtilities.invokeLater(doRunnable);
1624     }
1625
1626     return result;
1627   }
1628
1629   private void tryToExecuteNow(@NotNull AnAction action, @NotNull InputEvent inputEvent, @Nullable Component contextComponent, String place, ActionCallback result) {
1630     Presentation presentation = action.getTemplatePresentation().clone();
1631     IdeFocusManager.findInstanceByContext(getContextBy(contextComponent)).doWhenFocusSettlesDown(() -> {
1632       ((TransactionGuardImpl)TransactionGuard.getInstance()).performUserActivity(() -> {
1633         DataContext context = getContextBy(contextComponent);
1634
1635         AnActionEvent event = new AnActionEvent(
1636           inputEvent, context,
1637           place != null ? place : ActionPlaces.UNKNOWN,
1638           presentation, this,
1639           inputEvent.getModifiersEx()
1640         );
1641
1642         ActionUtil.performDumbAwareUpdate(LaterInvocator.isInModalContext(), action, event, false);
1643         if (!event.getPresentation().isEnabled()) {
1644           result.setRejected();
1645           return;
1646         }
1647
1648         ActionUtil.lastUpdateAndCheckDumb(action, event, false);
1649         if (!event.getPresentation().isEnabled()) {
1650           result.setRejected();
1651           return;
1652         }
1653
1654         Component component = PlatformDataKeys.CONTEXT_COMPONENT.getData(context);
1655         if (component != null && !component.isShowing() && !ActionPlaces.TOUCHBAR_GENERAL.equals(place)) {
1656           result.setRejected();
1657           return;
1658         }
1659
1660         fireBeforeActionPerformed(action, context, event);
1661
1662         UIUtil.addAwtListener(event1 -> {
1663           if (event1.getID() == WindowEvent.WINDOW_OPENED || event1.getID() == WindowEvent.WINDOW_ACTIVATED) {
1664             if (!result.isProcessed()) {
1665               final WindowEvent we = (WindowEvent)event1;
1666               IdeFocusManager.findInstanceByComponent(we.getWindow()).doWhenFocusSettlesDown(result.createSetDoneRunnable(),
1667                                                                                              ModalityState.defaultModalityState());
1668             }
1669           }
1670         }, AWTEvent.WINDOW_EVENT_MASK, result);
1671
1672         ActionUtil.performActionDumbAware(action, event);
1673         result.setDone();
1674         queueActionPerformedEvent(action, context, event);
1675       });
1676     }, ModalityState.defaultModalityState());
1677   }
1678
1679   @Override
1680   public @NotNull List<EditorActionHandlerBean> getRegisteredHandlers(@NotNull EditorAction editorAction) {
1681     List<EditorActionHandlerBean> result = new ArrayList<>();
1682     String id = getId(editorAction);
1683     if (id != null) {
1684       List<EditorActionHandlerBean> extensions = EDITOR_ACTION_HANDLER_EP.getExtensionList();
1685       for (int i = extensions.size() - 1; i >= 0; i--) {
1686         EditorActionHandlerBean handlerBean = extensions.get(i);
1687         if (handlerBean.action.equals(id)) {
1688           result.add(handlerBean);
1689         }
1690       }
1691     }
1692     return result;
1693   }
1694
1695   private void updateAllHandlers() {
1696     synchronized (myLock) {
1697       myAction2Id.keySet().forEach(ActionManagerImpl::updateHandlers);
1698     }
1699   }
1700
1701   private static void updateHandlers(Object action) {
1702     if (action instanceof EditorAction) {
1703       ((EditorAction)action).clearDynamicHandlersCache();
1704     }
1705   }
1706
1707   private final class MyTimer extends Timer implements ActionListener {
1708     private final List<TimerListener> myTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1709     private final List<TimerListener> myTransparentTimerListeners = ContainerUtil.createLockFreeCopyOnWriteList();
1710     private int myLastTimePerformed;
1711
1712     private MyTimer() {
1713       super(TIMER_DELAY, null);
1714       addActionListener(this);
1715       setRepeats(true);
1716       final MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
1717       connection.subscribe(ApplicationActivationListener.TOPIC, new ApplicationActivationListener() {
1718         @Override
1719         public void applicationActivated(@NotNull IdeFrame ideFrame) {
1720           setDelay(TIMER_DELAY);
1721           restart();
1722         }
1723
1724         @Override
1725         public void applicationDeactivated(@NotNull IdeFrame ideFrame) {
1726           setDelay(DEACTIVATED_TIMER_DELAY);
1727         }
1728       });
1729     }
1730
1731     @Override
1732     public String toString() {
1733       return "Action manager timer";
1734     }
1735
1736     void addTimerListener(@NotNull TimerListener listener, boolean transparent) {
1737       (transparent ? myTransparentTimerListeners : myTimerListeners).add(listener);
1738     }
1739
1740     void removeTimerListener(@NotNull TimerListener listener, boolean transparent) {
1741       (transparent ? myTransparentTimerListeners : myTimerListeners).remove(listener);
1742     }
1743
1744     @Override
1745     public void actionPerformed(ActionEvent e) {
1746       if (myLastTimeEditorWasTypedIn + UPDATE_DELAY_AFTER_TYPING > System.currentTimeMillis()) {
1747         return;
1748       }
1749
1750       final int lastEventCount = myLastTimePerformed;
1751       myLastTimePerformed = ActivityTracker.getInstance().getCount();
1752
1753       if (myLastTimePerformed == lastEventCount && !Registry.is("actionSystem.always.update.toolbar.actions")) {
1754         return;
1755       }
1756
1757       boolean transparentOnly = myLastTimePerformed == lastEventCount;
1758
1759       try {
1760         myTransparentOnlyUpdate = transparentOnly;
1761         Set<TimerListener> notified = new HashSet<>();
1762         notifyListeners(myTransparentTimerListeners, notified);
1763
1764         if (transparentOnly) {
1765           return;
1766         }
1767
1768         notifyListeners(myTimerListeners, notified);
1769       }
1770       finally {
1771         myTransparentOnlyUpdate = false;
1772       }
1773     }
1774
1775     private void notifyListeners(final List<? extends TimerListener> timerListeners, final Set<? super TimerListener> notified) {
1776       for (TimerListener listener : timerListeners) {
1777         if (notified.add(listener)) {
1778           runListenerAction(listener);
1779         }
1780       }
1781     }
1782
1783     private void runListenerAction(@NotNull TimerListener listener) {
1784       ModalityState modalityState = listener.getModalityState();
1785       if (modalityState == null) return;
1786       LOG.debug("notify ", listener);
1787       if (!ModalityState.current().dominates(modalityState)) {
1788         try {
1789           listener.run();
1790         }
1791         catch (ProcessCanceledException ex) {
1792           // ignore
1793         }
1794         catch (Throwable e) {
1795           LOG.error(e);
1796         }
1797       }
1798     }
1799   }
1800 }