cleanup
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / ShowIntentionsPass.java
1 /*
2  * Copyright 2000-2016 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.HighlightDisplayLevel;
20 import com.intellij.codeHighlighting.TextEditorHighlightingPass;
21 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
22 import com.intellij.codeInsight.daemon.HighlightDisplayKey;
23 import com.intellij.codeInsight.daemon.impl.analysis.HighlightingLevelManager;
24 import com.intellij.codeInsight.hint.HintManager;
25 import com.intellij.codeInsight.intention.IntentionAction;
26 import com.intellij.codeInsight.intention.IntentionManager;
27 import com.intellij.codeInsight.intention.impl.IntentionHintComponent;
28 import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler;
29 import com.intellij.codeInsight.intention.impl.config.IntentionManagerSettings;
30 import com.intellij.codeInsight.template.impl.TemplateManagerImpl;
31 import com.intellij.codeInsight.template.impl.TemplateState;
32 import com.intellij.codeInspection.*;
33 import com.intellij.codeInspection.actions.CleanupAllIntention;
34 import com.intellij.codeInspection.ex.InspectionToolWrapper;
35 import com.intellij.codeInspection.ex.LocalInspectionToolWrapper;
36 import com.intellij.codeInspection.ex.QuickFixWrapper;
37 import com.intellij.concurrency.JobLauncher;
38 import com.intellij.lang.annotation.HighlightSeverity;
39 import com.intellij.openapi.application.ApplicationManager;
40 import com.intellij.openapi.diagnostic.Attachment;
41 import com.intellij.openapi.diagnostic.Logger;
42 import com.intellij.openapi.editor.Document;
43 import com.intellij.openapi.editor.Editor;
44 import com.intellij.openapi.editor.LogicalPosition;
45 import com.intellij.openapi.editor.RangeMarker;
46 import com.intellij.openapi.editor.ex.EditorEx;
47 import com.intellij.openapi.editor.ex.MarkupModelEx;
48 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
49 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
50 import com.intellij.openapi.fileEditor.FileDocumentManager;
51 import com.intellij.openapi.progress.ProgressIndicator;
52 import com.intellij.openapi.project.DumbService;
53 import com.intellij.openapi.project.Project;
54 import com.intellij.openapi.util.Pair;
55 import com.intellij.openapi.util.Segment;
56 import com.intellij.openapi.util.TextRange;
57 import com.intellij.openapi.vfs.VirtualFile;
58 import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
59 import com.intellij.psi.PsiDocumentManager;
60 import com.intellij.psi.PsiElement;
61 import com.intellij.psi.PsiFile;
62 import com.intellij.psi.PsiWhiteSpace;
63 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
64 import com.intellij.util.CommonProcessors;
65 import com.intellij.util.Processor;
66 import com.intellij.util.Processors;
67 import com.intellij.util.containers.ContainerUtil;
68 import org.jetbrains.annotations.NonNls;
69 import org.jetbrains.annotations.NotNull;
70 import org.jetbrains.annotations.Nullable;
71
72 import java.awt.*;
73 import java.util.ArrayList;
74 import java.util.Iterator;
75 import java.util.List;
76 import java.util.Set;
77
78 public class ShowIntentionsPass extends TextEditorHighlightingPass {
79   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.ShowIntentionsPass");
80   private final Editor myEditor;
81
82   private final PsiFile myFile;
83   private final int myPassIdToShowIntentionsFor;
84   private final IntentionsInfo myIntentionsInfo = new IntentionsInfo();
85   private volatile boolean myShowBulb;
86   private volatile boolean myHasToRecreate;
87
88   @NotNull
89   public static List<HighlightInfo.IntentionActionDescriptor> getAvailableFixes(@NotNull final Editor editor,
90                                                                                 @NotNull final PsiFile file,
91                                                                                 final int passId) {
92     final int offset = ((EditorEx)editor).getExpectedCaretOffset();
93     final Project project = file.getProject();
94
95     List<HighlightInfo> infos = new ArrayList<>();
96     DaemonCodeAnalyzerImpl.processHighlightsNearOffset(editor.getDocument(), project, HighlightSeverity.INFORMATION, offset, true,
97                                                        new CommonProcessors.CollectProcessor<>(infos));
98     List<HighlightInfo.IntentionActionDescriptor> result = new ArrayList<>();
99     infos.forEach(info-> addAvailableFixesForGroups(info, editor, file, result, passId, offset));
100     return result;
101   }
102
103   private static void addAvailableFixesForGroups(@NotNull HighlightInfo info,
104                                                  @NotNull Editor editor,
105                                                  @NotNull PsiFile file,
106                                                  @NotNull List<HighlightInfo.IntentionActionDescriptor> outList,
107                                                  int group,
108                                                  int offset) {
109     if (info.quickFixActionMarkers == null) return;
110     if (group != -1 && group != info.getGroup()) return;
111     boolean fixRangeIsNotEmpty = !info.getFixTextRange().isEmpty();
112     Editor injectedEditor = null;
113     PsiFile injectedFile = null;
114     for (Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker> pair : info.quickFixActionMarkers) {
115       HighlightInfo.IntentionActionDescriptor actionInGroup = pair.first;
116       RangeMarker range = pair.second;
117       if (!range.isValid() || fixRangeIsNotEmpty && isEmpty(range)) continue;
118
119       if (DumbService.isDumb(file.getProject()) && !DumbService.isDumbAware(actionInGroup.getAction())) {
120         continue;
121       }
122
123       int start = range.getStartOffset();
124       int end = range.getEndOffset();
125       final Project project = file.getProject();
126       if (start > offset || offset > end) {
127         continue;
128       }
129       Editor editorToUse;
130       PsiFile fileToUse;
131       if (info.isFromInjection()) {
132         if (injectedEditor == null) {
133           injectedFile = InjectedLanguageUtil.findInjectedPsiNoCommit(file, offset);
134           injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(editor, injectedFile);
135         }
136         editorToUse = injectedEditor;
137         fileToUse = injectedFile;
138       }
139       else {
140         editorToUse = editor;
141         fileToUse = file;
142       }
143       if (actionInGroup.getAction().isAvailable(project, editorToUse, fileToUse)) {
144         outList.add(actionInGroup);
145       }
146     }
147   }
148
149   private static boolean isEmpty(@NotNull Segment segment) {
150     return segment.getEndOffset() <= segment.getStartOffset();
151   }
152
153   public static class IntentionsInfo {
154     public final List<HighlightInfo.IntentionActionDescriptor> intentionsToShow = ContainerUtil.createLockFreeCopyOnWriteList();
155     public final List<HighlightInfo.IntentionActionDescriptor> errorFixesToShow = ContainerUtil.createLockFreeCopyOnWriteList();
156     public final List<HighlightInfo.IntentionActionDescriptor> inspectionFixesToShow = ContainerUtil.createLockFreeCopyOnWriteList();
157     public final List<HighlightInfo.IntentionActionDescriptor> guttersToShow = ContainerUtil.createLockFreeCopyOnWriteList();
158     public final List<HighlightInfo.IntentionActionDescriptor> notificationActionsToShow = ContainerUtil.createLockFreeCopyOnWriteList();
159
160     void filterActions(@Nullable PsiFile psiFile) {
161       IntentionActionFilter[] filters = IntentionActionFilter.EXTENSION_POINT_NAME.getExtensions();
162       filter(intentionsToShow, psiFile, filters);
163       filter(errorFixesToShow, psiFile, filters);
164       filter(inspectionFixesToShow, psiFile, filters);
165       filter(guttersToShow, psiFile, filters);
166       filter(notificationActionsToShow, psiFile, filters);
167     }
168
169     private static void filter(@NotNull List<HighlightInfo.IntentionActionDescriptor> descriptors,
170                                @Nullable PsiFile psiFile,
171                                @NotNull IntentionActionFilter[] filters) {
172       for (Iterator<HighlightInfo.IntentionActionDescriptor> it = descriptors.iterator(); it.hasNext(); ) {
173         HighlightInfo.IntentionActionDescriptor actionDescriptor = it.next();
174         for (IntentionActionFilter filter : filters) {
175           if (!filter.accept(actionDescriptor.getAction(), psiFile)) {
176             it.remove();
177             break;
178           }
179         }
180       }
181     }
182
183     public boolean isEmpty() {
184       return intentionsToShow.isEmpty() && errorFixesToShow.isEmpty() && inspectionFixesToShow.isEmpty() && guttersToShow.isEmpty() && 
185              notificationActionsToShow.isEmpty();
186     }
187
188     @NonNls
189     @Override
190     public String toString() {
191       return
192         "Errors: " + errorFixesToShow + "; " +
193         "Inspection fixes: " + inspectionFixesToShow + "; " +
194         "Intentions: " + intentionsToShow + "; " +
195         "Gutters: " + guttersToShow +
196         "Notifications: " + notificationActionsToShow;
197     }
198   }
199
200   ShowIntentionsPass(@NotNull Project project, @NotNull Editor editor, int passId) {
201     super(project, editor.getDocument(), false);
202     myPassIdToShowIntentionsFor = passId;
203     ApplicationManager.getApplication().assertIsDispatchThread();
204
205     myEditor = editor;
206
207     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
208
209     myFile = documentManager.getPsiFile(myEditor.getDocument());
210     assert myFile != null : FileDocumentManager.getInstance().getFile(myEditor.getDocument());
211   }
212
213   @Override
214   public void doCollectInformation(@NotNull ProgressIndicator progress) {
215     if (!ApplicationManager.getApplication().isUnitTestMode() && !myEditor.getContentComponent().hasFocus()) return;
216     TemplateState state = TemplateManagerImpl.getTemplateState(myEditor);
217     if (state != null && !state.isFinished()) return;
218     DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(myProject);
219     getIntentionActionsToShow();
220     updateActions(codeAnalyzer);
221   }
222
223   @Override
224   public void doApplyInformationToEditor() {
225     ApplicationManager.getApplication().assertIsDispatchThread();
226
227     if (!ApplicationManager.getApplication().isUnitTestMode() && !myEditor.getContentComponent().hasFocus()) return;
228
229     // do not show intentions if caret is outside visible area
230     LogicalPosition caretPos = myEditor.getCaretModel().getLogicalPosition();
231     Rectangle visibleArea = myEditor.getScrollingModel().getVisibleArea();
232     Point xy = myEditor.logicalPositionToXY(caretPos);
233     if (!visibleArea.contains(xy)) return;
234
235     TemplateState state = TemplateManagerImpl.getTemplateState(myEditor);
236     if (myShowBulb && (state == null || state.isFinished()) && !HintManager.getInstance().hasShownHintsThatWillHideByOtherHint(false)) {
237       DaemonCodeAnalyzerImpl codeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(myProject);
238       codeAnalyzer.setLastIntentionHint(myProject, myFile, myEditor, myIntentionsInfo, myHasToRecreate);
239     }
240   }
241
242   private void getIntentionActionsToShow() {
243     getActionsToShow(myEditor, myFile, myIntentionsInfo, myPassIdToShowIntentionsFor);
244
245     if (myIntentionsInfo.isEmpty()) {
246       return;
247     }
248     myShowBulb = !myIntentionsInfo.guttersToShow.isEmpty() || !myIntentionsInfo.notificationActionsToShow.isEmpty() ||
249       ContainerUtil.exists(ContainerUtil.concat(myIntentionsInfo.errorFixesToShow, myIntentionsInfo.inspectionFixesToShow,myIntentionsInfo.intentionsToShow),
250                            descriptor -> IntentionManagerSettings.getInstance().isShowLightBulb(descriptor.getAction()));
251   }
252
253   private static boolean appendCleanupCode(@NotNull List<HighlightInfo.IntentionActionDescriptor> actionDescriptors, @NotNull PsiFile file) {
254     for (HighlightInfo.IntentionActionDescriptor descriptor : actionDescriptors) {
255       if (descriptor.canCleanup(file)) {
256         final ArrayList<IntentionAction> options = new ArrayList<>();
257         options.add(EditCleanupProfileIntentionAction.INSTANCE);
258         options.add(CleanupOnScopeIntention.INSTANCE);
259         actionDescriptors.add(new HighlightInfo.IntentionActionDescriptor(CleanupAllIntention.INSTANCE, options, "Code Cleanup Options"));
260         return true;
261       }
262     }
263     return false;
264   }
265
266   private void updateActions(@NotNull DaemonCodeAnalyzerImpl codeAnalyzer) {
267     IntentionHintComponent hintComponent = codeAnalyzer.getLastIntentionHint();
268     if (!myShowBulb || hintComponent == null || !hintComponent.isForEditor(myEditor)) {
269       return;
270     }
271     IntentionHintComponent.PopupUpdateResult result = hintComponent.updateActions(myIntentionsInfo);
272     if (result == IntentionHintComponent.PopupUpdateResult.HIDE_AND_RECREATE) {
273       // reshow all
274     }
275     else if (result == IntentionHintComponent.PopupUpdateResult.CHANGED_INVISIBLE) {
276       myHasToRecreate = true;
277     }
278   }
279
280   public static void getActionsToShow(@NotNull final Editor hostEditor,
281                                       @NotNull final PsiFile hostFile,
282                                       @NotNull final IntentionsInfo intentions,
283                                       int passIdToShowIntentionsFor) {
284     final PsiElement psiElement = hostFile.findElementAt(hostEditor.getCaretModel().getOffset());
285     LOG.assertTrue(psiElement == null || psiElement.isValid(), psiElement);
286
287     int offset = hostEditor.getCaretModel().getOffset();
288     final Project project = hostFile.getProject();
289
290     List<HighlightInfo.IntentionActionDescriptor> fixes = getAvailableFixes(hostEditor, hostFile, passIdToShowIntentionsFor);
291     final DaemonCodeAnalyzer codeAnalyzer = DaemonCodeAnalyzer.getInstance(project);
292     final Document hostDocument = hostEditor.getDocument();
293     HighlightInfo infoAtCursor = ((DaemonCodeAnalyzerImpl)codeAnalyzer).findHighlightByOffset(hostDocument, offset, true);
294     if (infoAtCursor == null) {
295       intentions.errorFixesToShow.addAll(fixes);
296     }
297     else {
298       final boolean isError = infoAtCursor.getSeverity() == HighlightSeverity.ERROR;
299       for (HighlightInfo.IntentionActionDescriptor fix : fixes) {
300         if (fix.isError() && isError) {
301           intentions.errorFixesToShow.add(fix);
302         }
303         else {
304           intentions.inspectionFixesToShow.add(fix);
305         }
306       }
307     }
308
309     for (final IntentionAction action : IntentionManager.getInstance().getAvailableIntentionActions()) {
310       Pair<PsiFile, Editor> place =
311         ShowIntentionActionsHandler.chooseBetweenHostAndInjected(hostFile, hostEditor,
312                                                                  (psiFile, editor) -> ShowIntentionActionsHandler.availableFor(psiFile, editor, action));
313
314       if (place != null) {
315         List<IntentionAction> enableDisableIntentionAction = new ArrayList<>();
316         enableDisableIntentionAction.add(new IntentionHintComponent.EnableDisableIntentionAction(action));
317         enableDisableIntentionAction.add(new IntentionHintComponent.EditIntentionSettingsAction(action));
318         HighlightInfo.IntentionActionDescriptor descriptor = new HighlightInfo.IntentionActionDescriptor(action, enableDisableIntentionAction, null);
319         if (!fixes.contains(descriptor)) {
320           intentions.intentionsToShow.add(descriptor);
321         }
322       }
323     }
324
325     if (HighlightingLevelManager.getInstance(project).shouldInspect(hostFile)) {
326       PsiElement intentionElement = psiElement;
327       int intentionOffset = offset;
328       if (psiElement instanceof PsiWhiteSpace && offset == psiElement.getTextRange().getStartOffset() && offset > 0) {
329         final PsiElement prev = hostFile.findElementAt(offset - 1);
330         if (prev != null && prev.isValid()) {
331           intentionElement = prev;
332           intentionOffset = offset - 1;
333         }
334       }
335       if (intentionElement != null && intentionElement.getManager().isInProject(intentionElement)) {
336         collectIntentionsFromDoNotShowLeveledInspections(project, hostFile, intentionElement, intentionOffset, intentions);
337       }
338     }
339
340     final int line = hostDocument.getLineNumber(offset);
341     MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(hostDocument, project, true);
342     List<RangeHighlighterEx> result = new ArrayList<>();
343     Processor<RangeHighlighterEx> processor = Processors.cancelableCollectProcessor(result);
344     model.processRangeHighlightersOverlappingWith(hostDocument.getLineStartOffset(line),
345                                                   hostDocument.getLineEndOffset(line),
346                                                   processor);
347
348     GutterIntentionAction.addActions(hostEditor, intentions, project, result);
349
350     boolean cleanup = appendCleanupCode(intentions.inspectionFixesToShow, hostFile);
351     if (!cleanup) {
352       appendCleanupCode(intentions.errorFixesToShow, hostFile);
353     }
354     
355     EditorNotificationActions.collectDescriptorsForEditor(hostEditor, intentions.notificationActionsToShow);
356
357     intentions.filterActions(hostFile);
358   }
359
360   /**
361    * Can be invoked in EDT, each inspection should be fast
362    */
363   private static void collectIntentionsFromDoNotShowLeveledInspections(@NotNull final Project project,
364                                                                        @NotNull final PsiFile hostFile,
365                                                                        PsiElement psiElement,
366                                                                        final int offset,
367                                                                        @NotNull final IntentionsInfo intentions) {
368     if (psiElement != null) {
369       if (!psiElement.isPhysical()) {
370         VirtualFile virtualFile = hostFile.getVirtualFile();
371         String text = hostFile.getText();
372         LOG.error("not physical: '" + psiElement.getText() + "' @" + offset + psiElement.getTextRange() +
373                   " elem:" + psiElement + " (" + psiElement.getClass().getName() + ")" +
374                   " in:" + psiElement.getContainingFile() + " host:" + hostFile + "(" + hostFile.getClass().getName() + ")",
375                   new Attachment(virtualFile != null ? virtualFile.getPresentableUrl() : "null", text != null ? text : "null"));
376       }
377       if (DumbService.isDumb(project)) {
378         return;
379       }
380
381       final List<LocalInspectionToolWrapper> intentionTools = new ArrayList<>();
382       final InspectionProfile profile = InspectionProjectProfileManager.getInstance(project).getInspectionProfile();
383       final InspectionToolWrapper[] tools = profile.getInspectionTools(hostFile);
384       for (InspectionToolWrapper toolWrapper : tools) {
385         if (toolWrapper instanceof LocalInspectionToolWrapper && !((LocalInspectionToolWrapper)toolWrapper).isUnfair()) {
386           final HighlightDisplayKey key = HighlightDisplayKey.find(toolWrapper.getShortName());
387           if (profile.isToolEnabled(key, hostFile) &&
388               HighlightDisplayLevel.DO_NOT_SHOW.equals(profile.getErrorLevel(key, hostFile))) {
389             intentionTools.add((LocalInspectionToolWrapper)toolWrapper);
390           }
391         }
392       }
393
394       if (!intentionTools.isEmpty()) {
395         final List<PsiElement> elements = new ArrayList<>();
396         PsiElement el = psiElement;
397         while (el != null) {
398           elements.add(el);
399           if (el instanceof PsiFile) break;
400           el = el.getParent();
401         }
402
403         final Set<String> dialectIds = InspectionEngine.calcElementDialectIds(elements);
404         final LocalInspectionToolSession session = new LocalInspectionToolSession(hostFile, 0, hostFile.getTextLength());
405         final Processor<LocalInspectionToolWrapper> processor = toolWrapper -> {
406           final LocalInspectionTool localInspectionTool = toolWrapper.getTool();
407           final HighlightDisplayKey key = HighlightDisplayKey.find(toolWrapper.getShortName());
408           final String displayName = toolWrapper.getDisplayName();
409           final ProblemsHolder holder = new ProblemsHolder(InspectionManager.getInstance(project), hostFile, true) {
410             @Override
411             public void registerProblem(@NotNull ProblemDescriptor problemDescriptor) {
412               super.registerProblem(problemDescriptor);
413               if (problemDescriptor instanceof ProblemDescriptorBase) {
414                 final TextRange range = ((ProblemDescriptorBase)problemDescriptor).getTextRange();
415                 if (range != null && range.contains(offset)) {
416                   final QuickFix[] fixes = problemDescriptor.getFixes();
417                   if (fixes != null) {
418                     for (int k = 0; k < fixes.length; k++) {
419                       final IntentionAction intentionAction = QuickFixWrapper.wrap(problemDescriptor, k);
420                       final HighlightInfo.IntentionActionDescriptor actionDescriptor =
421                         new HighlightInfo.IntentionActionDescriptor(intentionAction, null, displayName, null,
422                                                                     key, null, HighlightSeverity.INFORMATION);
423                       intentions.intentionsToShow.add(actionDescriptor);
424                     }
425                   }
426                 }
427               }
428             }
429           };
430           InspectionEngine.createVisitorAndAcceptElements(localInspectionTool, holder, true, session, elements,
431                                                           dialectIds, InspectionEngine.getDialectIdsSpecifiedForTool(toolWrapper));
432           localInspectionTool.inspectionFinished(session, holder);
433           return true;
434         };
435         JobLauncher.getInstance().invokeConcurrentlyUnderProgress(intentionTools, new DaemonProgressIndicator(), false, processor);
436       }
437     }
438   }
439 }
440