da055a4d1044dd86d3776eb401756d586773c549
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / ShowIntentionsPass.java
1 /*
2  * Copyright 2000-2013 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.daemon.impl;
18
19 import com.intellij.codeHighlighting.TextEditorHighlightingPass;
20 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
21 import com.intellij.codeInsight.hint.HintManager;
22 import com.intellij.codeInsight.intention.AbstractIntentionAction;
23 import com.intellij.codeInsight.intention.IntentionAction;
24 import com.intellij.codeInsight.intention.IntentionManager;
25 import com.intellij.codeInsight.intention.impl.IntentionHintComponent;
26 import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler;
27 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
28 import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
29 import com.intellij.codeInsight.template.impl.TemplateState;
30 import com.intellij.codeInspection.actions.CleanupAllIntention;
31 import com.intellij.ide.DataManager;
32 import com.intellij.lang.annotation.HighlightSeverity;
33 import com.intellij.openapi.actionSystem.ActionManager;
34 import com.intellij.openapi.actionSystem.AnAction;
35 import com.intellij.openapi.actionSystem.AnActionEvent;
36 import com.intellij.openapi.actionSystem.Presentation;
37 import com.intellij.openapi.application.ApplicationManager;
38 import com.intellij.openapi.diagnostic.Logger;
39 import com.intellij.openapi.editor.Document;
40 import com.intellij.openapi.editor.Editor;
41 import com.intellij.openapi.editor.LogicalPosition;
42 import com.intellij.openapi.editor.RangeMarker;
43 import com.intellij.openapi.editor.markup.GutterIconRenderer;
44 import com.intellij.openapi.fileEditor.FileDocumentManager;
45 import com.intellij.openapi.progress.ProgressIndicator;
46 import com.intellij.openapi.project.Project;
47 import com.intellij.openapi.ui.popup.JBPopupFactory;
48 import com.intellij.openapi.util.Condition;
49 import com.intellij.openapi.util.Pair;
50 import com.intellij.psi.IntentionFilterOwner;
51 import com.intellij.psi.PsiDocumentManager;
52 import com.intellij.psi.PsiElement;
53 import com.intellij.psi.PsiFile;
54 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
55 import com.intellij.ui.awt.RelativePoint;
56 import com.intellij.util.IncorrectOperationException;
57 import com.intellij.util.PairProcessor;
58 import com.intellij.util.Processor;
59 import com.intellij.util.containers.ContainerUtil;
60 import org.jetbrains.annotations.NonNls;
61 import org.jetbrains.annotations.NotNull;
62
63 import java.awt.*;
64 import java.util.ArrayList;
65 import java.util.Collections;
66 import java.util.Iterator;
67 import java.util.List;
68
69 public class ShowIntentionsPass extends TextEditorHighlightingPass {
70   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.ShowIntentionsPass");
71   private final Editor myEditor;
72
73   private final PsiFile myFile;
74   private final int myPassIdToShowIntentionsFor;
75   private final IntentionsInfo myIntentionsInfo = new IntentionsInfo();
76   private volatile boolean myShowBulb;
77   private volatile boolean myHasToRecreate;
78
79   @NotNull
80   public static List<HighlightInfo.IntentionActionDescriptor> getAvailableActions(@NotNull final Editor editor, @NotNull final PsiFile file, final int passId) {
81     final int offset = editor.getCaretModel().getOffset();
82     final Project project = file.getProject();
83
84     final List<HighlightInfo.IntentionActionDescriptor> result = new ArrayList<HighlightInfo.IntentionActionDescriptor>();
85     DaemonCodeAnalyzerImpl.processHighlightsNearOffset(editor.getDocument(), project, HighlightSeverity.INFORMATION, offset, true, new Processor<HighlightInfo>() {
86       @Override
87       public boolean process(HighlightInfo info) {
88         addAvailableActionsForGroups(info, editor, file, result, passId, offset);
89         return true;
90       }
91     });
92     return result;
93   }
94
95   private static void addAvailableActionsForGroups(@NotNull HighlightInfo info,
96                                                    @NotNull Editor editor,
97                                                    @NotNull PsiFile file,
98                                                    @NotNull List<HighlightInfo.IntentionActionDescriptor> outList,
99                                                    int group,
100                                                    int offset) {
101     if (info.quickFixActionMarkers == null) return;
102     if (group != -1 && group != info.getGroup()) return;
103     Editor injectedEditor = null;
104     PsiFile injectedFile = null;
105     for (Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker> pair : info.quickFixActionMarkers) {
106       HighlightInfo.IntentionActionDescriptor actionInGroup = pair.first;
107       RangeMarker range = pair.second;
108       if (!range.isValid()) continue;
109       int start = range.getStartOffset();
110       int end = range.getEndOffset();
111       final Project project = file.getProject();
112       if (start > offset || offset > end) {
113         continue;
114       }
115       Editor editorToUse;
116       PsiFile fileToUse;
117       if (info.isFromInjection()) {
118         if (injectedEditor == null) {
119           injectedFile = InjectedLanguageUtil.findInjectedPsiNoCommit(file, offset);
120           injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(editor, injectedFile);
121         }
122         editorToUse = injectedEditor;
123         fileToUse = injectedFile;
124       }
125       else {
126         editorToUse = editor;
127         fileToUse = file;
128       }
129       if (actionInGroup.getAction().isAvailable(project, editorToUse, fileToUse)) {
130         outList.add(actionInGroup);
131       }
132     }
133   }
134
135   public static class IntentionsInfo {
136     public final List<HighlightInfo.IntentionActionDescriptor> intentionsToShow = ContainerUtil.createLockFreeCopyOnWriteList();
137     public final List<HighlightInfo.IntentionActionDescriptor> errorFixesToShow = ContainerUtil.createLockFreeCopyOnWriteList();
138     public final List<HighlightInfo.IntentionActionDescriptor> inspectionFixesToShow = ContainerUtil.createLockFreeCopyOnWriteList();
139     public final List<HighlightInfo.IntentionActionDescriptor> guttersToShow = ContainerUtil.createLockFreeCopyOnWriteList();
140
141     public void filterActions(@NotNull IntentionFilterOwner.IntentionActionsFilter actionsFilter) {
142       filter(intentionsToShow, actionsFilter);
143       filter(errorFixesToShow, actionsFilter);
144       filter(inspectionFixesToShow, actionsFilter);
145       filter(guttersToShow, actionsFilter);
146     }
147
148     private static void filter(@NotNull List<HighlightInfo.IntentionActionDescriptor> descriptors,
149                                @NotNull IntentionFilterOwner.IntentionActionsFilter actionsFilter) {
150       for (Iterator<HighlightInfo.IntentionActionDescriptor> it = descriptors.iterator(); it.hasNext();) {
151           HighlightInfo.IntentionActionDescriptor actionDescriptor = it.next();
152           if (!actionsFilter.isAvailable(actionDescriptor.getAction())) it.remove();
153         }
154     }
155
156     public boolean isEmpty() {
157       return intentionsToShow.isEmpty() && errorFixesToShow.isEmpty() && inspectionFixesToShow.isEmpty() && guttersToShow.isEmpty();
158     }
159
160     @NonNls
161     @Override
162     public String toString() {
163       return "Intentions: " + intentionsToShow + "; Errors: " + errorFixesToShow + "; Inspection fixes: " + inspectionFixesToShow + "; Gutters: " + guttersToShow;
164     }
165   }
166
167   ShowIntentionsPass(@NotNull Project project, @NotNull Editor editor, int passId) {
168     super(project, editor.getDocument(), false);
169     myPassIdToShowIntentionsFor = passId;
170     ApplicationManager.getApplication().assertIsDispatchThread();
171
172     myEditor = editor;
173
174     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
175
176     myFile = documentManager.getPsiFile(myEditor.getDocument());
177     assert myFile != null : FileDocumentManager.getInstance().getFile(myEditor.getDocument());
178   }
179
180   @Override
181   public void doCollectInformation(@NotNull ProgressIndicator progress) {
182     if (!ApplicationManager.getApplication().isUnitTestMode() && !myEditor.getContentComponent().hasFocus()) return;
183     TemplateState state = TemplateManagerImpl.getTemplateState(myEditor);
184     if (state != null && !state.isFinished()) return;
185     DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(myProject);
186     getIntentionActionsToShow();
187     updateActions(codeAnalyzer);
188   }
189
190   @Override
191   public void doApplyInformationToEditor() {
192     ApplicationManager.getApplication().assertIsDispatchThread();
193
194     if (!ApplicationManager.getApplication().isUnitTestMode() && !myEditor.getContentComponent().hasFocus()) return;
195
196     // do not show intentions if caret is outside visible area
197     LogicalPosition caretPos = myEditor.getCaretModel().getLogicalPosition();
198     Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
199     Point xy = myEditor.logicalPositionToXY(caretPos);
200     if (!visibleArea.contains(xy)) return;
201
202     TemplateState state = TemplateManagerImpl.getTemplateState(myEditor);
203     if (myShowBulb && (state == null || state.isFinished()) && !HintManager.getInstance().hasShownHintsThatWillHideByOtherHint(false)) {
204       DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(myProject);
205       codeAnalyzer.setLastIntentionHint(myProject, myFile, myEditor, myIntentionsInfo, myHasToRecreate);
206     }
207   }
208
209   private void getIntentionActionsToShow() {
210     getActionsToShow(myEditor, myFile, myIntentionsInfo, myPassIdToShowIntentionsFor);
211     if (myFile instanceof IntentionFilterOwner) {
212       final IntentionFilterOwner.IntentionActionsFilter actionsFilter = ((IntentionFilterOwner)myFile).getIntentionActionsFilter();
213       if (actionsFilter == null) return;
214       if (actionsFilter != IntentionFilterOwner.IntentionActionsFilter.EVERYTHING_AVAILABLE) {
215         myIntentionsInfo.filterActions(actionsFilter);
216       }
217     }
218
219     if (myIntentionsInfo.isEmpty()) {
220       return;
221     }
222     myShowBulb = !myIntentionsInfo.guttersToShow.isEmpty() ||
223       ContainerUtil.exists(ContainerUtil.concat(myIntentionsInfo.errorFixesToShow, myIntentionsInfo.inspectionFixesToShow,myIntentionsInfo.intentionsToShow), new Condition<HighlightInfo.IntentionActionDescriptor>() {
224         @Override
225         public boolean value(HighlightInfo.IntentionActionDescriptor descriptor) {
226           return IntentionManagerSettings.getInstance().isShowLightBulb(descriptor.getAction());
227         }
228       });
229   }
230
231   private static boolean appendCleanupCode(final List<HighlightInfo.IntentionActionDescriptor> actionDescriptors, PsiFile file) {
232     for (HighlightInfo.IntentionActionDescriptor descriptor : actionDescriptors) {
233       if (descriptor.canCleanup(file)) {
234         final ArrayList<IntentionAction> options = new ArrayList<IntentionAction>();
235         options.add(EditCleanupProfileIntentionAction.INSTANCE);
236         options.add(CleanupOnScopeIntention.INSTANCE);
237         actionDescriptors.add(new HighlightInfo.IntentionActionDescriptor(CleanupAllIntention.INSTANCE, options, "Code Cleanup Options"));
238         return true;
239       }
240     }
241     return false;
242   }
243
244   private void updateActions(@NotNull DaemonCodeAnalyzerImpl codeAnalyzer) {
245     IntentionHintComponent hintComponent = codeAnalyzer.getLastIntentionHint();
246     if (!myShowBulb || hintComponent == null) {
247       return;
248     }
249     Boolean result = hintComponent.updateActions(myIntentionsInfo);
250     if (result == null) {
251       // reshow all
252     }
253     else if (result == Boolean.FALSE) {
254       myHasToRecreate = true;
255     }
256     else {
257       myShowBulb = false;  // nothing to apply
258     }
259   }
260
261   public static void getActionsToShow(@NotNull final Editor hostEditor,
262                                       @NotNull final PsiFile hostFile,
263                                       @NotNull final IntentionsInfo intentions,
264                                       int passIdToShowIntentionsFor) {
265     final PsiElement psiElement = hostFile.findElementAt(hostEditor.getCaretModel().getOffset());
266     LOG.assertTrue(psiElement == null || psiElement.isValid(), psiElement);
267
268     int offset = hostEditor.getCaretModel().getOffset();
269     Project project = hostFile.getProject();
270
271     List<HighlightInfo.IntentionActionDescriptor> fixes = getAvailableActions(hostEditor, hostFile, passIdToShowIntentionsFor);
272     final DaemonCodeAnalyzer codeAnalyzer = DaemonCodeAnalyzer.getInstance(project);
273     final Document hostDocument = hostEditor.getDocument();
274     HighlightInfo infoAtCursor = ((DaemonCodeAnalyzerImpl)codeAnalyzer).findHighlightByOffset(hostDocument, offset, true);
275     if (infoAtCursor == null) {
276       intentions.errorFixesToShow.addAll(fixes);
277     }
278     else {
279       final boolean isError = infoAtCursor.getSeverity() == HighlightSeverity.ERROR;
280       for (HighlightInfo.IntentionActionDescriptor fix : fixes) {
281         if (fix.notError() || !isError) {
282           intentions.inspectionFixesToShow.add(fix);
283         }
284         else {
285           intentions.errorFixesToShow.add(fix);
286         }
287       }
288     }
289
290     for (final IntentionAction action : IntentionManager.getInstance().getAvailableIntentionActions()) {
291       Pair<PsiFile, Editor> place =
292         ShowIntentionActionsHandler.chooseBetweenHostAndInjected(hostFile, hostEditor, new PairProcessor<PsiFile, Editor>() {
293           @Override
294           public boolean process(PsiFile psiFile, Editor editor) {
295             return ShowIntentionActionsHandler.availableFor(psiFile, editor, action);
296           }
297         });
298
299       if (place != null) {
300         List<IntentionAction> enableDisableIntentionAction = new ArrayList<IntentionAction>();
301         enableDisableIntentionAction.add(new IntentionHintComponent.EnableDisableIntentionAction(action));
302         enableDisableIntentionAction.add(new IntentionHintComponent.EditIntentionSettingsAction(action));
303         HighlightInfo.IntentionActionDescriptor descriptor = new HighlightInfo.IntentionActionDescriptor(action, enableDisableIntentionAction, null);
304         if (!fixes.contains(descriptor)) {
305           intentions.intentionsToShow.add(descriptor);
306         }
307       }
308     }
309
310     final int line = hostDocument.getLineNumber(offset);
311     DaemonCodeAnalyzerEx.processHighlights(hostDocument, project, null,
312                                            hostDocument.getLineStartOffset(line),
313                                            hostDocument.getLineEndOffset(line), new Processor<HighlightInfo>() {
314       @Override
315       public boolean process(HighlightInfo info) {
316         final GutterIconRenderer renderer = (GutterIconRenderer)info.getGutterIconRenderer();
317         if (renderer == null) {
318           return true;
319         }
320         final AnAction action = renderer.getClickAction();
321         if (action == null) {
322           return true;
323         }
324         final String text = renderer.getTooltipText();
325         if (text == null) {
326           return true;
327         }
328         final IntentionAction actionAdapter = new AbstractIntentionAction() {
329           @Override
330           public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
331             final RelativePoint relativePoint = JBPopupFactory.getInstance().guessBestPopupLocation(editor);
332             action.actionPerformed(
333               new AnActionEvent(relativePoint.toMouseEvent(), DataManager.getInstance().getDataContext(), text, new Presentation(),
334                                 ActionManager.getInstance(), 0));
335           }
336
337           @Override
338           @NotNull
339           public String getText() {
340             return text;
341           }
342         };
343         intentions.guttersToShow.add(
344           new HighlightInfo.IntentionActionDescriptor(actionAdapter, Collections.<IntentionAction>emptyList(), text, renderer.getIcon()));
345         return true;
346       }
347     });
348
349     boolean cleanup = appendCleanupCode(intentions.inspectionFixesToShow, hostFile);
350     if (!cleanup) {
351       appendCleanupCode(intentions.errorFixesToShow, hostFile);
352     }
353   }
354 }
355