Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInspection / ui / InspectionTree.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 /*
18  * Created by IntelliJ IDEA.
19  * User: max
20  * Date: Nov 4, 2001
21  * Time: 5:19:35 PM
22  */
23 package com.intellij.codeInspection.ui;
24
25 import com.intellij.codeInspection.CommonProblemDescriptor;
26 import com.intellij.codeInspection.ProblemDescriptor;
27 import com.intellij.codeInspection.ex.GlobalInspectionContextImpl;
28 import com.intellij.codeInspection.ex.InspectionToolWrapper;
29 import com.intellij.codeInspection.reference.RefEntity;
30 import com.intellij.openapi.application.ApplicationManager;
31 import com.intellij.openapi.diagnostic.Logger;
32 import com.intellij.openapi.project.Project;
33 import com.intellij.openapi.vfs.VirtualFile;
34 import com.intellij.profile.codeInspection.ui.inspectionsTree.InspectionsConfigTreeComparator;
35 import com.intellij.psi.PsiElement;
36 import com.intellij.psi.util.PsiUtilCore;
37 import com.intellij.ui.TreeSpeedSearch;
38 import com.intellij.ui.treeStructure.Tree;
39 import com.intellij.util.ArrayUtil;
40 import com.intellij.util.SmartList;
41 import com.intellij.util.containers.ContainerUtil;
42 import com.intellij.util.containers.MultiMap;
43 import com.intellij.util.ui.UIUtil;
44 import com.intellij.util.ui.tree.TreeUtil;
45 import org.jetbrains.annotations.NotNull;
46 import org.jetbrains.annotations.Nullable;
47
48 import javax.swing.event.TreeExpansionEvent;
49 import javax.swing.event.TreeWillExpandListener;
50 import javax.swing.tree.DefaultTreeModel;
51 import javax.swing.tree.ExpandVetoException;
52 import javax.swing.tree.TreeNode;
53 import javax.swing.tree.TreePath;
54 import java.util.*;
55 import java.util.stream.Collectors;
56 import java.util.stream.Stream;
57
58 public class InspectionTree extends Tree {
59   private static final Logger LOG = Logger.getInstance(InspectionTree.class);
60   private static final Comparator<CommonProblemDescriptor> DESCRIPTOR_COMPARATOR = (c1, c2) -> {
61     if (c1 instanceof ProblemDescriptor && c2 instanceof ProblemDescriptor) {
62       return PsiUtilCore.compareElementsByPosition(((ProblemDescriptor)c2).getPsiElement(),
63                                                    ((ProblemDescriptor)c1).getPsiElement());
64     }
65     return c1.getDescriptionTemplate().compareTo(c2.getDescriptionTemplate());
66   };
67
68   @NotNull private final GlobalInspectionContextImpl myContext;
69   @NotNull private final ExcludedInspectionTreeNodesManager myExcludedManager;
70   @NotNull private InspectionTreeState myState = new InspectionTreeState();
71   private boolean myQueueUpdate;
72
73   public InspectionTree(@NotNull Project project,
74                         @NotNull GlobalInspectionContextImpl context,
75                         @NotNull InspectionResultsView view) {
76     setModel(new DefaultTreeModel(new InspectionRootNode(project, new InspectionTreeUpdater(view))));
77     myContext = context;
78     myExcludedManager = view.getExcludedManager();
79
80     setCellRenderer(new InspectionTreeCellRenderer(view));
81     setRootVisible(false);
82     setShowsRootHandles(true);
83     UIUtil.setLineStyleAngled(this);
84     addTreeWillExpandListener(new ExpandListener());
85
86     myState.getExpandedUserObjects().add(project);
87
88     TreeUtil.installActions(this);
89     new TreeSpeedSearch(this, o -> InspectionsConfigTreeComparator.getDisplayTextToSort(o.getLastPathComponent().toString()));
90
91     addTreeSelectionListener(e -> {
92       TreePath newSelection = e.getNewLeadSelectionPath();
93       if (newSelection != null && !isUnderQueueUpdate()) {
94         myState.setSelectionPath(newSelection);
95       }
96     });
97   }
98
99   public void setQueueUpdate(boolean queueUpdate) {
100     myQueueUpdate = queueUpdate;
101   }
102
103   public boolean isUnderQueueUpdate() {
104     return myQueueUpdate;
105   }
106
107   public void removeAllNodes() {
108     getRoot().removeAllChildren();
109     ApplicationManager.getApplication().invokeLater(() -> nodeStructureChanged(getRoot()));
110   }
111
112   public InspectionTreeNode getRoot() {
113     return (InspectionTreeNode)getModel().getRoot();
114   }
115
116   @Nullable
117   public String[] getSelectedGroupPath() {
118     final TreePath[] paths = getSelectionPaths();
119     if (paths == null) return null;
120     final TreePath commonPath = TreeUtil.findCommonPath(paths);
121     for (Object n : commonPath.getPath()) {
122       if (n instanceof InspectionGroupNode) {
123         return ((InspectionGroupNode)n).getGroupPath();
124       }
125     }
126     return null;
127   }
128
129   @Nullable
130   public InspectionToolWrapper getSelectedToolWrapper(boolean allowDummy) {
131     final TreePath[] paths = getSelectionPaths();
132     if (paths == null) return null;
133     InspectionToolWrapper toolWrapper = null;
134     for (TreePath path : paths) {
135       Object[] nodes = path.getPath();
136       for (int j = nodes.length - 1; j >= 0; j--) {
137         Object node = nodes[j];
138         if (node instanceof InspectionGroupNode) {
139           return null;
140         }
141         if (node instanceof InspectionNode) {
142           InspectionToolWrapper wrapper = ((InspectionNode)node).getToolWrapper();
143           if (!allowDummy && getContext().getPresentation(wrapper).isDummy()) {
144             continue;
145           }
146           if (toolWrapper == null) {
147             toolWrapper = wrapper;
148           }
149           else if (toolWrapper != wrapper) {
150             return null;
151           }
152           break;
153         }
154       }
155     }
156
157     return toolWrapper;
158   }
159
160   @Nullable
161   public RefEntity getCommonSelectedElement() {
162     final Object node = getCommonSelectedNode();
163     return node instanceof RefElementNode ? ((RefElementNode)node).getElement() : null;
164   }
165
166   @Nullable
167   private Object getCommonSelectedNode() {
168     final TreePath[] paths = getSelectionPaths();
169     if (paths == null) return null;
170     final Object[][] resolvedPaths = new Object[paths.length][];
171     for (int i = 0; i < paths.length; i++) {
172       TreePath path = paths[i];
173       resolvedPaths[i] = path.getPath();
174     }
175
176     Object currentCommonNode = null;
177     for (int i = 0; i < resolvedPaths[0].length; i++) {
178       final Object currentNode = resolvedPaths[0][i];
179       for (int j = 1; j < resolvedPaths.length; j++) {
180         final Object o = resolvedPaths[j][i];
181         if (!o.equals(currentNode)) {
182           return currentCommonNode;
183         }
184       }
185       currentCommonNode = currentNode;
186     }
187     return currentCommonNode;
188   }
189
190   @NotNull
191   public RefEntity[] getSelectedElements() {
192     TreePath[] selectionPaths = getSelectionPaths();
193     if (selectionPaths != null) {
194       InspectionToolWrapper toolWrapper = getSelectedToolWrapper(true);
195       if (toolWrapper == null) return RefEntity.EMPTY_ELEMENTS_ARRAY;
196
197       Set<RefEntity> result = new LinkedHashSet<RefEntity>();
198       for (TreePath selectionPath : selectionPaths) {
199         final InspectionTreeNode node = (InspectionTreeNode)selectionPath.getLastPathComponent();
200         addElementsInNode(node, result);
201       }
202       return ArrayUtil.reverseArray(result.toArray(new RefEntity[result.size()]));
203     }
204     return RefEntity.EMPTY_ELEMENTS_ARRAY;
205   }
206
207   private static void addElementsInNode(InspectionTreeNode node, Set<RefEntity> out) {
208     if (!node.isValid()) return;
209     if (node instanceof RefElementNode) {
210       final RefEntity element = ((RefElementNode)node).getElement();
211       out.add(element);
212     }
213     if (node instanceof ProblemDescriptionNode) {
214       final RefEntity element = ((ProblemDescriptionNode)node).getElement();
215       out.add(element);
216     }
217     final Enumeration children = node.children();
218     while (children.hasMoreElements()) {
219       InspectionTreeNode child = (InspectionTreeNode)children.nextElement();
220       addElementsInNode(child, out);
221     }
222   }
223
224   public CommonProblemDescriptor[] getSelectedDescriptors() {
225     return getSelectedDescriptors(false, null);
226   }
227
228   public CommonProblemDescriptor[] getSelectedDescriptors(boolean sortedByPosition, @Nullable Set<VirtualFile> readOnlyFilesSink) {
229     final TreePath[] paths = getSelectionPaths();
230     if (paths == null) return CommonProblemDescriptor.EMPTY_ARRAY;
231     final TreePath[] selectionPaths = TreeUtil.selectMaximals(paths);
232     final LinkedHashSet<CommonProblemDescriptor> descriptors = new LinkedHashSet<CommonProblemDescriptor>();
233
234     MultiMap<Object, ProblemDescriptionNode> parentToChildNode = new MultiMap<>();
235     final List<InspectionTreeNode> nonDescriptorNodes = new SmartList<>();
236     for (TreePath path : selectionPaths) {
237       final Object[] pathAsArray = path.getPath();
238       final int length = pathAsArray.length;
239       final Object node = pathAsArray[length - 1];
240       if (node instanceof ProblemDescriptionNode) {
241         if (isNodeValidAndIncluded((ProblemDescriptionNode)node)) {
242           if (length >= 2) {
243             parentToChildNode.putValue(pathAsArray[length - 2], (ProblemDescriptionNode)node);
244           } else {
245             parentToChildNode.putValue(node, (ProblemDescriptionNode)node);
246           }
247         }
248       } else {
249         nonDescriptorNodes.add((InspectionTreeNode)node);
250       }
251     }
252
253     for (InspectionTreeNode node : nonDescriptorNodes) {
254       processChildDescriptorsDeep(node, descriptors, sortedByPosition, readOnlyFilesSink);
255     }
256
257     for (Map.Entry<Object, Collection<ProblemDescriptionNode>> entry : parentToChildNode.entrySet()) {
258       final Collection<ProblemDescriptionNode> siblings = entry.getValue();
259       if (siblings.size() == 1) {
260         final ProblemDescriptionNode descriptorNode = ContainerUtil.getFirstItem(siblings);
261         LOG.assertTrue(descriptorNode != null);
262         CommonProblemDescriptor descriptor = descriptorNode.getDescriptor();
263         if (descriptor != null) {
264           descriptors.add(descriptor);
265           if (readOnlyFilesSink != null) {
266             checkDescriptorFileIsWritable(descriptor, readOnlyFilesSink);
267           }
268         }
269       } else {
270         Stream<CommonProblemDescriptor> descriptorStream = siblings.stream().map(ProblemDescriptionNode::getDescriptor);
271         if (sortedByPosition) {
272           descriptorStream = descriptorStream.sorted(DESCRIPTOR_COMPARATOR);
273         }
274         descriptorStream = descriptorStream.filter(descriptors::add);
275         if (readOnlyFilesSink != null) {
276           checkDescriptorsFileIsWritable(descriptorStream.collect(Collectors.toList()), readOnlyFilesSink);
277         }
278       }
279     }
280
281     return descriptors.toArray(new CommonProblemDescriptor[descriptors.size()]);
282   }
283
284   public boolean areDescriptorNodesSelected() {
285     final TreePath[] paths = getSelectionPaths();
286     if (paths == null) return false;
287     for (TreePath path : paths) {
288       if (!(path.getLastPathComponent() instanceof ProblemDescriptionNode)) {
289         return false;
290       }
291     }
292     return true;
293   }
294
295   public int getSelectedProblemCount() {
296     if (getSelectionCount() == 0) return 0;
297     final TreePath[] paths = getSelectionPaths();
298     LOG.assertTrue(paths != null);
299     Set<InspectionTreeNode> result = new HashSet<>();
300     MultiMap<InspectionTreeNode, InspectionTreeNode> rootDependencies = new MultiMap<>();
301     for (TreePath path : paths) {
302
303       final InspectionTreeNode node = (InspectionTreeNode)path.getLastPathComponent();
304       final Collection<InspectionTreeNode> visitedChildren = rootDependencies.get(node);
305       for (InspectionTreeNode child : visitedChildren) {
306         result.remove(child);
307       }
308
309       boolean needToAdd = true;
310       for (int i = 0; i < path.getPathCount() - 1; i++) {
311         final InspectionTreeNode parent = (InspectionTreeNode) path.getPathComponent(i);
312         rootDependencies.putValue(parent, node);
313         if (result.contains(parent)) {
314           needToAdd = false;
315           break;
316         }
317       }
318
319       if (needToAdd) {
320         result.add(node);
321       }
322     }
323
324     int count = 0;
325     for (InspectionTreeNode node : result) {
326       count += node.getProblemCount();
327     }
328     return count;
329   }
330
331   private void processChildDescriptorsDeep(InspectionTreeNode node,
332                                            LinkedHashSet<CommonProblemDescriptor> descriptors,
333                                            boolean sortedByPosition,
334                                            @Nullable Set<VirtualFile> readOnlyFilesSink) {
335     List<CommonProblemDescriptor> descriptorChildren = null;
336     for (int i = 0; i < node.getChildCount(); i++) {
337       final TreeNode child = node.getChildAt(i);
338       if (child instanceof ProblemDescriptionNode) {
339         if (isNodeValidAndIncluded((ProblemDescriptionNode)child)) {
340           if (sortedByPosition) {
341             if (descriptorChildren == null) {
342               descriptorChildren = new ArrayList<>();
343             }
344             descriptorChildren.add(((ProblemDescriptionNode)child).getDescriptor());
345           } else {
346             descriptors.add(((ProblemDescriptionNode)child).getDescriptor());
347           }
348         }
349       }
350       else {
351         processChildDescriptorsDeep((InspectionTreeNode)child, descriptors, sortedByPosition, readOnlyFilesSink);
352       }
353     }
354
355     if (descriptorChildren != null) {
356       if (descriptorChildren.size() > 1) {
357         Collections.sort(descriptorChildren, DESCRIPTOR_COMPARATOR);
358       }
359       if (readOnlyFilesSink != null) {
360         checkDescriptorsFileIsWritable(descriptorChildren, readOnlyFilesSink);
361       }
362
363       descriptors.addAll(descriptorChildren);
364     }
365   }
366
367   private boolean isNodeValidAndIncluded(ProblemDescriptionNode node) {
368     return node.isValid() &&
369            !node.isExcluded(myExcludedManager) &&
370            !node.isAlreadySuppressedFromView() &&
371            !node.isQuickFixAppliedFromView();
372   }
373
374   private void nodeStructureChanged(InspectionTreeNode node) {
375     ((DefaultTreeModel)getModel()).nodeStructureChanged(node);
376   }
377
378   public void queueUpdate() {
379     ((InspectionRootNode) getRoot()).getUpdater().update(null, true);
380   }
381
382   public void restoreExpansionAndSelection(boolean treeNodesMightChange) {
383     myState.restoreExpansionAndSelection(this, treeNodesMightChange);
384   }
385
386   public void setState(@NotNull InspectionTreeState state) {
387     myState = state;
388   }
389
390   public InspectionTreeState getTreeState() {
391     return myState;
392   }
393
394   public void setTreeState(@NotNull InspectionTreeState treeState) {
395     myState = treeState;
396   }
397
398   private class ExpandListener implements TreeWillExpandListener {
399     @Override
400     public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
401       final InspectionTreeNode node = (InspectionTreeNode)event.getPath().getLastPathComponent();
402       myState.getExpandedUserObjects().add(node.getUserObject());
403     }
404
405     @Override
406     public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
407       InspectionTreeNode node = (InspectionTreeNode)event.getPath().getLastPathComponent();
408       myState.getExpandedUserObjects().remove(node.getUserObject());
409     }
410   }
411
412   @NotNull
413   public GlobalInspectionContextImpl getContext() {
414     return myContext;
415   }
416
417   private static void checkDescriptorsFileIsWritable(@NotNull Collection<CommonProblemDescriptor> descriptors, @NotNull Set<VirtualFile> readOnlySink) {
418     for (CommonProblemDescriptor descriptor : descriptors) {
419       if (checkDescriptorFileIsWritable(descriptor, readOnlySink)) {
420         return;
421       }
422     }
423   }
424
425   private static boolean checkDescriptorFileIsWritable(@NotNull CommonProblemDescriptor descriptor, @NotNull Set<VirtualFile> readOnlySink) {
426     if (descriptor instanceof ProblemDescriptor) {
427       PsiElement psiElement = ((ProblemDescriptor)descriptor).getPsiElement();
428       if (psiElement != null && !psiElement.isWritable()) {
429         readOnlySink.add(psiElement.getContainingFile().getVirtualFile());
430         return true;
431       }
432     }
433     return false;
434   }
435 }