IDEA-160816 Search by abbreviation in Keymap configurable
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / keymap / impl / ui / ActionsTreeUtil.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.openapi.keymap.impl.ui;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.DataManager;
20 import com.intellij.ide.actionMacro.ActionMacro;
21 import com.intellij.ide.plugins.IdeaPluginDescriptor;
22 import com.intellij.ide.plugins.PluginManagerCore;
23 import com.intellij.ide.ui.search.SearchUtil;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
26 import com.intellij.openapi.actionSystem.ex.QuickList;
27 import com.intellij.openapi.diagnostic.Logger;
28 import com.intellij.openapi.extensions.Extensions;
29 import com.intellij.openapi.extensions.PluginId;
30 import com.intellij.openapi.keymap.KeyMapBundle;
31 import com.intellij.openapi.keymap.Keymap;
32 import com.intellij.openapi.keymap.KeymapExtension;
33 import com.intellij.openapi.keymap.ex.KeymapManagerEx;
34 import com.intellij.openapi.keymap.impl.ActionShortcutRestrictions;
35 import com.intellij.openapi.keymap.impl.KeymapImpl;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.util.Condition;
38 import com.intellij.openapi.util.registry.Registry;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.util.containers.ContainerUtil;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.Nullable;
43
44 import javax.swing.*;
45 import javax.swing.tree.DefaultMutableTreeNode;
46 import java.util.*;
47
48 public class ActionsTreeUtil {
49   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.keymap.impl.ui.ActionsTreeUtil");
50
51   public static final String MAIN_MENU_TITLE = KeyMapBundle.message("main.menu.action.title");
52   public static final String MAIN_TOOLBAR = KeyMapBundle.message("main.toolbar.title");
53   public static final String EDITOR_POPUP = KeyMapBundle.message("editor.popup.menu.title");
54
55   public static final String EDITOR_TAB_POPUP = KeyMapBundle.message("editor.tab.popup.menu.title");
56   public static final String FAVORITES_POPUP = KeyMapBundle.message("favorites.popup.title");
57   public static final String PROJECT_VIEW_POPUP = KeyMapBundle.message("project.view.popup.menu.title");
58   public static final String COMMANDER_POPUP = KeyMapBundle.message("commender.view.popup.menu.title");
59   public static final String J2EE_POPUP = KeyMapBundle.message("j2ee.view.popup.menu.title");
60
61   @NonNls
62   private static final String EDITOR_PREFIX = "Editor";
63   @NonNls private static final String TOOL_ACTION_PREFIX = "Tool_";
64
65   private ActionsTreeUtil() {
66   }
67
68   public static Map<String, String> createPluginActionsMap() {
69     Set<PluginId> visited = ContainerUtil.newHashSet();
70     Map<String, String> result = ContainerUtil.newHashMap();
71     for (IdeaPluginDescriptor descriptor : PluginManagerCore.getPlugins()) {
72       PluginId id = descriptor.getPluginId();
73       visited.add(id);
74       if (PluginManagerCore.CORE_PLUGIN_ID.equals(id.getIdString())) continue;
75       for (String actionId : ActionManagerEx.getInstanceEx().getPluginActions(id)) {
76         result.put(actionId, descriptor.getName());
77       }
78     }
79     for (PluginId id : PluginId.getRegisteredIds().values()) {
80       if (visited.contains(id)) continue;
81       for (String actionId : ActionManagerEx.getInstanceEx().getPluginActions(id)) {
82         result.put(actionId, id.getIdString());
83       }
84     }
85     return result;
86   }
87
88   private static Group createPluginsActionsGroup(Condition<AnAction> filtered) {
89     Group pluginsGroup = new Group(KeyMapBundle.message("plugins.group.title"), null, null);
90     final KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
91     ActionManagerEx managerEx = ActionManagerEx.getInstanceEx();
92     final List<IdeaPluginDescriptor> plugins = new ArrayList<>();
93     Collections.addAll(plugins, PluginManagerCore.getPlugins());
94     Collections.sort(plugins, (o1, o2) -> o1.getName().compareTo(o2.getName()));
95
96     List<PluginId> collected = new ArrayList<>();
97     for (IdeaPluginDescriptor plugin : plugins) {
98       collected.add(plugin.getPluginId());
99       Group pluginGroup;
100       if (plugin.getName().equals("IDEA CORE")) {
101         continue;
102       }
103       else {
104         pluginGroup = new Group(plugin.getName(), null, null);
105       }
106       final String[] pluginActions = managerEx.getPluginActions(plugin.getPluginId());
107       if (pluginActions == null || pluginActions.length == 0) {
108         continue;
109       }
110       Arrays.sort(pluginActions, (o1, o2) -> getTextToCompare(o1).compareTo(getTextToCompare(o2)));
111       for (String pluginAction : pluginActions) {
112         if (keymapManager.getBoundActions().contains(pluginAction)) continue;
113         final AnAction anAction = managerEx.getActionOrStub(pluginAction);
114         if (filtered == null || filtered.value(anAction)) {
115           pluginGroup.addActionId(pluginAction);
116         }
117       }
118       if (pluginGroup.getSize() > 0) {
119         pluginsGroup.addGroup(pluginGroup);
120       }
121     }
122
123     for (PluginId pluginId : PluginId.getRegisteredIds().values()) {
124       if (collected.contains(pluginId)) continue;
125       Group pluginGroup = new Group(pluginId.getIdString(), null, null);
126       final String[] pluginActions = managerEx.getPluginActions(pluginId);
127       if (pluginActions == null || pluginActions.length == 0) {
128         continue;
129       }
130       for (String pluginAction : pluginActions) {
131         if (keymapManager.getBoundActions().contains(pluginAction)) continue;
132         final AnAction anAction = managerEx.getActionOrStub(pluginAction);
133         if (filtered == null || filtered.value(anAction)) {
134           pluginGroup.addActionId(pluginAction);
135         }
136       }
137       if (pluginGroup.getSize() > 0) {
138         pluginsGroup.addGroup(pluginGroup);
139       }
140     }
141
142     return pluginsGroup;
143   }
144
145   private static Group createMainMenuGroup(Condition<AnAction> filtered) {
146     Group group = new Group(MAIN_MENU_TITLE, IdeActions.GROUP_MAIN_MENU, AllIcons.Nodes.KeymapMainMenu);
147     ActionGroup mainMenuGroup = (ActionGroup)ActionManager.getInstance().getActionOrStub(IdeActions.GROUP_MAIN_MENU);
148     fillGroupIgnorePopupFlag(mainMenuGroup, group, filtered);
149     return group;
150   }
151
152   @Nullable
153   private static Condition<AnAction> wrapFilter(@Nullable final Condition<AnAction> filter, final Keymap keymap, final ActionManager actionManager) {
154     final ActionShortcutRestrictions shortcutRestrictions = ActionShortcutRestrictions.getInstance();
155     return action -> {
156       if (action == null) return false;
157       final String id = action instanceof ActionStub ? ((ActionStub)action).getId() : actionManager.getId(action);
158       if (id != null) {
159         if (!Registry.is("keymap.show.alias.actions")) {
160           String binding = getActionBinding(keymap, id);
161           boolean bound = binding != null
162                           && actionManager.getAction(binding) != null // do not hide bound action, that miss the 'bound-with'
163                           && !hasAssociatedShortcutsInHierarchy(id, keymap); // do not hide bound actions when they are redefined
164           if (bound) {
165             return false;
166           }
167         }
168         if (!shortcutRestrictions.getForActionId(id).allowChanging) {
169           return false;
170         }
171       }
172
173       return filter == null || filter.value(action);
174     };
175   }
176
177   private static boolean hasAssociatedShortcutsInHierarchy(String id, Keymap keymap) {
178     while (keymap != null) {
179       if (((KeymapImpl)keymap).hasOwnActionId(id)) return true;
180       keymap = keymap.getParent();
181     }
182     return false;
183   }
184
185   private static void fillGroupIgnorePopupFlag(ActionGroup actionGroup, Group group, Condition<AnAction> filtered) {
186     AnAction[] mainMenuTopGroups = actionGroup instanceof DefaultActionGroup
187                                    ? ((DefaultActionGroup)actionGroup).getChildActionsOrStubs()
188                                    : actionGroup.getChildren(null);
189     for (AnAction action : mainMenuTopGroups) {
190       if (!(action instanceof ActionGroup)) continue;
191       Group subGroup = createGroup((ActionGroup)action, false, filtered);
192       if (subGroup.getSize() > 0) {
193         group.addGroup(subGroup);
194       }
195     }
196   }
197
198   public static Group createGroup(ActionGroup actionGroup, boolean ignore, Condition<AnAction> filtered) {
199     return createGroup(actionGroup, getName(actionGroup), null, null, ignore, filtered);
200   }
201
202   private static String getName(AnAction action) {
203     final String name = action.getTemplatePresentation().getText();
204     if (name != null && !name.isEmpty()) {
205       return name;
206     }
207     else {
208       final String id = action instanceof ActionStub ? ((ActionStub)action).getId() : ActionManager.getInstance().getId(action);
209       if (id != null) {
210         return id;
211       }
212       if (action instanceof DefaultActionGroup) {
213         final DefaultActionGroup group = (DefaultActionGroup)action;
214         if (group.getChildrenCount() == 0) return "Empty group";
215         final AnAction[] children = group.getChildActionsOrStubs();
216         for (AnAction child : children) {
217           if (!(child instanceof Separator)) {
218             return "group." + getName(child);
219           }
220         }
221         return "Empty unnamed group";
222       }
223       return action.getClass().getName();
224     }
225   }
226
227   public static Group createGroup(ActionGroup actionGroup,
228                                   String groupName,
229                                   Icon icon,
230                                   Icon openIcon,
231                                   boolean ignore,
232                                   Condition<AnAction> filtered) {
233     return createGroup(actionGroup, groupName, icon, openIcon, ignore, filtered, true);
234   }
235
236   public static Group createGroup(ActionGroup actionGroup, String groupName, Icon icon, Icon openIcon, boolean ignore, Condition<AnAction> filtered,
237                                   boolean normalizeSeparators) {
238     ActionManager actionManager = ActionManager.getInstance();
239     Group group = new Group(groupName, actionManager.getId(actionGroup), icon);
240     AnAction[] children = actionGroup instanceof DefaultActionGroup
241                           ? ((DefaultActionGroup)actionGroup).getChildActionsOrStubs()
242                           : actionGroup.getChildren(null);
243
244     for (AnAction action : children) {
245       if (action == null) {
246         LOG.error(groupName + " contains null actions");
247         continue;
248       }
249       if (action instanceof ActionGroup) {
250         Group subGroup = createGroup((ActionGroup)action, getName(action), null, null, ignore, filtered, normalizeSeparators);
251         if (subGroup.getSize() > 0) {
252           if (!ignore && !((ActionGroup)action).isPopup()) {
253             group.addAll(subGroup);
254           }
255           else {
256             group.addGroup(subGroup);
257           }
258         }
259         else if (filtered == null || filtered.value(action)) {
260           group.addGroup(subGroup);
261         }
262       }
263       else if (action instanceof Separator) {
264         group.addSeparator();
265       }
266       else {
267         String id = action instanceof ActionStub ? ((ActionStub)action).getId() : actionManager.getId(action);
268         if (id != null) {
269           if (id.startsWith(TOOL_ACTION_PREFIX)) continue;
270           if (filtered == null || filtered.value(action)) {
271             group.addActionId(id);
272           }
273         }
274       }
275     }
276     if (normalizeSeparators) group.normalizeSeparators();
277     return group;
278   }
279
280   private static Group createEditorActionsGroup(Condition<AnAction> filtered) {
281     ActionManager actionManager = ActionManager.getInstance();
282     DefaultActionGroup editorGroup = (DefaultActionGroup)actionManager.getActionOrStub(IdeActions.GROUP_EDITOR);
283     ArrayList<String> ids = new ArrayList<>();
284
285     addEditorActions(filtered, editorGroup, ids);
286
287     Collections.sort(ids);
288     Group group = new Group(KeyMapBundle.message("editor.actions.group.title"), IdeActions.GROUP_EDITOR, AllIcons.Nodes.KeymapEditor
289     );
290     for (String id : ids) {
291       group.addActionId(id);
292     }
293
294     return group;
295   }
296
297   @Nullable
298   private static String getActionBinding(final Keymap keymap, final String id) {
299     if (keymap == null) return null;
300     
301     Keymap parent = keymap.getParent();
302     String result = ((KeymapImpl)keymap).getActionBinding(id);
303     if (result == null && parent != null) {
304       result = ((KeymapImpl)parent).getActionBinding(id);
305     }
306     return result;
307   }
308
309   private static void addEditorActions(final Condition<AnAction> filtered,
310                                        final DefaultActionGroup editorGroup,
311                                        final ArrayList<String> ids) {
312     AnAction[] editorActions = editorGroup.getChildActionsOrStubs();
313     final ActionManager actionManager = ActionManager.getInstance();
314     for (AnAction editorAction : editorActions) {
315       if (editorAction instanceof DefaultActionGroup) {
316         addEditorActions(filtered, (DefaultActionGroup) editorAction, ids);
317       }
318       else {
319         String actionId = editorAction instanceof ActionStub ? ((ActionStub)editorAction).getId() : actionManager.getId(editorAction);
320         if (actionId == null) continue;
321         if (filtered == null || filtered.value(editorAction)) {
322           ids.add(actionId);
323         }
324       }
325     }
326   }
327
328   private static Group createExtensionGroup(Condition<AnAction> filtered, final Project project, KeymapExtension provider) {
329     return (Group) provider.createGroup(filtered, project);
330   }
331
332   private static Group createMacrosGroup(Condition<AnAction> filtered) {
333     final ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
334     String[] ids = actionManager.getActionIds(ActionMacro.MACRO_ACTION_PREFIX);
335     Arrays.sort(ids);
336     Group group = new Group(KeyMapBundle.message("macros.group.title"), null, null);
337     for (String id : ids) {
338       if (filtered == null || filtered.value(actionManager.getActionOrStub(id))) {
339         group.addActionId(id);
340       }
341     }
342     return group;
343   }
344
345   private static Group createQuickListsGroup(final Condition<AnAction> filtered, final String filter, final boolean forceFiltering, final QuickList[] quickLists) {
346     Arrays.sort(quickLists, (l1, l2) -> l1.getActionId().compareTo(l2.getActionId()));
347
348     Group group = new Group(KeyMapBundle.message("quick.lists.group.title"), null, null);
349     for (QuickList quickList : quickLists) {
350       if (filtered != null && filtered.value(ActionManagerEx.getInstanceEx().getAction(quickList.getActionId()))) {
351         group.addQuickList(quickList);
352       } else if (SearchUtil.isComponentHighlighted(quickList.getName(), filter, forceFiltering, null)) {
353         group.addQuickList(quickList);
354       } else if (filtered == null && StringUtil.isEmpty(filter)) {
355         group.addQuickList(quickList);
356       }
357     }
358     return group;
359   }
360
361
362   private static Group createOtherGroup(Condition<AnAction> filtered, Group addedActions, final Keymap keymap) {
363     addedActions.initIds();
364     ArrayList<String> result = new ArrayList<>();
365
366     if (keymap != null) {
367       String[] actionIds = keymap.getActionIds();
368       for (String id : actionIds) {
369         if (id.startsWith(EDITOR_PREFIX)) {
370           AnAction action = ActionManager.getInstance().getActionOrStub("$" + id.substring(6));
371           if (action != null) continue;
372         }
373
374         if (!id.startsWith(QuickList.QUICK_LIST_PREFIX) && !addedActions.containsId(id)) {
375           result.add(id);
376         }
377       }
378     }
379
380     // add all registered actions
381     final ActionManagerEx actionManager = ActionManagerEx.getInstanceEx();
382     final KeymapManagerEx keymapManager = KeymapManagerEx.getInstanceEx();
383     String[] registeredActionIds = actionManager.getActionIds("");
384     for (String id : registeredActionIds) {
385       final AnAction actionOrStub = actionManager.getActionOrStub(id);
386       if (actionOrStub instanceof ActionGroup && !((ActionGroup)actionOrStub).canBePerformed(DataManager.getInstance().getDataContext())) {
387         continue;
388       }
389       if (id.startsWith(QuickList.QUICK_LIST_PREFIX) || addedActions.containsId(id) || result.contains(id)) {
390         continue;
391       }
392
393       if (keymapManager.getBoundActions().contains(id)) continue;
394
395       result.add(id);
396     }
397
398     filterOtherActionsGroup(result);
399
400     ContainerUtil.quickSort(result, (id1, id2) -> getTextToCompare(id1).compareToIgnoreCase(getTextToCompare(id2)));
401
402     Group group = new Group(KeyMapBundle.message("other.group.title"), AllIcons.Nodes.KeymapOther);
403     for (String id : result) {
404       if (filtered == null || filtered.value(actionManager.getActionOrStub(id))) group.addActionId(id);
405     }
406     return group;
407   }
408
409   private static String getTextToCompare(String id) {
410     AnAction action = ActionManager.getInstance().getActionOrStub(id);
411     if (action == null) {
412       return id;
413     }
414     String text = action.getTemplatePresentation().getText();
415     return text != null ? text : id;
416   }
417
418   private static void filterOtherActionsGroup(ArrayList<String> actions) {
419     filterOutGroup(actions, IdeActions.GROUP_GENERATE);
420     filterOutGroup(actions, IdeActions.GROUP_NEW);
421     filterOutGroup(actions, IdeActions.GROUP_CHANGE_SCHEME);
422   }
423
424   private static void filterOutGroup(ArrayList<String> actions, String groupId) {
425     if (groupId == null) {
426       throw new IllegalArgumentException();
427     }
428     ActionManager actionManager = ActionManager.getInstance();
429     AnAction action = actionManager.getActionOrStub(groupId);
430     if (action instanceof DefaultActionGroup) {
431       DefaultActionGroup group = (DefaultActionGroup)action;
432       AnAction[] children = group.getChildActionsOrStubs();
433       for (AnAction child : children) {
434         String childId = child instanceof ActionStub ? ((ActionStub)child).getId() : actionManager.getId(child);
435         if (childId == null) {
436           // SCR 35149
437           continue;
438         }
439         if (child instanceof DefaultActionGroup) {
440           filterOutGroup(actions, childId);
441         }
442         else {
443           actions.remove(childId);
444         }
445       }
446     }
447   }
448
449   public static DefaultMutableTreeNode createNode(Group group) {
450     DefaultMutableTreeNode node = new DefaultMutableTreeNode(group);
451     for (Object child : group.getChildren()) {
452       if (child instanceof Group) {
453         DefaultMutableTreeNode childNode = createNode((Group)child);
454         node.add(childNode);
455       }
456       else {
457         LOG.assertTrue(child != null);
458         node.add(new DefaultMutableTreeNode(child));
459       }
460     }
461     return node;
462   }
463
464   public static Group createMainGroup(final Project project, final Keymap keymap, final QuickList[] quickLists) {
465     return createMainGroup(project, keymap, quickLists, null, false, null);
466   }
467
468   public static Group createMainGroup(final Project project,
469                                       final Keymap keymap,
470                                       final QuickList[] quickLists,
471                                       final String filter,
472                                       final boolean forceFiltering,
473                                       final Condition<AnAction> filtered) {
474     final Condition<AnAction> wrappedFilter = wrapFilter(filtered, keymap, ActionManager.getInstance());
475     Group mainGroup = new Group(KeyMapBundle.message("all.actions.group.title"), null, null);
476     mainGroup.addGroup(createEditorActionsGroup(wrappedFilter));
477     mainGroup.addGroup(createMainMenuGroup(wrappedFilter));
478     for (KeymapExtension extension : Extensions.getExtensions(KeymapExtension.EXTENSION_POINT_NAME)) {
479       final Group group = createExtensionGroup(wrappedFilter, project, extension);
480       if (group != null) {
481         mainGroup.addGroup(group);
482       }
483     }
484     mainGroup.addGroup(createMacrosGroup(wrappedFilter));
485     mainGroup.addGroup(createQuickListsGroup(wrappedFilter, filter, forceFiltering, quickLists));
486     mainGroup.addGroup(createPluginsActionsGroup(wrappedFilter));
487     mainGroup.addGroup(createOtherGroup(wrappedFilter, mainGroup, keymap));
488     if (!StringUtil.isEmpty(filter) || filtered != null) {
489       final ArrayList list = mainGroup.getChildren();
490       for (Iterator i = list.iterator(); i.hasNext();) {
491         final Object o = i.next();
492         if (o instanceof Group) {
493           final Group group = (Group)o;
494           if (group.getSize() == 0) {
495             if (!SearchUtil.isComponentHighlighted(group.getName(), filter, forceFiltering, null)) {
496               i.remove();
497             }
498           }
499         }
500       }
501     }
502     return mainGroup;
503   }
504
505   public static Condition<AnAction> isActionFiltered(final String filter, final boolean force) {
506     return action -> {
507       if (filter == null) return true;
508       if (action == null) return false;
509       final String insensitiveFilter = filter.toLowerCase();
510       ArrayList<String> options = new ArrayList<>();
511       options.add(action.getTemplatePresentation().getText());
512       options.add(action.getTemplatePresentation().getDescription());
513       options.add(action instanceof ActionStub ? ((ActionStub)action).getId() : ActionManager.getInstance().getId(action));
514       options.addAll(AbbreviationManager.getInstance().getAbbreviations(ActionManager.getInstance().getId(action)));
515
516       for (String text : options) {
517         if (text != null) {
518           final String lowerText = text.toLowerCase();
519
520           if (SearchUtil.isComponentHighlighted(lowerText, insensitiveFilter, force, null)) {
521             return true;
522           }
523           else if (lowerText.contains(insensitiveFilter)) {
524             return true;
525           }
526         }
527       }
528       return false;
529     };
530   }
531
532   public static Condition<AnAction> isActionFiltered(final ActionManager actionManager,
533                                                      final Keymap keymap,
534                                                      final Shortcut shortcut) {
535     return action -> {
536       if (shortcut == null) return true;
537       if (action == null) return false;
538       final Shortcut[] actionShortcuts =
539         keymap.getShortcuts(action instanceof ActionStub ? ((ActionStub)action).getId() : actionManager.getId(action));
540       for (Shortcut actionShortcut : actionShortcuts) {
541         if (actionShortcut != null && actionShortcut.startsWith(shortcut)) {
542           return true;
543         }
544       }
545       return false;
546     };
547   }
548
549   public static Condition<AnAction> isActionFiltered(final ActionManager actionManager,
550                                                      final Keymap keymap,
551                                                      final Shortcut shortcut,
552                                                      final String filter,
553                                                      final boolean force) {
554     return filter != null && filter.length() > 0 ? isActionFiltered(filter, force) :
555            shortcut != null ? isActionFiltered(actionManager, keymap, shortcut) : null;
556   }
557 }