hide non-dumb-aware intentions (EA-65004 - INRE: FileBasedIndexImpl.handleDumbMode)
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / intention / impl / IntentionListStep.java
1 /*
2  * Copyright 2000-2014 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.HighPriorityAction;
24 import com.intellij.codeInsight.intention.IntentionAction;
25 import com.intellij.codeInsight.intention.LowPriorityAction;
26 import com.intellij.codeInsight.intention.impl.config.IntentionActionWrapper;
27 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
28 import com.intellij.codeInspection.IntentionWrapper;
29 import com.intellij.codeInspection.LocalQuickFix;
30 import com.intellij.codeInspection.SuppressIntentionActionFromFix;
31 import com.intellij.codeInspection.ex.QuickFixWrapper;
32 import com.intellij.icons.AllIcons;
33 import com.intellij.openapi.application.ApplicationManager;
34 import com.intellij.openapi.diagnostic.Logger;
35 import com.intellij.openapi.editor.Editor;
36 import com.intellij.openapi.project.DumbService;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.ui.popup.*;
39 import com.intellij.openapi.util.Comparing;
40 import com.intellij.openapi.util.Iconable;
41 import com.intellij.psi.*;
42 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
43 import com.intellij.psi.util.PsiUtilBase;
44 import com.intellij.util.ThreeState;
45 import com.intellij.util.containers.ContainerUtil;
46 import gnu.trove.THashSet;
47 import gnu.trove.TObjectHashingStrategy;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50
51 import javax.swing.*;
52 import java.util.*;
53
54 /**
55 * @author cdr
56 */
57 class IntentionListStep implements ListPopupStep<IntentionActionWithTextCaching>, SpeedSearchFilter<IntentionActionWithTextCaching> {
58   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.intention.impl.IntentionListStep");
59
60   private final Set<IntentionActionWithTextCaching> myCachedIntentions =
61     ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
62   private final Set<IntentionActionWithTextCaching> myCachedErrorFixes =
63     ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
64   private final Set<IntentionActionWithTextCaching> myCachedInspectionFixes = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
65   private final Set<IntentionActionWithTextCaching> myCachedGutters = ContainerUtil.newConcurrentSet(ACTION_TEXT_AND_CLASS_EQUALS);
66   private final IntentionManagerSettings mySettings;
67   @Nullable
68   private final IntentionHintComponent myIntentionHintComponent;
69   private final Editor myEditor;
70   private final PsiFile myFile;
71   private final Project myProject;
72   private static final TObjectHashingStrategy<IntentionActionWithTextCaching> ACTION_TEXT_AND_CLASS_EQUALS = new TObjectHashingStrategy<IntentionActionWithTextCaching>() {
73     @Override
74     public int computeHashCode(final IntentionActionWithTextCaching object) {
75       return object.getText().hashCode();
76     }
77
78     @Override
79     public boolean equals(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
80       return o1.getAction().getClass() == o2.getAction().getClass() && o1.getText().equals(o2.getText());
81     }
82   };
83   private Runnable myFinalRunnable;
84
85   IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent,
86                     @NotNull ShowIntentionsPass.IntentionsInfo intentions,
87                     @NotNull Editor editor,
88                     @NotNull PsiFile file,
89                     @NotNull Project project) {
90     this(intentionHintComponent, editor, file, project);
91     updateActions(intentions);
92   }
93
94   IntentionListStep(@Nullable IntentionHintComponent intentionHintComponent,
95                     @NotNull Editor editor,
96                     @NotNull PsiFile file,
97                     @NotNull Project project) {
98     myIntentionHintComponent = intentionHintComponent;
99     myEditor = editor;
100     myFile = file;
101     myProject = project;
102     mySettings = IntentionManagerSettings.getInstance();
103   }
104
105   //true if something changed
106   boolean updateActions(@NotNull ShowIntentionsPass.IntentionsInfo intentions) {
107     boolean changed = wrapActionsTo(intentions.errorFixesToShow, myCachedErrorFixes);
108     changed |= wrapActionsTo(intentions.inspectionFixesToShow, myCachedInspectionFixes);
109     changed |= wrapActionsTo(intentions.intentionsToShow, myCachedIntentions);
110     changed |= wrapActionsTo(intentions.guttersToShow, myCachedGutters);
111     return changed;
112   }
113
114   private boolean wrapActionsTo(@NotNull List<HighlightInfo.IntentionActionDescriptor> newDescriptors,
115                                 @NotNull Set<IntentionActionWithTextCaching> cachedActions) {
116     final int caretOffset = myEditor.getCaretModel().getOffset();
117     final int fileOffset = caretOffset > 0 && caretOffset == myFile.getTextLength() ? caretOffset - 1 : caretOffset;
118     PsiElement element;
119     final PsiElement hostElement;
120     if (myFile instanceof PsiCompiledElement) {
121       hostElement = element = myFile;
122
123     }
124     else if (PsiDocumentManager.getInstance(myProject).isUncommited(myEditor.getDocument())) {
125       //???
126       FileViewProvider viewProvider = myFile.getViewProvider();
127       hostElement = element = viewProvider.findElementAt(fileOffset, viewProvider.getBaseLanguage());
128     }
129     else {
130       hostElement = myFile.getViewProvider().findElementAt(fileOffset, myFile.getLanguage());
131       element = InjectedLanguageUtil.findElementAtNoCommit(myFile, fileOffset);
132     }
133     PsiFile injectedFile;
134     Editor injectedEditor;
135     if (element == null || element == hostElement) {
136       injectedFile = myFile;
137       injectedEditor = myEditor;
138     }
139     else {
140       injectedFile = element.getContainingFile();
141       injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(myEditor, injectedFile);
142     }
143
144     boolean changed = false;
145     for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext();) {
146       IntentionActionWithTextCaching cachedAction = iterator.next();
147       IntentionAction action = cachedAction.getAction();
148       if (!ShowIntentionActionsHandler.availableFor(myFile, myEditor, action)
149         && (hostElement == element || element != null && !ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action))) {
150         iterator.remove();
151         changed = true;
152       }
153     }
154
155     Set<IntentionActionWithTextCaching> wrappedNew = new THashSet<IntentionActionWithTextCaching>(newDescriptors.size(), ACTION_TEXT_AND_CLASS_EQUALS);
156     for (HighlightInfo.IntentionActionDescriptor descriptor : newDescriptors) {
157       final IntentionAction action = descriptor.getAction();
158       if (element != null && element != hostElement && ShowIntentionActionsHandler.availableFor(injectedFile, injectedEditor, action)) {
159         IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, element, injectedFile, injectedEditor);
160         wrappedNew.add(cachedAction);
161         changed |= cachedActions.add(cachedAction);
162       }
163       else if (hostElement != null && ShowIntentionActionsHandler.availableFor(myFile, myEditor, action)) {
164         IntentionActionWithTextCaching cachedAction = wrapAction(descriptor, hostElement, myFile, myEditor);
165         wrappedNew.add(cachedAction);
166         changed |= cachedActions.add(cachedAction);
167       }
168     }
169     for (Iterator<IntentionActionWithTextCaching> iterator = cachedActions.iterator(); iterator.hasNext();) {
170       IntentionActionWithTextCaching cachedAction = iterator.next();
171       if (!wrappedNew.contains(cachedAction)) {
172         // action disappeared
173         iterator.remove();
174         changed = true;
175       }
176     }
177     return changed;
178   }
179
180   @NotNull
181   IntentionActionWithTextCaching wrapAction(@NotNull HighlightInfo.IntentionActionDescriptor descriptor,
182                                             @NotNull PsiElement element,
183                                             @NotNull PsiFile containingFile,
184                                             @NotNull Editor containingEditor) {
185     IntentionActionWithTextCaching cachedAction = new IntentionActionWithTextCaching(descriptor);
186     final List<IntentionAction> options = descriptor.getOptions(element, containingEditor);
187     if (options == null) return cachedAction;
188     for (IntentionAction option : options) {
189       if (!option.isAvailable(myProject, containingEditor, containingFile)) {
190         // if option is not applicable in injected fragment, check in host file context
191         if (containingEditor == myEditor || !option.isAvailable(myProject, myEditor, myFile)) {
192           continue;
193         }
194       }
195       IntentionActionWithTextCaching textCaching = new IntentionActionWithTextCaching(option);
196       boolean isErrorFix = myCachedErrorFixes.contains(textCaching);
197       if (isErrorFix) {
198         cachedAction.addErrorFix(option);
199       }
200       boolean isInspectionFix = myCachedInspectionFixes.contains(textCaching);
201       if (isInspectionFix) {
202         cachedAction.addInspectionFix(option);
203       }
204       else {
205         cachedAction.addIntention(option);
206       }
207     }
208     return cachedAction;
209   }
210
211   @Override
212   public String getTitle() {
213     return null;
214   }
215
216   @Override
217   public boolean isSelectable(final IntentionActionWithTextCaching action) {
218     return true;
219   }
220
221   @Override
222   public PopupStep onChosen(final IntentionActionWithTextCaching action, final boolean finalChoice) {
223     if (finalChoice && !(action.getAction() instanceof EmptyIntentionAction)) {
224       applyAction(action);
225       return FINAL_CHOICE;
226     }
227
228     if (hasSubstep(action)) {
229       return getSubStep(action, action.getToolName());
230     }
231
232     return FINAL_CHOICE;
233   }
234
235   @Override
236   public Runnable getFinalRunnable() {
237     return myFinalRunnable;
238   }
239
240   private void applyAction(final IntentionActionWithTextCaching cachedAction) {
241     myFinalRunnable = new Runnable() {
242       @Override
243       public void run() {
244         HintManager.getInstance().hideAllHints();
245         ApplicationManager.getApplication().invokeLater(new Runnable() {
246           @Override
247           public void run() {
248             if (myProject.isDisposed()) return;
249             PsiDocumentManager.getInstance(myProject).commitAllDocuments();
250             final PsiFile file = PsiUtilBase.getPsiFileInEditor(myEditor, myProject);
251             if (file == null) {
252               return;
253             }
254
255             ShowIntentionActionsHandler.chooseActionAndInvoke(file, myEditor, cachedAction.getAction(), cachedAction.getText());
256           }
257         });
258       }
259     };
260   }
261
262   IntentionListStep getSubStep(final IntentionActionWithTextCaching action, final String title) {
263     ShowIntentionsPass.IntentionsInfo intentions = new ShowIntentionsPass.IntentionsInfo();
264     for (final IntentionAction optionIntention : action.getOptionIntentions()) {
265       intentions.intentionsToShow.add(new HighlightInfo.IntentionActionDescriptor(optionIntention, getIcon(optionIntention)));
266     }
267     for (final IntentionAction optionFix : action.getOptionErrorFixes()) {
268       intentions.errorFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
269     }
270     for (final IntentionAction optionFix : action.getOptionInspectionFixes()) {
271       intentions.inspectionFixesToShow.add(new HighlightInfo.IntentionActionDescriptor(optionFix, getIcon(optionFix)));
272     }
273
274     return new IntentionListStep(myIntentionHintComponent, intentions,myEditor, myFile, myProject){
275       @Override
276       public String getTitle() {
277         return title;
278       }
279     };
280   }
281
282   private static Icon getIcon(IntentionAction optionIntention) {
283     return optionIntention instanceof Iconable ? ((Iconable)optionIntention).getIcon(0) : null;
284   }
285
286   @Override
287   public boolean hasSubstep(final IntentionActionWithTextCaching action) {
288     return action.getOptionIntentions().size() + action.getOptionErrorFixes().size() > 0;
289   }
290
291   @Override
292   @NotNull
293   public List<IntentionActionWithTextCaching> getValues() {
294     List<IntentionActionWithTextCaching> result = new ArrayList<IntentionActionWithTextCaching>(myCachedErrorFixes);
295     result.addAll(myCachedInspectionFixes);
296     result.addAll(myCachedIntentions);
297     result.addAll(myCachedGutters);
298     result = DumbService.getInstance(myProject).filterByDumbAwareness(result);
299     Collections.sort(result, new Comparator<IntentionActionWithTextCaching>() {
300       @Override
301       public int compare(final IntentionActionWithTextCaching o1, final IntentionActionWithTextCaching o2) {
302         int weight1 = getWeight(o1);
303         int weight2 = getWeight(o2);
304         if (weight1 != weight2) {
305           return weight2 - weight1;
306         }
307         return Comparing.compare(o1.getText(), o2.getText());
308       }
309     });
310     return result;
311   }
312
313   private int getWeight(IntentionActionWithTextCaching action) {
314     IntentionAction a = action.getAction();
315     int group = getGroup(action);
316     if (a instanceof IntentionActionWrapper) {
317       a = ((IntentionActionWrapper)a).getDelegate();
318     }
319     if (a instanceof IntentionWrapper) {
320       a = ((IntentionWrapper)a).getAction();
321     }
322     if (a instanceof HighPriorityAction) {
323       return group + 3;
324     }
325     if (a instanceof LowPriorityAction) {
326       return group - 3;
327     }
328     if (a instanceof SuppressIntentionActionFromFix) {
329       if (((SuppressIntentionActionFromFix)a).isShouldBeAppliedToInjectionHost() == ThreeState.NO) {
330         return group - 1;
331       }
332     }
333     if (a instanceof QuickFixWrapper) {
334       final LocalQuickFix quickFix = ((QuickFixWrapper)a).getFix();
335       if (quickFix instanceof HighPriorityAction) {
336         return group + 3;
337       }
338       if (quickFix instanceof LowPriorityAction) {
339         return group - 3;
340       }
341     }
342     return group;
343   }
344
345   private int getGroup(IntentionActionWithTextCaching action) {
346     if (myCachedErrorFixes.contains(action)) {
347       return 20;
348     }
349     if (myCachedInspectionFixes.contains(action)) {
350       return 10;
351     }
352     if (action.getAction() instanceof EmptyIntentionAction) {
353       return -10;
354     }
355     return 0;
356   }
357
358   @Override
359   @NotNull
360   public String getTextFor(final IntentionActionWithTextCaching action) {
361     final String text = action.getAction().getText();
362     if (LOG.isDebugEnabled() && text.startsWith("<html>")) {
363       LOG.info("IntentionAction.getText() returned HTML: action=" + action + " text=" + text);
364     }
365     return text;
366   }
367
368   @Override
369   public Icon getIconFor(final IntentionActionWithTextCaching value) {
370     if (value.getIcon() != null) {
371       return value.getIcon();
372     }
373
374     final IntentionAction action = value.getAction();
375
376     Object iconable = action;
377     //custom icon
378     if (action instanceof QuickFixWrapper) {
379       iconable = ((QuickFixWrapper)action).getFix();
380     } else if (action instanceof IntentionActionWrapper) {
381       iconable = ((IntentionActionWrapper)action).getDelegate();
382     }
383
384     if (iconable instanceof Iconable) {
385       final Icon icon = ((Iconable)iconable).getIcon(0);
386       if (icon != null) {
387         return icon;
388       }
389     }
390
391     if (mySettings.isShowLightBulb(action)) {
392       return myCachedErrorFixes.contains(value) ? AllIcons.Actions.QuickfixBulb
393              : myCachedInspectionFixes.contains(value) ? AllIcons.Actions.IntentionBulb :
394                AllIcons.Actions.RealIntentionBulb;
395     }
396     else {
397       return myCachedErrorFixes.contains(value) ? AllIcons.Actions.QuickfixOffBulb : AllIcons.Actions.RealIntentionOffBulb;
398     }
399   }
400
401   @Override
402   public void canceled() {
403     if (myIntentionHintComponent != null) {
404       myIntentionHintComponent.canceled(this);
405     }
406   }
407
408   @Override
409   public int getDefaultOptionIndex() { return 0; }
410   @Override
411   public ListSeparator getSeparatorAbove(final IntentionActionWithTextCaching value) {
412     List<IntentionActionWithTextCaching> values = getValues();
413     int index = values.indexOf(value);
414     if (index <= 0) return null;
415     IntentionActionWithTextCaching prev = values.get(index - 1);
416
417     if (getGroup(value) != getGroup(prev)) {
418       return new ListSeparator();
419     }
420     return null;
421   }
422   @Override
423   public boolean isMnemonicsNavigationEnabled() { return false; }
424   @Override
425   public MnemonicNavigationFilter<IntentionActionWithTextCaching> getMnemonicNavigationFilter() { return null; }
426   @Override
427   public boolean isSpeedSearchEnabled() { return true; }
428   @Override
429   public boolean isAutoSelectionEnabled() { return false; }
430   @Override
431   public SpeedSearchFilter<IntentionActionWithTextCaching> getSpeedSearchFilter() { return this; }
432
433   //speed search filter
434   @Override
435   public boolean canBeHidden(final IntentionActionWithTextCaching value) { return true;}
436   @Override
437   public String getIndexedString(final IntentionActionWithTextCaching value) { return getTextFor(value);}
438 }