custom icons for intention actions
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / intention / impl / IntentionListStep.java
1 /*
2  * Copyright 2000-2010 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
17 package com.intellij.codeInsight.intention.impl;
18
19 import com.intellij.codeInsight.daemon.impl.HighlightInfo;
20 import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass;
21 import com.intellij.codeInsight.hint.HintManager;
22 import com.intellij.codeInsight.intention.EmptyIntentionAction;
23 import com.intellij.codeInsight.intention.IntentionAction;
24 import com.intellij.codeInsight.intention.PreferredAction;
25 import com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper;
26 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
27 import com.intellij.codeInspection.ex.QuickFixWrapper;
28 import com.intellij.openapi.application.ApplicationManager;
29 import com.intellij.openapi.editor.Editor;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.ui.popup.*;
32 import com.intellij.openapi.util.Comparing;
33 import com.intellij.openapi.util.Iconable;
34 import com.intellij.psi.*;
35 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
36 import com.intellij.psi.util.PsiUtilBase;
37 import gnu.trove.THashSet;
38 import gnu.trove.TObjectHashingStrategy;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import javax.swing.*;
43 import java.util.*;
44
45 /**
46 * @author cdr
47 */
48 class IntentionListStep implements ListPopupStep<IntentionActionWithTextCaching>, SpeedSearchFilter<IntentionActionWithTextCaching> {
49   private final Set<IntentionActionWithTextCaching> myCachedIntentions = new THashSet<IntentionActionWithTextCaching>(ACTION_TEXT_AND_CLASS_EQUALS);
50   private final Set<IntentionActionWithTextCaching> myCachedErrorFixes = new THashSet<IntentionActionWithTextCaching>(ACTION_TEXT_AND_CLASS_EQUALS);
51   private final Set<IntentionActionWithTextCaching> myCachedInspectionFixes = new THashSet<IntentionActionWithTextCaching>(ACTION_TEXT_AND_CLASS_EQUALS);
52   private final Set<IntentionActionWithTextCaching> myCachedGutters = new THashSet<IntentionActionWithTextCaching>(ACTION_TEXT_AND_CLASS_EQUALS);
53   private final IntentionManagerSettings mySettings;
54   @Nullable
55   private final IntentionHintComponent myIntentionHintComponent;
56   private final Editor myEditor;
57   private final PsiFile myFile;
58   private final Project myProject;
59   private static final TObjectHashingStrategy<IntentionActionWithTextCaching> ACTION_TEXT_AND_CLASS_EQUALS = new TObjectHashingStrategy<IntentionActionWithTextCaching>() {
60     public int computeHashCode(final IntentionActionWithTextCaching object) {
61       return object.getText().hashCode();
62     }
63
64     public boolean equals(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
65       return o1.getAction().getClass() == o2.getAction().getClass() && o1.getText().equals(o2.getText());
66     }
67   };
68   private Runnable myFinalRunnable;
69
70   IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent, ShowIntentionsPass.IntentionsInfo intentions, Editor editor, PsiFile file,
71                     Project project) {
72     myIntentionHintComponent = intentionHintComponent;
73     myEditor = editor;
74     myFile = file;
75     myProject = project;
76     mySettings = IntentionManagerSettings.getInstance();
77     updateActions(intentions);
78   }
79
80   //true if something changed
81   boolean updateActions(ShowIntentionsPass.IntentionsInfo intentions) {
82     boolean result = wrapActionsTo(intentions.errorFixesToShow, myCachedErrorFixes);
83     result &= wrapActionsTo(intentions.inspectionFixesToShow, myCachedInspectionFixes);
84     result &= wrapActionsTo(intentions.intentionsToShow, myCachedIntentions);
85     result &= wrapActionsTo(intentions.guttersToShow, myCachedGutters);
86     return !result;
87   }
88
89   private boolean wrapActionsTo(final List<HighlightInfo.IntentionActionDescriptor> descriptors, final Set<IntentionActionWithTextCaching> cachedActions) {
90     final int caretOffset = myEditor.getCaretModel().getOffset();
91     final int fileOffset = caretOffset > 0 && caretOffset == myFile.getTextLength() ? caretOffset - 1 : caretOffset;
92     PsiElement element;
93     if (myFile instanceof PsiCompiledElement) {
94       element = myFile;
95     }
96     else if (PsiDocumentManager.getInstance(myProject).isUncommited(myEditor.getDocument())) {
97       //???
98       FileViewProvider viewProvider = myFile.getViewProvider();
99       element = viewProvider.findElementAt(fileOffset, viewProvider.getBaseLanguage());
100     }
101     else {
102       element = InjectedLanguageUtil.findElementAtNoCommit(myFile, fileOffset);
103     }
104     boolean result = removeInvalidActions(cachedActions, element);
105     for (HighlightInfo.IntentionActionDescriptor descriptor : descriptors) {
106       IntentionAction action = descriptor.getAction();
107       if (!isAvailable(action, element)) continue;
108       if (element == null) continue;
109       IntentionActionWithTextCaching cachedAction = wrapAction(element, descriptor);
110       result &= !cachedActions.add(cachedAction);
111     }
112     return result;
113   }
114
115   IntentionActionWithTextCaching wrapAction(PsiElement element, HighlightInfo.IntentionActionDescriptor descriptor) {
116     IntentionActionWithTextCaching cachedAction = new IntentionActionWithTextCaching(descriptor);
117     final List<IntentionAction> options = descriptor.getOptions(element);
118     if (options != null) {
119       for (IntentionAction option : options) {
120         if (!option.isAvailable(myProject, myEditor, element.getContainingFile())) continue;
121         IntentionActionWithTextCaching textCaching = new IntentionActionWithTextCaching(option);
122         boolean isErrorFix = myCachedErrorFixes.contains(textCaching);
123         if (isErrorFix) {
124           cachedAction.addErrorFix(option);
125         }
126         boolean isInspectionFix = myCachedInspectionFixes.contains(textCaching);
127         if (isInspectionFix) {
128           cachedAction.addInspectionFix(option);
129         }
130         else {
131           cachedAction.addIntention(option);
132         }
133       }
134     }
135     return cachedAction;
136   }
137
138   private boolean removeInvalidActions(final Collection<IntentionActionWithTextCaching> cachedActions, final PsiElement element) {
139     boolean result = true;
140     Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator();
141     while (iterator.hasNext()) {
142       IntentionActionWithTextCaching cachedAction = iterator.next();
143       IntentionAction action = cachedAction.getAction();
144       if (!isAvailable(action, element)) {
145         iterator.remove();
146         result = false;
147       }
148     }
149     return result;
150   }
151
152   private boolean isAvailable(IntentionAction action, PsiElement element) {
153     return ShowIntentionActionsHandler.availableFor(myFile, myEditor, action, element) != null;
154   }
155
156   public String getTitle() {
157     return null;
158   }
159
160   public boolean isSelectable(final IntentionActionWithTextCaching action) {
161     return true;
162   }
163
164   public PopupStep onChosen(final IntentionActionWithTextCaching action, final boolean finalChoice) {
165     if (finalChoice && !(action.getAction() instanceof EmptyIntentionAction)) {
166       applyAction(action);
167       return FINAL_CHOICE;
168     }
169
170     if (hasSubstep(action)) {
171       return getSubStep(action, action.getToolName());
172     }
173
174     return FINAL_CHOICE;
175   }
176
177   public Runnable getFinalRunnable() {
178     return myFinalRunnable;
179   }
180
181   private void applyAction(final IntentionActionWithTextCaching cachedAction) {
182     myFinalRunnable = new Runnable() {
183       public void run() {
184         HintManager.getInstance().hideAllHints();
185         ApplicationManager.getApplication().invokeLater(new Runnable() {
186           public void run() {
187             if (myProject.isDisposed()) return;
188             PsiDocumentManager.getInstance(myProject).commitAllDocuments();
189             final PsiFile file = PsiUtilBase.getPsiFileInEditor(myEditor, myProject);
190             if (file == null) {
191               return;
192             }
193
194             ShowIntentionActionsHandler.chooseActionAndInvoke(file, myEditor, cachedAction.getAction(), cachedAction.getText());
195           }
196         });
197       }
198     };
199   }
200
201   IntentionListStep getSubStep(final IntentionActionWithTextCaching action, final String title) {
202     ShowIntentionsPass.IntentionsInfo intentions = new ShowIntentionsPass.IntentionsInfo();
203     for (final IntentionAction optionIntention : action.getOptionIntentions()) {
204       intentions.intentionsToShow.add(new HighlightInfo.IntentionActionDescriptor(optionIntention, getIcon(optionIntention)));
205     }
206     for (final IntentionAction optionFix : action.getOptionErrorFixes()) {
207       intentions.errorFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
208     }
209     for (final IntentionAction optionFix : action.getOptionInspectionFixes()) {
210       intentions.inspectionFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
211     }
212
213     return new IntentionListStep(myIntentionHintComponent, intentions,myEditor, myFile, myProject){
214       public String getTitle() {
215         return title;
216       }
217     };
218   }
219
220   private static Icon getIcon(IntentionAction optionIntention) {
221     return optionIntention instanceof Iconable ? ((Iconable)optionIntention).getIcon(0) : null;
222   }
223
224   public boolean hasSubstep(final IntentionActionWithTextCaching action) {
225     return action.getOptionIntentions().size() + action.getOptionErrorFixes().size() > 0;
226   }
227
228   @NotNull
229   public List<IntentionActionWithTextCaching> getValues() {
230     List<IntentionActionWithTextCaching> result = new ArrayList<IntentionActionWithTextCaching>(myCachedErrorFixes);
231     result.addAll(myCachedInspectionFixes);
232     result.addAll(myCachedIntentions);
233     result.addAll(myCachedGutters);
234     Collections.sort(result, new Comparator<IntentionActionWithTextCaching>() {
235       public int compare(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
236         int weight1 = getWeight(o1);
237         int weight2 = getWeight(o2);
238         if (weight1 != weight2) {
239           return weight2 - weight1;
240         }
241         return Comparing.compare(o1.getText(), o2.getText());
242       }
243     });
244     return result;
245   }
246
247   private int getWeight(IntentionActionWithTextCaching action) {
248     if (action.getAction() instanceof PreferredAction) {
249       return 3;
250     }
251     else if (myCachedErrorFixes.contains(action)) {
252       return 2;
253     }
254     else if (myCachedInspectionFixes.contains(action)) {
255       return 1;
256     }
257     else if (action.getAction() instanceof EmptyIntentionAction) {
258       return -1;
259     }
260     else {
261       return 0;
262     }
263   }
264
265   @NotNull
266   public String getTextFor(final IntentionActionWithTextCaching action) {
267     return action.getAction().getText();
268   }
269
270   public Icon getIconFor(final IntentionActionWithTextCaching value) {
271     if (value.getIcon() != null) {
272       return value.getIcon();
273     }
274
275     final IntentionAction action = value.getAction();
276
277     Object iconable = null;
278     //custom icon
279     if (action instanceof QuickFixWrapper) {
280       iconable = ((QuickFixWrapper)action).getFix();
281     } else if (action instanceof IntentionActionWrapper) {
282       iconable = ((IntentionActionWrapper)action).getDelegate(); 
283     }
284
285     if (iconable instanceof Iconable) {
286       final Icon icon = ((Iconable)iconable).getIcon(0);
287       if (icon != null) {
288         return icon;
289       }
290     }
291
292     if (mySettings.isShowLightBulb(action)) {
293       return myCachedErrorFixes.contains(value) ? IntentionHintComponent.ourQuickFixIcon
294              : myCachedInspectionFixes.contains(value) ? IntentionHintComponent.ourBulbIcon :
295                IntentionHintComponent.ourIntentionIcon;
296     }
297     else {
298       return myCachedErrorFixes.contains(value) ? IntentionHintComponent.ourQuickFixOffIcon : IntentionHintComponent.ourIntentionOffIcon;
299     }
300   }
301
302   public void canceled() {
303     if (myIntentionHintComponent != null) {
304       myIntentionHintComponent.canceled(this);
305     }
306   }
307
308   public int getDefaultOptionIndex() { return 0; }
309   public ListSeparator getSeparatorAbove(final IntentionActionWithTextCaching value) {
310     List<IntentionActionWithTextCaching> values = getValues();
311     int index = values.indexOf(value);
312     if (index == 0) return null;
313     IntentionActionWithTextCaching prev = values.get(index - 1);
314
315     if (getWeight(value) != getWeight(prev)) {
316       return new ListSeparator();
317     }
318     return null;
319   }
320   public boolean isMnemonicsNavigationEnabled() { return false; }
321   public MnemonicNavigationFilter<IntentionActionWithTextCaching> getMnemonicNavigationFilter() { return null; }
322   public boolean isSpeedSearchEnabled() { return true; }
323   public boolean isAutoSelectionEnabled() { return false; }
324   public SpeedSearchFilter<IntentionActionWithTextCaching> getSpeedSearchFilter() { return this; }
325
326   //speed search filter
327   public boolean canBeHidden(final IntentionActionWithTextCaching value) { return true;}
328   public String getIndexedString(final IntentionActionWithTextCaching value) { return getTextFor(value);}
329 }