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