update usage node on exclude to fix IDEA-166924 'Exclude' in Find tool window does not exclude its children

This commit is contained in:
Alexey Kudravtsev
2017-04-04 10:17:59 +03:00
parent 434f705d0f
commit ac1996a708
4 changed files with 196 additions and 76 deletions
@@ -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<Usage> 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<Node> 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<Node> 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<Node> excluded = new HashSet<>();
TreeUtil.traverse(usageView.getRoot(), node -> {
if (((Node)node).isExcluded()) {
excluded.add((Node)node);
}
return true;
});
Set<Node> 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);
}
}
@@ -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<Node> 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<Node> edtNodeChangedQueue) {
setFlag(EXCLUDED_MASK, excluded);
edtNodeChangedQueue.consume(this);
}
}
@@ -28,6 +28,7 @@ import java.util.Arrays;
*/
public class UsageNode extends Node implements Comparable<UsageNode>, 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<UsageNode>, Navigatabl
return Arrays.asList(getUsage().getPresentation().getText()).toString();
}
}
void setUsageExcluded(boolean usageExcluded) {
setFlag(EXCLUDED_MASK, usageExcluded);
}
}
@@ -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<Usage> usages = new HashSet<>();
collectUsages(node, usages);
excludeUsages(usages.toArray(new Usage[usages.size()]));
Set<Node> 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<Node> nodes, boolean almostAllChildrenExcluded) {
TreeNode parent = node.getParent();
if (parent == myRoot || !(parent instanceof GroupNode)) return;
GroupNode parentNode = (GroupNode)parent;
List<Node> 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<Node> nodes, boolean excluded) {
for (Node node : nodes) {
node.setExcluded(excluded, edtNodeChangedQueue);
}
updateImmediatelyNodesUpToRoot(nodes);
}
@Override
public void includeNode(@NotNull DefaultMutableTreeNode node) {
final HashSet<Usage> usages = new HashSet<>();
collectUsages(node, usages);
includeUsages(usages.toArray(new Usage[usages.size()]));
Set<Node> 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<Usage> usages) {
final Set<UsageNode> nodes = new THashSet<>(usages.size());
for (Usage usage : usages) {
UsageNode node = myUsageNodes.remove(usage);
if (node != null && node != NULL_NODE) {
nodes.add(node);
}
}
Set<UsageNode> 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<TreeNode> 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<TreeNode> 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<UsageNode> usagesToNodes(Stream<Usage> usages) {
return usages
.map(myUsageNodes::get)
.filter(node -> node != NULL_NODE && node != null);
}
@Override
public void selectUsages(@NotNull Usage[] usages) {
List<TreePath> 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<TreeNode> nodes) {
private void updateImmediatelyNodesUpToRoot(@NotNull Collection<Node> nodes) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myProject.isDisposed()) return;
TreeNode root = (TreeNode)myTree.getModel().getRoot();
for (int i=0; i<nodes.size(); i++) {
TreeNode node = nodes.get(i);
if (node instanceof Node) {
((Node)node).update(this, edtNodeChangedQueue);
Set<Node> updated = new HashSet<>();
while (true) {
Set<Node> 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<Node> 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();