diff --git a/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java b/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java index 7be2c2732d74..1d72a9f930f9 100644 --- a/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java +++ b/platform/platform-tests/testSrc/com/intellij/usages/impl/UsageViewTest.java @@ -20,6 +20,10 @@ import com.intellij.find.findUsages.FindUsagesHandler; import com.intellij.find.findUsages.FindUsagesManager; import com.intellij.find.findUsages.PsiElement2UsageTargetAdapter; import com.intellij.find.impl.FindManagerImpl; +import com.intellij.ide.actions.exclusion.ExclusionHandler; +import com.intellij.ide.impl.TypeSafeDataProviderAdapter; +import com.intellij.openapi.actionSystem.DataProvider; +import com.intellij.openapi.actionSystem.TypeSafeDataProvider; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Document; @@ -38,7 +42,11 @@ import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCa import com.intellij.usageView.UsageInfo; import com.intellij.usages.*; import com.intellij.util.ui.UIUtil; +import com.intellij.util.ui.tree.TreeUtil; +import javax.swing.*; +import javax.swing.tree.TreeNode; +import java.util.HashSet; import java.util.Set; /** @@ -158,4 +166,101 @@ public class UsageViewTest extends LightPlatformCodeInsightFixtureTestCase { Set usages = usageView.getUsages(); assertEquals(2, usages.size()); } + + public void testExcludeUsageMustExcludeChildrenAndParents() throws Exception { + PsiFile psiFile = myFixture.addFileToProject("X.java", "public class X{ int xxx; } //comment"); + Usage usage = new UsageInfo2UsageAdapter(new UsageInfo(psiFile, psiFile.getText().indexOf("xxx"), StringUtil.indexOfSubstringEnd(psiFile.getText(),"xxx"))); + + UsageViewImpl usageView = + (UsageViewImpl)UsageViewManager.getInstance(getProject()).createUsageView(UsageTarget.EMPTY_ARRAY, new Usage[]{usage}, new UsageViewPresentation(), null); + Disposer.register(myFixture.getTestRootDisposable(), usageView); + + usageView.excludeUsages(new Usage[]{usage}); + UIUtil.dispatchAllInvocationEvents(); + + Set excluded = new HashSet<>(); + Node[] usageNode = new Node[1]; + TreeUtil.traverse(usageView.getRoot(), node -> { + if (((Node)node).isExcluded()) { + excluded.add((Node)node); + } + if (node instanceof UsageNode && ((UsageNode)node).getUsage() == usage) { + usageNode[0] = (UsageNode)node; + } + return true; + }); + + Set expectedExcluded = new HashSet<>(); + for (TreeNode n = usageNode[0]; n != usageView.getRoot(); n = n.getParent()) { + expectedExcluded.add((Node)n); + } + assertEquals(expectedExcluded, excluded); + + + usageView.includeUsages(new Usage[]{usage}); + UIUtil.dispatchAllInvocationEvents(); + + excluded.clear(); + TreeUtil.traverse(usageView.getRoot(), node -> { + if (((Node)node).isExcluded()) { + excluded.add((Node)node); + } + return true; + }); + + assertEmpty(excluded); + } + + public void testExcludeNodeMustExcludeChildrenAndParents() throws Exception { + PsiFile psiFile = myFixture.addFileToProject("X.java", "public class X{ int xxx; } //comment"); + Usage usage = new UsageInfo2UsageAdapter(new UsageInfo(psiFile, psiFile.getText().indexOf("xxx"), StringUtil.indexOfSubstringEnd(psiFile.getText(),"xxx"))); + + UsageViewImpl usageView = + (UsageViewImpl)UsageViewManager.getInstance(getProject()).createUsageView(UsageTarget.EMPTY_ARRAY, new Usage[]{usage}, new UsageViewPresentation(), null); + Disposer.register(myFixture.getTestRootDisposable(), usageView); + UIUtil.dispatchAllInvocationEvents(); + + Node[] usageNode = new Node[1]; + TreeUtil.traverse(usageView.getRoot(), node -> { + if (node instanceof UsageNode && ((UsageNode)node).getUsage() == usage) { + usageNode[0] = (UsageNode)node; + } + return true; + }); + Node nodeToExclude = (Node)usageNode[0].getParent(); + + JComponent component = usageView.getComponent(); + DataProvider provider = new TypeSafeDataProviderAdapter((TypeSafeDataProvider)component); + ExclusionHandler exclusionHandler = (ExclusionHandler)provider.getData(ExclusionHandler.EXCLUSION_HANDLER.getName()); + exclusionHandler.excludeNode(nodeToExclude); + UIUtil.dispatchAllInvocationEvents(); + + Set excluded = new HashSet<>(); + TreeUtil.traverse(usageView.getRoot(), node -> { + if (((Node)node).isExcluded()) { + excluded.add((Node)node); + } + return true; + }); + + Set expectedExcluded = new HashSet<>(); + for (TreeNode n = usageNode[0]; n != usageView.getRoot(); n = n.getParent()) { + expectedExcluded.add((Node)n); + } + assertEquals(expectedExcluded, excluded); + + + exclusionHandler.includeNode(nodeToExclude); + UIUtil.dispatchAllInvocationEvents(); + + excluded.clear(); + TreeUtil.traverse(usageView.getRoot(), node -> { + if (((Node)node).isExcluded()) { + excluded.add((Node)node); + } + return true; + }); + + assertEmpty(excluded); + } } diff --git a/platform/usageView/src/com/intellij/usages/impl/Node.java b/platform/usageView/src/com/intellij/usages/impl/Node.java index 0082eceff891..8c531366811a 100644 --- a/platform/usageView/src/com/intellij/usages/impl/Node.java +++ b/platform/usageView/src/com/intellij/usages/impl/Node.java @@ -31,20 +31,21 @@ public abstract class Node extends DefaultMutableTreeNode { private int myCachedTextHash; private byte myCachedFlags; // bit packed flags below: - private static final byte INVALID_MASK = 1; - private static final byte READ_ONLY_MASK = 1<<1; - private static final byte READ_ONLY_COMPUTED_MASK = 1<<2; static final byte EXCLUDED_MASK = 1<<3; private static final byte UPDATED_MASK = 1<<4; - @MagicConstant(intValues = {INVALID_MASK, READ_ONLY_MASK, READ_ONLY_COMPUTED_MASK, EXCLUDED_MASK, UPDATED_MASK}) + private static final byte CACHED_INVALID_MASK = 1; + private static final byte CACHED_READ_ONLY_MASK = 1 << 1; + private static final byte READ_ONLY_COMPUTED_MASK = 1<<2; + + @MagicConstant(intValues = {CACHED_INVALID_MASK, CACHED_READ_ONLY_MASK, READ_ONLY_COMPUTED_MASK, EXCLUDED_MASK, UPDATED_MASK}) private @interface FlagConstant {} - boolean isFlagSet(@FlagConstant byte mask) { + private boolean isFlagSet(@FlagConstant byte mask) { return BitUtil.isSet(myCachedFlags, mask); } - void setFlag(@FlagConstant byte mask, boolean value) { + private void setFlag(@FlagConstant byte mask, boolean value) { myCachedFlags = BitUtil.set(myCachedFlags, mask, value); } @@ -69,19 +70,19 @@ public abstract class Node extends DefaultMutableTreeNode { protected abstract String getText(@NotNull UsageView view); public final boolean isValid() { - return !isFlagSet(INVALID_MASK); + return !isFlagSet(CACHED_INVALID_MASK); } public final boolean isReadOnly() { boolean result; boolean computed = isFlagSet(READ_ONLY_COMPUTED_MASK); if (computed) { - result = isFlagSet(READ_ONLY_MASK); + result = isFlagSet(CACHED_READ_ONLY_MASK); } else { result = isDataReadOnly(); setFlag(READ_ONLY_COMPUTED_MASK, true); - setFlag(READ_ONLY_MASK, result); + setFlag(CACHED_READ_ONLY_MASK, result); } return result; } @@ -93,20 +94,16 @@ public abstract class Node extends DefaultMutableTreeNode { final synchronized void update(@NotNull UsageView view, @NotNull Consumer edtNodeChangedQueue) { boolean isDataValid = isDataValid(); boolean isReadOnly = isDataReadOnly(); - boolean isExcluded = isDataExcluded(); String text = getText(view); boolean cachedValid = isValid(); - boolean cachedReadOnly = isFlagSet(READ_ONLY_MASK); - boolean cachedExcluded = isFlagSet(EXCLUDED_MASK); + boolean cachedReadOnly = isFlagSet(CACHED_READ_ONLY_MASK); if (isDataValid != cachedValid || isReadOnly != cachedReadOnly || - isExcluded != cachedExcluded || myCachedTextHash != text.hashCode()) { - setFlag(INVALID_MASK, !isDataValid); - setFlag(READ_ONLY_MASK, isReadOnly); - setFlag(EXCLUDED_MASK, isExcluded); + setFlag(CACHED_INVALID_MASK, !isDataValid); + setFlag(CACHED_READ_ONLY_MASK, isReadOnly); myCachedTextHash = text.hashCode(); updateNotify(); @@ -131,8 +128,13 @@ public abstract class Node extends DefaultMutableTreeNode { // same as DefaultMutableTreeNode.insert() except it doesn't try to remove the newChild from its parent since we know it's new void insertNewNode(@NotNull Node newChild, int childIndex) { if (children == null) { - children = new Vector(); + children = new Vector(); } children.insertElementAt(newChild, childIndex); } + + void setExcluded(boolean excluded, @NotNull Consumer edtNodeChangedQueue) { + setFlag(EXCLUDED_MASK, excluded); + edtNodeChangedQueue.consume(this); + } } diff --git a/platform/usageView/src/com/intellij/usages/impl/UsageNode.java b/platform/usageView/src/com/intellij/usages/impl/UsageNode.java index 22e2feec3e60..73f7072a1059 100644 --- a/platform/usageView/src/com/intellij/usages/impl/UsageNode.java +++ b/platform/usageView/src/com/intellij/usages/impl/UsageNode.java @@ -28,6 +28,7 @@ import java.util.Arrays; */ public class UsageNode extends Node implements Comparable, Navigatable { @Deprecated + // todo remove in 2018.1 public UsageNode(@NotNull Usage usage, UsageViewTreeModelBuilder model) { this(null, usage); } @@ -98,8 +99,4 @@ public class UsageNode extends Node implements Comparable, Navigatabl return Arrays.asList(getUsage().getPresentation().getText()).toString(); } } - - void setUsageExcluded(boolean usageExcluded) { - setFlag(EXCLUDED_MASK, usageExcluded); - } } diff --git a/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java b/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java index fee94e7dac1f..f362bd7f4aa2 100644 --- a/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java +++ b/platform/usageView/src/com/intellij/usages/impl/UsageViewImpl.java @@ -56,10 +56,7 @@ import com.intellij.usageView.UsageViewBundle; import com.intellij.usageView.UsageViewManager; import com.intellij.usages.*; import com.intellij.usages.rules.*; -import com.intellij.util.Alarm; -import com.intellij.util.Consumer; -import com.intellij.util.EditSourceOnDoubleClickHandler; -import com.intellij.util.ReflectionUtil; +import com.intellij.util.*; import com.intellij.util.concurrency.EdtExecutorService; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.LinkedMultiMap; @@ -90,6 +87,8 @@ import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author max @@ -300,16 +299,38 @@ public class UsageViewImpl implements UsageView { @Override public void excludeNode(@NotNull DefaultMutableTreeNode node) { - final HashSet usages = new HashSet<>(); - collectUsages(node, usages); - excludeUsages(usages.toArray(new Usage[usages.size()])); + Set nodes = new HashSet<>(); + collectAllChildNodes(node, nodes); + collectParentNodes(node, nodes, true); + setExcludeNodes(nodes, true); + } + + // include the parent if its all children (except the "node" itself) excluded flags are "almostAllChildrenExcluded" + private void collectParentNodes(DefaultMutableTreeNode node, Set nodes, boolean almostAllChildrenExcluded) { + TreeNode parent = node.getParent(); + if (parent == myRoot || !(parent instanceof GroupNode)) return; + GroupNode parentNode = (GroupNode)parent; + List otherNodes = + parentNode.getChildren().stream().filter(n -> n.isExcluded() != almostAllChildrenExcluded).collect(Collectors.toList()); + if (otherNodes.size() == 1 && otherNodes.get(0) == node) { + nodes.add(parentNode); + collectParentNodes(parentNode, nodes, almostAllChildrenExcluded); + } + } + + private void setExcludeNodes(@NotNull Set nodes, boolean excluded) { + for (Node node : nodes) { + node.setExcluded(excluded, edtNodeChangedQueue); + } + updateImmediatelyNodesUpToRoot(nodes); } @Override public void includeNode(@NotNull DefaultMutableTreeNode node) { - final HashSet usages = new HashSet<>(); - collectUsages(node, usages); - includeUsages(usages.toArray(new Usage[usages.size()])); + Set nodes = new HashSet<>(); + collectAllChildNodes(node, nodes); + collectParentNodes(node, nodes, false); + setExcludeNodes(nodes, false); } @Override @@ -1085,13 +1106,7 @@ public class UsageViewImpl implements UsageView { @Override public void removeUsagesBulk(@NotNull Collection usages) { - final Set nodes = new THashSet<>(usages.size()); - for (Usage usage : usages) { - UsageNode node = myUsageNodes.remove(usage); - if (node != null && node != NULL_NODE) { - nodes.add(node); - } - } + Set nodes = usagesToNodes(usages.stream()).collect(Collectors.toSet()); if (!nodes.isEmpty() && !myPresentation.isDetachedMode()) { UIUtil.invokeLaterIfNeeded(() -> { if (isDisposed) return; @@ -1112,44 +1127,30 @@ public class UsageViewImpl implements UsageView { @Override public void includeUsages(@NotNull Usage[] usages) { - List nodes = new ArrayList<>(usages.length); - for (Usage usage : usages) { - final UsageNode node = myUsageNodes.get(usage); - if (node != NULL_NODE && node != null) { - node.setUsageExcluded(false); - nodes.add(node); - } - } - updateImmediatelyNodesUpToRoot(nodes); + usagesToNodes(Arrays.stream(usages)) + .forEach(myExclusionHandler::includeNode); } @Override public void excludeUsages(@NotNull Usage[] usages) { - List nodes = new ArrayList<>(usages.length); - for (Usage usage : usages) { - final UsageNode node = myUsageNodes.get(usage); - if (node != NULL_NODE && node != null) { - node.setUsageExcluded(true); - nodes.add(node); - } - } - updateImmediatelyNodesUpToRoot(nodes); + usagesToNodes(Arrays.stream(usages)) + .forEach(myExclusionHandler::excludeNode); + } + + private Stream usagesToNodes(Stream usages) { + return usages + .map(myUsageNodes::get) + .filter(node -> node != NULL_NODE && node != null); } @Override public void selectUsages(@NotNull Usage[] usages) { - List paths = new LinkedList<>(); + TreePath[] paths = usagesToNodes(Arrays.stream(usages)) + .map(node -> new TreePath(node.getPath())) + .toArray(TreePath[]::new); - for (Usage usage : usages) { - final UsageNode node = myUsageNodes.get(usage); - - if (node != NULL_NODE && node != null) { - paths.add(new TreePath(node.getPath())); - } - } - - myTree.setSelectionPaths(paths.toArray(new TreePath[paths.size()])); - if (!paths.isEmpty()) myTree.scrollPathToVisible(paths.get(0)); + myTree.setSelectionPaths(paths); + if (paths.length != 0) myTree.scrollPathToVisible(paths[0]); } @Override @@ -1177,21 +1178,24 @@ public class UsageViewImpl implements UsageView { updateOnSelectionChanged(); } - private void updateImmediatelyNodesUpToRoot(@NotNull List nodes) { + private void updateImmediatelyNodesUpToRoot(@NotNull Collection nodes) { ApplicationManager.getApplication().assertIsDispatchThread(); if (myProject.isDisposed()) return; TreeNode root = (TreeNode)myTree.getModel().getRoot(); - - for (int i=0; i updated = new HashSet<>(); + while (true) { + Set parents = new HashSet<>(); + for (Node node : nodes) { + node.update(this, edtNodeChangedQueue); TreeNode parent = node.getParent(); - if (parent != root && parent != null) { - nodes.add(parent); + if (parent != root && parent instanceof Node && updated.add((Node)parent)) { + parents.add((Node)parent); } } + if (parents.isEmpty()) break; + nodes = parents; } + updateImmediately(); } @@ -1369,7 +1373,7 @@ public class UsageViewImpl implements UsageView { return new MyPerformOperationRunnable(cannotMakeString, processRunnable, commandName, checkReadOnlyStatus); } - protected boolean allTargetsAreValid() { + private boolean allTargetsAreValid() { for (UsageTarget target : myTargets) { if (!target.isValid()) { return false; @@ -1531,6 +1535,18 @@ public class UsageViewImpl implements UsageView { } } + private static void collectAllChildNodes(@NotNull DefaultMutableTreeNode node, @NotNull Set nodes) { + if (node instanceof Node) { + nodes.add((Node)node); + } + + Enumeration enumeration = node.children(); + while (enumeration.hasMoreElements()) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode)enumeration.nextElement(); + collectAllChildNodes(child, nodes); + } + } + @Nullable private UsageTarget[] getSelectedUsageTargets() { ApplicationManager.getApplication().assertIsDispatchThread();