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