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